diff --git a/README.md b/README.md index a8dbeca..94de0ee 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,12 @@ and the first Claude reference-fleet path: - a read-only COMBATRIG compatibility importer that preserves legacy prompts, branch/worktree mappings, checks, resources, schedules, release boundaries, and backlog inventory while refusing to invent missing ownership boundaries. +- a redesigned public product site and read-only local Fleet Console backed by + the same repository and runtime state rather than a separate dashboard model. Live mixed-provider qualification, stable self-host bootstrapping, COMBATRIG -cutover, scheduling, richer observability, and the Fleet Console remain staged -work. +cutover, scheduling, richer measured context telemetry, and public deployment +remain staged work. The canonical architecture and delivery plan is [docs/PORTABLE_AGENT_FLEET_SPEC.md](docs/PORTABLE_AGENT_FLEET_SPEC.md). @@ -176,6 +178,19 @@ Claude and Codex boundaries. The tests make no paid provider calls. A live mixed-provider, multi-domain acceptance run remains an explicit owner-authorized qualification gate. +Run the redesigned public surface and local operational console on loopback: + +```bash +npm run site:serve -- --repo /path/to/project --port 4317 +``` + +The public explanation is at `/`; `/console` reads Fleet state without repair +or mutation. Automation can obtain the same read-only view with: + +```bash +node bin/torch.mjs console snapshot --repo /path/to/project --json +``` + ## Product principles - Git is required; GitHub is optional. diff --git a/docs/PORTABLE_AGENT_FLEET_SPEC.md b/docs/PORTABLE_AGENT_FLEET_SPEC.md index 253704b..2587acf 100644 --- a/docs/PORTABLE_AGENT_FLEET_SPEC.md +++ b/docs/PORTABLE_AGENT_FLEET_SPEC.md @@ -1054,6 +1054,13 @@ not treat a smaller prompt as success when verification quality declined. Doctor output MUST distinguish observation from recommended action. Repair commands require explicit invocation and normal authority checks. +Implementation checkpoint (2026-09-27): a read-only observation service and +`torch console snapshot` now expose project, health, identity, message, +backlog, check, resource, integration, schedule, and audit state without +initializing a missing database. Context-locality output explicitly reports +`unavailable` until runtime usage samples exist, preventing estimates from +masquerading as measurements. + ## 22. Scheduling TORCH recognizes two schedule classes. @@ -1450,6 +1457,15 @@ COMBATRIG is safely installed and operated through portable TORCH. Exit gate: the public surface accurately demonstrates behavior proven by the released product. +Implementation checkpoint (2026-09-27): the Nostr-era landing page and +dashboard were removed. A new responsive public surface explains the +request-to-main flow through an interactive dispatch board, publishes the +current qualification ledger, and presents repository/spec-aware startup. A +separate loopback Fleet Console consumes the read-only observation service, +uses a restrictive content-security policy, and labels context locality as +unmeasured until evidence arrives. Local HTTP, desktop, and mobile rendering +are verified; public deployment and standalone distribution remain open. + ## 29. Required acceptance configurations ### A. Laptop-only diff --git a/package.json b/package.json index a7c0008..141bc62 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "./adapters/codex": "./src/adapters/codex.mjs", "./canonical/local": "./src/canonical/local.mjs", "./checks": "./src/checks/service.mjs", + "./console": "./src/console/server.mjs", "./control-plane": "./src/control-plane/service.mjs", "./kernel/analyze": "./src/kernel/analyze.mjs", "./kernel/git": "./src/kernel/git.mjs", @@ -20,6 +21,7 @@ "./integration": "./src/integration/service.mjs", "./importers/combatrig": "./src/importers/combatrig.mjs", "./mcp": "./src/mcp/tools.mjs", + "./observability": "./src/observability/snapshot.mjs", "./resources": "./src/resources/service.mjs", "./runtime/lifecycle": "./src/runtime/lifecycle.mjs", "./self-host": "./src/self-host/service.mjs" @@ -51,25 +53,29 @@ "src/backlog/", "src/canonical/", "src/checks/", + "src/console/", "src/control-plane/", "src/kernel/", "src/integration/", "src/importers/", "src/mcp/", + "src/observability/", "src/resources/", "src/runtime/", "src/self-host/", "docs/PORTABLE_AGENT_FLEET_SPEC.md", "README.md", "LICENSE", - "torch-release.json" + "torch-release.json", + "site/" ], "scripts": { "test": "node --test --test-timeout=30000 test/fleet/*.test.mjs", "torch": "node bin/torch.mjs", - "check": "node --check bin/torch.mjs && node --check src/cli.mjs && node --check src/mcp/server.mjs", - "format": "eslint src/adapters src/backlog src/canonical src/checks src/control-plane src/importers src/integration src/kernel src/mcp src/resources src/runtime src/self-host src/cli.mjs bin/torch.mjs test/fleet --fix", - "lint": "eslint src/adapters src/backlog src/canonical src/checks src/control-plane src/importers src/integration src/kernel src/mcp src/resources src/runtime src/self-host src/cli.mjs bin/torch.mjs test/fleet" + "check": "node --check bin/torch.mjs && node --check src/cli.mjs && node --check src/mcp/server.mjs && node --check src/console/server.mjs && node --check site/app.js && node --check site/console.js", + "site:serve": "node bin/torch.mjs console serve", + "format": "eslint src/adapters src/backlog src/canonical src/checks src/console src/control-plane src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/self-host src/cli.mjs bin/torch.mjs site/*.js test/fleet --fix", + "lint": "eslint src/adapters src/backlog src/canonical src/checks src/console src/control-plane src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/self-host src/cli.mjs bin/torch.mjs site/*.js test/fleet" }, "dependencies": { "@modelcontextprotocol/server": "^2.1.0", diff --git a/reports/test-audit/test-integrity-note-2026-09-27-portable-kernel.yaml b/reports/test-audit/test-integrity-note-2026-09-27-portable-kernel.yaml index cee5aae..3399ffe 100644 --- a/reports/test-audit/test-integrity-note-2026-09-27-portable-kernel.yaml +++ b/reports/test-audit/test-integrity-note-2026-09-27-portable-kernel.yaml @@ -142,6 +142,14 @@ test_integrity_note: given: a caller outside a selected Git repository with an external specification when: torch design is invoked with explicit repo and spec paths then: a pending project-specific proposal is produced without creating TORCH state in the project + - id: SCN-product-site + given: the redesigned TORCH public surface + when: its product story and local-console boundary are inspected + then: it explains project-derived fleets, exact-commit routing, honest open gates, and measured context claims without obsolete product language + - id: SCN-console-readonly + given: an installed Fleet without an initialized control-plane database + when: the observation API and local HTTP console are requested + then: fixed assets and structured state are served with restrictive headers while Git and machine-local state remain unchanged observable_outcomes: - Git status before and after read-only analysis - presence or absence of .torch and XDG-local project state @@ -150,6 +158,8 @@ test_integrity_note: - proposal review status, repository commit binding, and generated domain count - specification digest, evidence-linked responsibility, and stale-spec refusal - compatibility report counts and unchanged COMBATRIG Git status + - HTTP status, content-security policy, fixed routes, and traversal refusal + - unchanged Git status and absent database before and after console observation - preservation of project source and modified owner configuration - SQLite state after process restart - CLI and MCP output over real process boundaries @@ -176,6 +186,7 @@ test_integrity_note: - injected post-pointer interruption with deterministic recovery timestamps - external temporary Markdown specification with no provider or network calls - synthetic COMBATRIG layout in an isolated temporary Git repository + - loopback-only ephemeral HTTP server and fixed static route map anti_cheat_rationale: prevents: - init that silently writes files @@ -205,6 +216,8 @@ test_integrity_note: - specifications being executed as instructions or changing after owner review - generic fleet templates replacing project-derived responsibilities - COMBATRIG compatibility inventing path ownership to force a green migration + - a disconnected dashboard inventing state or creating it during observation + - public copy claiming live qualification that only virtualized tests prove relaxation: did_relax_any_assertion: false if_true_explain_spec_basis: "" diff --git a/site/app.js b/site/app.js new file mode 100644 index 0000000..40020b7 --- /dev/null +++ b/site/app.js @@ -0,0 +1,47 @@ +const routeButton = document.querySelector('#route-toggle'); +const steps = [...document.querySelectorAll('.route-step')]; +const readout = document.querySelector('#route-readout'); +const routeMessages = [ + 'Owner intent recorded.', 'Session Manager assigned terrain ownership.', + 'Terrain and QA coordinate directly.', 'Checks passed for commit 8f6c2e1.', + 'Authorized fast-forward reached canonical main.', +]; +let routeTimer; + +routeButton?.addEventListener('click', () => { + clearInterval(routeTimer); + steps.forEach((step) => step.classList.remove('is-active', 'is-complete')); + routeButton.setAttribute('aria-pressed', 'true'); + routeButton.textContent = 'Routing…'; + let index = 0; + const advance = () => { + steps.forEach((step, stepIndex) => { + step.classList.toggle('is-complete', stepIndex < index); + step.classList.toggle('is-active', stepIndex === index); + }); + readout.textContent = routeMessages[index]; + index += 1; + if (index === steps.length) { + clearInterval(routeTimer); + routeButton.setAttribute('aria-pressed', 'false'); + routeButton.textContent = 'Run again'; + } + }; + advance(); + routeTimer = setInterval(advance, window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 80 : 850); +}); + +const commands = { + both: `node bin/torch.mjs design \\\n+ --repo /path/to/project \\\n+ --spec /path/to/product-spec.md \\\n+ --output fleet-proposal.json \\\n+ --json`, + repo: `node bin/torch.mjs design \\\n+ --repo /path/to/project \\\n+ --output fleet-proposal.json \\\n+ --json`, +}; +const command = document.querySelector('#start-command'); +document.querySelectorAll('[data-command]').forEach((button) => button.addEventListener('click', () => { + document.querySelectorAll('[data-command]').forEach((tab) => tab.setAttribute('aria-selected', String(tab === button))); + command.textContent = commands[button.dataset.command]; +})); +document.querySelector('#copy-command')?.addEventListener('click', async (event) => { + await navigator.clipboard.writeText(command.textContent); + event.currentTarget.textContent = 'Copied'; + setTimeout(() => { event.currentTarget.textContent = 'Copy command'; }, 1500); +}); diff --git a/site/console.html b/site/console.html new file mode 100644 index 0000000..c8f1d10 --- /dev/null +++ b/site/console.html @@ -0,0 +1,36 @@ + + + + + + + TORCH Fleet Console + + + + + +
+ T TORCH +

Local Fleet Console

+ +
+
+
+

Project

Loading…
+

Commit

—
+

Fleet health

Observing
+

Context locality

Unmeasured
+
+ +
+

Fleet

0 identities
+

Backlog

0 active
+

Integration

Exact commits
+

Resources

FIFO leases
+

Messages

0 queued
+

Context locality

Measured, never assumed
+
+
+ + diff --git a/site/console.js b/site/console.js new file mode 100644 index 0000000..838df44 --- /dev/null +++ b/site/console.js @@ -0,0 +1,71 @@ +const $ = (selector) => document.querySelector(selector); + +function empty(message) { + return `

${message}

`; +} + +function safe(value) { + const element = document.createElement('span'); + element.textContent = value ?? ''; + return element.innerHTML; +} + +function render(snapshot) { + const project = snapshot.project ?? snapshot.repository; + $('#project-name').textContent = project?.name ?? project?.id ?? 'Not installed'; + $('#project-commit').textContent = snapshot.repository?.head?.slice(0, 9) ?? '—'; + $('#fleet-health').textContent = snapshot.mode === 'installed' + ? (snapshot.doctor?.healthy ? 'Healthy' : 'Needs attention') : 'Not installed'; + $('#context-state').textContent = snapshot.contextLocality?.measured ? 'Measured' : 'Unmeasured'; + + const agents = snapshot.agents ?? []; + $('#agent-count').textContent = `${agents.length} ${agents.length === 1 ? 'identity' : 'identities'}`; + $('#agent-list').innerHTML = agents.length ? agents.map((agent) => ` +
${safe(agent.title ?? agent.areaId)}${safe(agent.summary ?? `${agent.runtime ?? 'runtime'} · ${agent.state}`)}
${safe(agent.areaId)}
+ `).join('') : empty('Install an approved fleet to see persistent identities.'); + + const tasks = (snapshot.backlog ?? []).filter((task) => !['completed', 'cancelled'].includes(task.state)); + $('#task-count').textContent = `${tasks.length} active`; + $('#task-list').innerHTML = tasks.length ? tasks.slice(0, 12).map((task) => ` +
${safe(task.title ?? task.id)}${safe(task.owner ?? 'unassigned')}
${safe(task.state)}
+ `).join('') : empty('No active tracked work.'); + + const integrations = snapshot.integration ?? []; + $('#integration-list').innerHTML = integrations.length ? integrations.map((item) => ` +
${safe(item.sourceArea)}${safe(item.sourceCommit?.slice(0, 9))}
${safe(item.state)}
+ `).join('') : empty('No commits waiting to land.'); + + const holders = snapshot.resources?.holders ?? []; + const waiters = snapshot.resources?.waiters ?? []; + $('#resource-list').innerHTML = holders.length || waiters.length + ? [...holders.map((item) => `
${safe(item.resourceId)}${safe(item.areaId)} holds
`), + ...waiters.map((item) => `
${safe(item.resourceId)}${safe(item.areaId)} waits
`)].join('') + : empty('All configured resources are available.'); + + const messages = snapshot.messages?.recent ?? []; + $('#message-count').textContent = `${snapshot.messages?.unacknowledged ?? 0} unacknowledged`; + $('#message-list').innerHTML = messages.length ? messages.map((message) => ` +

${safe(message.sender)} to ${safe(message.recipient)}

${safe(message.body)}
+ `).join('') : empty('No durable messages yet.'); + + const locality = snapshot.contextLocality; + $('#context-detail').innerHTML = locality?.measured + ? locality.samples.map((sample) => `
${safe(sample.areaId)}${safe(String(sample.cacheRead ?? 0))} cached tokens
`).join('') + : `

${safe(locality?.reason ?? 'Runtime usage reporting is not available.')}

TORCH will compare cache activity with verified outcomes, not token count alone.`; +} + +async function refresh() { + $('#console-error').hidden = true; + try { + const response = await fetch('/api/snapshot', { cache: 'no-store' }); + const snapshot = await response.json(); + if (!response.ok) throw new Error(snapshot.message ?? snapshot.error); + render(snapshot); + } catch (error) { + $('#console-error').textContent = `Snapshot unavailable: ${error.message}`; + $('#console-error').hidden = false; + } +} + +$('#refresh-console')?.addEventListener('click', refresh); +refresh(); diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..d2853b1 --- /dev/null +++ b/site/index.html @@ -0,0 +1,133 @@ + + + + + + + TORCH — one repository, a coordinated AI engineering team + + + + + +
+ T TORCH + +
+ +
+
+
+

Portable fleet alpha

+

One repository.
A coordinated AI engineering team.

+

TORCH studies your code and project specification, proposes durable areas of responsibility, and gives each specialist its own branch, worktree, context, and route back to main.

+ +
+ +
+
+

Request CR-184

+ +
+
    +
  1. A
    Owner requestChange terrain streaming without weakening frame checks.
  2. +
  3. S
    Session ManagerRoutes ownership and establishes acceptance.
  4. +
  5. T
    Terrain specialistWorks in its persistent domain context.
    QA peer
  6. +
  7. ✓
    Exact-commit checksReceipts bind evidence to the measured SHA.
  8. +
  9. M
    Controlled mainAuthorized fast-forward after convergence.
  10. +
+

Ready at owner request.

+
+
+ +
+

Git required

GitHub optional

Provider independent

Local first

Human approved

+
+ +
+
+

The project decides the team.

+

TORCH does not install a catalog of generic agents. It reads the repository and the specifications you select, then produces a pending organization for you to split, merge, rename, or reject.

+
+
+
+ Input +

Code and intent

+

Architecture, tests, operations, scarce resources, and external Markdown specifications become evidence—not instructions to execute.

+
+
+ Design +

Explicit boundaries

+

Every domain receives scope, exclusions, owned paths, neighbors, checks, runtime policy, and collision rules.

+
+
+ Operate +

Persistent specialists

+

Stable identities survive runtime sessions. Durable messages and project state outlive model context.

+
+
+
+ +
+ +
+

Keep useful context hot.

+

A specialist can retain the working set for one coherent area instead of repeatedly loading unrelated project history. TORCH measures cache reads, cache creation, compactions, resumed prompt size, cost, and verified outcomes before calling that an efficiency win.

+

Context savings are a measured hypothesis, not a marketing claim.

+
+
+ +
+
+

Proof has a commit.

+

TORCH distinguishes implementation from qualification. Green output without the right scenario, authority, and SHA is not proof.

+
+
+
CapabilityCurrent evidenceState
+
Portable kernel and reversalScenario testedProven locally
+
Durable control plane and backlogSQLite + tracked tasksProven locally
+
Checks, resources, integrationExact-SHA lifecycleProven locally
+
Claude and Codex coordinationVirtualized adaptersLive gate open
+
Self-managed successorAtomic candidate mechanicsStable boot open
+
COMBATRIG migration33 areas, 570 tasks inventoriedBoundaries open
+
+
+ +
+
+

Start with the work you actually have.

+

Point TORCH at a Git repository, a product specification, or both. Design is read-only; installation waits for your reviewed proposal.

+
+
+
+ + +
+
node bin/torch.mjs design \
+  --repo /path/to/project \
+  --spec /path/to/product-spec.md \
+  --output fleet-proposal.json \
+  --json
+ +
+
+
+ + + + diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..b5431c6 --- /dev/null +++ b/site/styles.css @@ -0,0 +1,172 @@ +:root { + color-scheme: light; + --surface: #e7eef2; + --paper: #f8fbfc; + --ink: #102a3a; + --muted: #557080; + --line: #a8bbc5; + --signal: #f05a28; + --route: #246bfe; + --field: #2e7d5a; + --shadow: 0 18px 50px rgba(16, 42, 58, 0.12); + --display: "Bahnschrift", "DIN Alternate", "Arial Narrow", sans-serif; + --body: "Source Sans 3", "Noto Sans", "Segoe UI", sans-serif; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; background: var(--surface); color: var(--ink); font-family: var(--body); font-size: 17px; line-height: 1.55; } +a { color: inherit; text-underline-offset: 0.22em; } +button { font: inherit; } +.skip-link { position: fixed; left: 1rem; top: -5rem; z-index: 30; background: var(--ink); color: white; padding: .65rem 1rem; } +.skip-link:focus { top: 1rem; } +:focus-visible { outline: 3px solid var(--signal); outline-offset: 3px; } + +.masthead { min-height: 78px; display: flex; align-items: center; justify-content: space-between; gap: 2rem; padding: 0 clamp(1.25rem, 4vw, 5rem); border-bottom: 1px solid var(--line); } +.wordmark { display: flex; align-items: center; gap: .7rem; font-family: var(--display); font-weight: 700; letter-spacing: .04em; text-decoration: none; } +.mark { display: grid; place-items: center; width: 2rem; height: 2rem; background: var(--signal); color: white; clip-path: polygon(50% 0, 100% 28%, 82% 100%, 18% 100%, 0 28%); } +nav { display: flex; gap: clamp(1rem, 3vw, 2.4rem); font-size: .92rem; } +nav a { text-decoration: none; } +nav a:hover { text-decoration: underline; } + +.hero { min-height: min(calc(100vh - 78px), 900px); display: grid; grid-template-columns: minmax(0, 1fr) minmax(440px, 1fr); align-items: center; gap: clamp(3rem, 6vw, 8rem); padding: clamp(4rem, 7vw, 7rem) clamp(1.25rem, 6vw, 7rem); } +.hero-copy { max-width: 700px; } +.status-line { display: flex; align-items: center; gap: .65rem; color: var(--muted); font-weight: 650; } +.status-line span { width: .72rem; height: .72rem; border-radius: 50%; background: var(--field); box-shadow: 0 0 0 .28rem rgba(46, 125, 90, .15); } +h1, h2, h3 { font-family: var(--display); font-weight: 650; line-height: .98; margin: 0; } +h1 { font-size: clamp(3.3rem, 6vw, 6.6rem); letter-spacing: -.055em; max-width: 12ch; } +h2 { font-size: clamp(2.5rem, 5vw, 5.2rem); letter-spacing: -.04em; max-width: 13ch; } +h3 { font-size: 1.55rem; line-height: 1.15; } +.lede { max-width: 630px; font-size: clamp(1.1rem, 1.6vw, 1.38rem); color: #2e4a5a; margin: 2rem 0; } +.hero-actions { display: flex; align-items: center; gap: 1.5rem; } +.primary-action { display: inline-flex; background: var(--ink); color: white; padding: .82rem 1.15rem; text-decoration: none; font-weight: 700; } +.primary-action:hover { background: var(--route); } +.text-action { font-weight: 650; } + +.dispatch { background: var(--paper); border: 1px solid var(--ink); box-shadow: var(--shadow); } +.dispatch-head { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.2rem; border-bottom: 1px solid var(--line); } +.dispatch-head p { margin: 0; font-family: var(--display); font-weight: 650; } +.dispatch-head button, .console-nav button, .copy-button { border: 1px solid var(--ink); background: transparent; color: var(--ink); padding: .45rem .72rem; cursor: pointer; font-weight: 650; } +.dispatch-head button:hover, .console-nav button:hover, .copy-button:hover { background: var(--ink); color: white; } +.route { list-style: none; margin: 0; padding: 1.2rem 1.4rem; } +.route-step { position: relative; display: grid; grid-template-columns: 44px 1fr auto; align-items: center; gap: 1rem; min-height: 88px; opacity: .58; } +.route-step:not(:last-child)::after { content: ""; position: absolute; left: 21px; top: 65px; height: 48px; width: 2px; background: var(--line); } +.route-step.is-active, .route-step.is-complete, .route-step.is-ready { opacity: 1; } +.route-step.is-active .node { background: var(--signal); color: white; border-color: var(--signal); transform: scale(1.12); } +.route-step.is-complete .node { background: var(--field); color: white; border-color: var(--field); } +.route-step.is-complete::after { background: var(--field); } +.node { display: grid; place-items: center; width: 44px; height: 44px; border: 2px solid var(--route); border-radius: 50%; font-family: var(--display); font-weight: 700; transition: transform .2s, background .2s; } +.route-step strong, .route-step small { display: block; } +.route-step small { color: var(--muted); margin-top: .15rem; } +.peer { border: 1px solid var(--route); color: var(--route); padding: .25rem .5rem; font-size: .78rem; } +.route-readout { margin: 0; padding: .85rem 1.4rem; border-top: 1px solid var(--line); color: var(--muted); font-size: .9rem; } + +.principle-strip { display: grid; grid-template-columns: repeat(5, 1fr); border-block: 1px solid var(--line); } +.principle-strip p { margin: 0; padding: 1rem; text-align: center; border-right: 1px solid var(--line); font-size: .9rem; font-weight: 650; } +.principle-strip p:last-child { border: 0; } +.system-section, .evidence-section { padding: clamp(5rem, 10vw, 10rem) clamp(1.25rem, 6vw, 7rem); } +.section-intro { display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; align-items: end; margin-bottom: 4rem; } +.section-intro p { margin: 0; max-width: 650px; font-size: 1.18rem; color: #2e4a5a; } +.blueprint { display: grid; grid-template-columns: repeat(3, 1fr); background-image: linear-gradient(var(--line) 1px, transparent 1px), linear-gradient(90deg, var(--line) 1px, transparent 1px); background-size: 24px 24px; border: 1px solid var(--line); } +.blueprint article { min-height: 330px; padding: clamp(1.5rem, 3vw, 3rem); background: rgba(248, 251, 252, .93); border-right: 1px solid var(--line); } +.blueprint article:last-child { border: 0; } +.blueprint-index { display: inline-block; margin-bottom: 5rem; color: var(--route); border-bottom: 2px solid currentColor; font-weight: 700; } +.blueprint p { color: var(--muted); } + +.context-section { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(3rem, 7vw, 8rem); align-items: center; padding: clamp(5rem, 9vw, 9rem) clamp(1.25rem, 6vw, 7rem); background: var(--ink); color: var(--paper); } +.context-section p { max-width: 650px; color: #c1d0d8; font-size: 1.14rem; } +.truth-note { display: inline-block; border-left: 3px solid var(--signal); padding-left: 1rem; } +.context-diagram { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; min-height: 360px; } +.wide-context { position: relative; display: grid; place-items: center; height: 250px; border: 1px solid #6e8795; overflow: hidden; font-family: var(--display); } +.churn { position: absolute; width: 180%; height: 18px; background: var(--signal); transform: rotate(23deg); opacity: .72; } +.churn.second { transform: rotate(-28deg); background: var(--route); } +.churn.third { transform: rotate(66deg); background: #a8bbc5; } +.versus { color: #8fa5b1; font-size: .82rem; writing-mode: vertical-rl; } +.focused-context { display: grid; gap: .7rem; } +.focused-context span { display: grid; place-items: center; min-height: 74px; border: 1px solid #8fa5b1; background: #18384a; font-family: var(--display); } + +.evidence-section { background: var(--paper); } +.ledger { border-top: 2px solid var(--ink); } +.ledger-row { display: grid; grid-template-columns: 1.4fr 1fr .65fr; gap: 2rem; align-items: center; padding: 1rem .2rem; border-bottom: 1px solid var(--line); } +.ledger-head { font-size: .82rem; color: var(--muted); font-weight: 700; } +.ledger-row strong { justify-self: start; padding: .24rem .5rem; font-size: .8rem; } +.proven { color: var(--field); border: 1px solid currentColor; } +.pending { color: #a64a28; border: 1px solid currentColor; } + +.start-section { display: grid; grid-template-columns: .8fr 1.2fr; gap: clamp(3rem, 8vw, 9rem); padding: clamp(5rem, 9vw, 9rem) clamp(1.25rem, 6vw, 7rem); } +.start-section p { color: var(--muted); max-width: 600px; } +.command-panel { background: #0d2534; color: #dbe6eb; box-shadow: var(--shadow); } +.command-tabs { display: flex; border-bottom: 1px solid #446170; } +.command-tabs button { border: 0; border-right: 1px solid #446170; background: transparent; color: #9eb2bc; padding: .75rem 1rem; cursor: pointer; } +.command-tabs button[aria-selected="true"] { background: #17394b; color: white; } +pre { margin: 0; padding: 1.6rem; min-height: 190px; overflow: auto; font-size: .88rem; line-height: 1.7; } +.copy-button { margin: 0 1.4rem 1.4rem; border-color: #9eb2bc; color: #dbe6eb; } +footer { display: flex; justify-content: space-between; gap: 2rem; padding: 2.2rem clamp(1.25rem, 6vw, 7rem); border-top: 1px solid var(--line); } +footer div { display: flex; gap: 1.5rem; } + +.console-body { min-height: 100vh; background: #dce7ec; } +.console-nav p { margin: 0 auto 0 0; color: var(--muted); } +.console-shell { padding: 2rem clamp(1rem, 4vw, 4rem) 5rem; } +.console-summary { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); background: var(--paper); } +.console-summary div { padding: 1.1rem 1.3rem; border-right: 1px solid var(--line); } +.console-summary div:last-child { border: 0; } +.console-summary p { margin: 0 0 .3rem; color: var(--muted); font-size: .78rem; } +.console-summary strong { font-family: var(--display); font-size: 1.12rem; } +.console-error { background: #ffe4da; border-left: 4px solid var(--signal); padding: 1rem; } +.console-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-top: 1rem; } +.console-panel { min-height: 280px; padding: 1.2rem; border: 1px solid var(--line); background: var(--paper); } +.agents-panel, .messages-panel { grid-column: span 2; } +.panel-heading { display: flex; justify-content: space-between; align-items: baseline; border-bottom: 1px solid var(--line); padding-bottom: .8rem; } +.panel-heading h1, .panel-heading h2 { font-size: 1.45rem; letter-spacing: -.02em; } +.panel-heading span { color: var(--muted); font-size: .78rem; } +.agent-list { display: grid; grid-template-columns: repeat(2, 1fr); column-gap: 2rem; } +.agent-row { display: grid; grid-template-columns: 12px 1fr auto; gap: .75rem; align-items: center; padding: .8rem 0; border-bottom: 1px solid #d7e1e6; } +.agent-row small { display: block; color: var(--muted); } +.agent-row code { font-size: .75rem; } +.presence { width: 10px; height: 10px; border-radius: 50%; background: #8297a2; } +.presence.working, .presence.online { background: var(--field); } +.presence.blocked { background: var(--signal); } +.list-row, .message-row, .locality-row { display: flex; justify-content: space-between; gap: 1rem; padding: .72rem 0; border-bottom: 1px solid #d7e1e6; } +.list-row small { display: block; color: var(--muted); } +.list-row > span { color: var(--route); font-size: .8rem; } +.message-row { display: block; } +.message-row p { margin: 0; font-size: .8rem; color: var(--muted); } +.message-row span { display: block; margin-top: .25rem; } +.empty-state { color: var(--muted); max-width: 40ch; padding: 1rem 0; } +.context-detail p { max-width: 55ch; } +.context-detail small { color: var(--muted); } + +@media (max-width: 900px) { + .hero { grid-template-columns: 1fr; min-height: auto; } + .dispatch { max-width: 650px; } + .principle-strip { grid-template-columns: 1fr 1fr; } + .principle-strip p { border-bottom: 1px solid var(--line); } + .section-intro, .context-section, .start-section { grid-template-columns: 1fr; } + .blueprint { grid-template-columns: 1fr; } + .blueprint article { min-height: auto; border-right: 0; border-bottom: 1px solid var(--line); } + .blueprint-index { margin-bottom: 2rem; } + .console-summary { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 600px) { + body { font-size: 16px; } + .masthead { padding-block: 1rem; align-items: flex-start; } + nav { display: grid; grid-template-columns: 1fr 1fr; gap: .4rem 1rem; } + .hero { padding-top: 4rem; } + h1 { font-size: clamp(3rem, 13vw, 5.2rem); } + .hero-actions, footer { align-items: flex-start; flex-direction: column; } + .peer { display: none; } + .ledger-row { grid-template-columns: 1fr; gap: .25rem; } + .ledger-head { display: none; } + .context-diagram { grid-template-columns: 1fr; } + .versus { writing-mode: initial; text-align: center; } + .console-summary, .console-grid, .agent-list { grid-template-columns: 1fr; } + .console-summary div { border-right: 0; border-bottom: 1px solid var(--line); } + .agents-panel, .messages-panel { grid-column: auto; } + .console-nav p { display: none; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; } +} diff --git a/src/cli.mjs b/src/cli.mjs index 7938dba..f89c678 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -3,6 +3,7 @@ import { BacklogService } from './backlog/service.mjs'; import { createClaudeAdapter } from './adapters/claude.mjs'; import { createCodexAdapter } from './adapters/codex.mjs'; import { CheckService } from './checks/service.mjs'; +import { startConsole } from './console/server.mjs'; import { classifyRecoverability, createLocalCanonical, planLocalCanonical } from './canonical/local.mjs'; import { openControlPlane } from './control-plane/service.mjs'; import { spawnSync } from 'node:child_process'; @@ -21,6 +22,7 @@ import { createWorktrees, planWorktrees } from './kernel/worktrees.mjs'; import { planFleetDown, planFleetUp, startFleet, stopFleet } from './runtime/lifecycle.mjs'; import { ResourceService } from './resources/service.mjs'; import { CANDIDATE_ACCEPTANCE_SCENARIOS, createVersionService } from './self-host/service.mjs'; +import { observeProject } from './observability/snapshot.mjs'; const HELP = `TORCH — portable agent fleet @@ -76,6 +78,8 @@ Usage: torch upgrade --version [--dry-run] --yes [--json] torch rollback [--dry-run] --yes [--json] torch import combatrig --source [--output ] [--json] + torch console snapshot [--repo ] [--json] + torch console serve [--repo ] [--host
] [--port ] torch doctor [--json] torch uninstall [--dry-run] [--purge] [--json] `; @@ -164,6 +168,7 @@ function runCandidateAcceptance(candidateRoot, spawn) { ['capture-stop-resume', ['SCN-fleet-fresh-resume']], ['detach-uninstall', ['SCN-install-doctor-purge']], ['combatrig-compatibility', ['SCN-combatrig-import', 'SCN-cli-combatrig-import']], + ['product-surface', ['SCN-product-site', 'SCN-console-readonly']], ]); const scenarios = CANDIDATE_ACCEPTANCE_SCENARIOS.filter((scenario) => (evidenceByScenario.get(scenario) ?? []).every((marker) => testOutput.includes(marker))); @@ -285,6 +290,24 @@ export async function runCli(argv = process.argv.slice(2), { } else print({ proposal, mutationPerformed: false }, { json }); return proposal.compatibility.readyForApproval ? 0 : 1; } + if (command === 'console') { + const operation = argv[1] ?? 'serve'; + const root = resolve(cwd, optionValue(argv, '--repo') ?? '.'); + if (operation === 'snapshot') { + print(observeProject({ repositoryRoot: root, env }), { json }); + return 0; + } + if (operation === 'serve') { + const host = optionValue(argv, '--host') ?? '127.0.0.1'; + const port = Number(optionValue(argv, '--port') ?? 4317); + if (!Number.isInteger(port) || port < 0 || port > 65535) { + throw new TorchError('Console port must be an integer from 0 through 65535', { code: 'CONSOLE_PORT_INVALID' }); + } + startConsole({ repositoryRoot: root, env, host, port }); + return 0; + } + throw new TorchError(`Unknown console operation: ${operation}`, { code: 'UNKNOWN_COMMAND' }); + } const designCommand = ['init', 'analyze', 'domains', 'design'].includes(command); const repositoryRoot = designCommand ? resolve(cwd, optionValue(argv, '--repo') ?? '.') : cwd; const repository = inspectRepository(repositoryRoot); diff --git a/src/console/server.mjs b/src/console/server.mjs new file mode 100644 index 0000000..108aadd --- /dev/null +++ b/src/console/server.mjs @@ -0,0 +1,60 @@ +import { createServer } from 'node:http'; +import { readFileSync } from 'node:fs'; +import { dirname, extname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { observeProject } from '../observability/snapshot.mjs'; + +const SITE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'site'); +const ROUTES = new Map([ + ['/', 'index.html'], ['/index.html', 'index.html'], ['/styles.css', 'styles.css'], + ['/app.js', 'app.js'], ['/console', 'console.html'], ['/console.html', 'console.html'], + ['/console.js', 'console.js'], +]); +const TYPES = new Map([ + ['.html', 'text/html; charset=utf-8'], ['.css', 'text/css; charset=utf-8'], + ['.js', 'text/javascript; charset=utf-8'], ['.svg', 'image/svg+xml'], +]); + +function respond(response, status, body, type = 'application/json; charset=utf-8') { + response.writeHead(status, { + 'content-type': type, + 'cache-control': 'no-store', + 'x-content-type-options': 'nosniff', + 'content-security-policy': "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'", + }); + response.end(body); +} + +export function createConsoleServer({ repositoryRoot = process.cwd(), env = process.env } = {}) { + return createServer((request, response) => { + const url = new URL(request.url ?? '/', 'http://torch.local'); + if (request.method !== 'GET') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (url.pathname === '/api/snapshot') { + try { + respond(response, 200, `${JSON.stringify(observeProject({ repositoryRoot, env }))}\n`); + } catch (error) { + respond(response, 503, `${JSON.stringify({ error: error.code ?? 'OBSERVATION_FAILED', message: error.message })}\n`); + } + return; + } + const file = ROUTES.get(url.pathname); + if (!file) { + respond(response, 404, 'Not found\n', 'text/plain; charset=utf-8'); + return; + } + const path = join(SITE_ROOT, file); + respond(response, 200, readFileSync(path), TYPES.get(extname(path)) ?? 'application/octet-stream'); + }); +} + +export function startConsole({ repositoryRoot = process.cwd(), env = process.env, host = '127.0.0.1', port = 4317 } = {}) { + const server = createConsoleServer({ repositoryRoot, env }); + server.listen(port, host, () => { + const address = server.address(); + process.stdout.write(`TORCH console: http://${host}:${address.port}/console\n`); + }); + return server; +} diff --git a/src/observability/snapshot.mjs b/src/observability/snapshot.mjs new file mode 100644 index 0000000..474d6e5 --- /dev/null +++ b/src/observability/snapshot.mjs @@ -0,0 +1,156 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; +import { analyzeRepository } from '../kernel/analyze.mjs'; +import { diagnoseProject } from '../kernel/doctor.mjs'; +import { inspectRepository } from '../kernel/git.mjs'; +import { readInstallManifest } from '../kernel/install.mjs'; +import { projectStatePath } from '../kernel/paths.mjs'; + +function json(path) { + return JSON.parse(readFileSync(path, 'utf8')); +} + +function hasTable(database, name) { + return Boolean(database.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(name)); +} + +function rows(database, table, statement) { + return hasTable(database, table) ? database.prepare(statement).all() : []; +} + +function backlog(repositoryRoot, manifest) { + const manager = (manifest.external ?? []).find((entry) => entry.type === 'worktree' && entry.area === 'session-manager'); + const root = join(manager?.path && existsSync(manager.path) ? manager.path : repositoryRoot, '.torch', 'backlog'); + if (!existsSync(root)) return []; + const tasks = []; + for (const name of readdirSync(root).filter((entry) => entry.endsWith('.json')).sort()) { + try { + const task = json(join(root, name)); + tasks.push({ + id: task.id, title: task.title, state: task.state, owner: task.owner ?? null, + priority: task.priority ?? null, revision: task.revision ?? null, + }); + } catch { + tasks.push({ id: name.slice(0, -5), title: 'Unreadable task', state: 'invalid', owner: null }); + } + } + return tasks; +} + +export function observeProject({ repositoryRoot, env = process.env, now = () => new Date() } = {}) { + const repository = inspectRepository(repositoryRoot); + const installed = existsSync(join(repository.root, '.torch', 'torch.yaml')); + if (!installed) { + const analysis = analyzeRepository(repository); + return { + schema: 'torch.dev/observation/v1alpha1', generatedAt: now().toISOString(), + mode: 'not-installed', mutationPerformed: false, + repository: analysis.repository, + inventory: analysis.inventory, + contextLocality: { + measured: false, status: 'unavailable', + reason: 'Install a Fleet and connect runtime usage reporting before comparing context locality.', + }, + }; + } + + const config = json(join(repository.root, '.torch', 'torch.yaml')); + const roster = json(join(repository.root, '.torch', 'roster.yaml')); + const manifest = readInstallManifest(repository.root); + const stateRoot = projectStatePath(manifest.projectId, env); + const databasePath = join(stateRoot, 'state.db'); + const result = { + schema: 'torch.dev/observation/v1alpha1', generatedAt: now().toISOString(), + mode: 'installed', mutationPerformed: false, + project: config.project, + repository: { + root: repository.root, branch: repository.branch, head: repository.head, + dirty: repository.dirtyEntries.length > 0, + }, + doctor: diagnoseProject({ repository, env }), + agents: (roster.areas ?? []).map((area) => ({ + areaId: area.id, title: area.title, state: 'offline', runtime: area.runtime ?? null, + runtimeSessionId: null, summary: null, task: null, heartbeatAt: null, + })), + messages: { total: 0, unacknowledged: 0, recent: [] }, + backlog: backlog(repository.root, manifest), + checks: { definitions: config.checks ?? [], receipts: [] }, + resources: { definitions: config.resources ?? [], holders: [], waiters: [] }, + integration: [], + schedules: config.schedules ?? [], + audit: [], + contextLocality: { + measured: false, status: 'unavailable', + reason: 'No runtime usage samples have been reported for this Fleet.', + metrics: ['cachedInput', 'uncachedInput', 'cacheCreation', 'cacheRead', 'compactions', 'resumedPromptBytes', 'costPerVerifiedItem'], + }, + }; + if (!existsSync(databasePath)) return result; + + const database = new DatabaseSync(databasePath, { readOnly: true }); + try { + if (hasTable(database, 'identities')) { + const live = new Map(database.prepare(` + SELECT area_id, state, runtime, runtime_session_id, summary, current_task AS task, heartbeat_at + FROM identities ORDER BY area_id + `).all().map((row) => [row.area_id, row])); + result.agents = result.agents.map((agent) => { + const row = live.get(agent.areaId); + return row ? { + ...agent, state: row.state, runtime: row.runtime ?? agent.runtime, + runtimeSessionId: row.runtime_session_id, summary: row.summary, + task: row.task, heartbeatAt: row.heartbeat_at, + } : agent; + }); + } + if (hasTable(database, 'messages')) { + result.messages.total = database.prepare('SELECT COUNT(*) AS count FROM messages').get().count; + result.messages.recent = database.prepare(` + SELECT id, sender_id AS sender, recipient_id AS recipient, kind, body, created_at AS createdAt + FROM messages ORDER BY created_at DESC LIMIT 20 + `).all(); + if (hasTable(database, 'message_acks') && hasTable(database, 'identities')) { + result.messages.unacknowledged = database.prepare(` + SELECT COUNT(*) AS count FROM messages m + JOIN identities i ON m.recipient_id = i.area_id OR m.recipient_id = 'all' + LEFT JOIN message_acks a ON a.message_id = m.id AND a.area_id = i.area_id + WHERE a.message_id IS NULL + `).get().count; + } + } + result.checks.receipts = rows(database, 'check_receipts', ` + SELECT id, check_id AS checkId, area_id AS areaId, commit_sha AS commit, result, finished_at AS finishedAt + FROM check_receipts ORDER BY finished_at DESC LIMIT 40 + `); + result.resources.holders = rows(database, 'resource_leases', ` + SELECT resource_id AS resourceId, area_id AS areaId, acquired_at AS acquiredAt, expires_at AS expiresAt + FROM resource_leases WHERE released_at IS NULL ORDER BY acquired_at + `); + result.resources.waiters = rows(database, 'resource_requests', ` + SELECT resource_id AS resourceId, area_id AS areaId, requested_at AS requestedAt + FROM resource_requests WHERE state = 'waiting' ORDER BY requested_at + `); + result.integration = rows(database, 'integration_requests', ` + SELECT id, source_area AS sourceArea, source_commit AS sourceCommit, target_branch AS targetBranch, state, reason, updated_at AS updatedAt + FROM integration_requests ORDER BY updated_at DESC LIMIT 40 + `); + result.audit = rows(database, 'audit_events', ` + SELECT id, actor_id AS actorId, operation, entity_type AS entityType, entity_id AS entityId, created_at AS createdAt + FROM audit_events ORDER BY created_at DESC LIMIT 30 + `); + if (hasTable(database, 'context_usage')) { + const samples = database.prepare(` + SELECT area_id AS areaId, SUM(cached_input) AS cachedInput, SUM(uncached_input) AS uncachedInput, + SUM(cache_creation) AS cacheCreation, SUM(cache_read) AS cacheRead, + SUM(compactions) AS compactions, SUM(resumed_prompt_bytes) AS resumedPromptBytes, + SUM(cost_microusd) AS costMicrousd, SUM(verified_items) AS verifiedItems + FROM context_usage GROUP BY area_id ORDER BY area_id + `).all(); + result.contextLocality = { measured: true, status: 'measured', samples }; + } + } finally { + database.close(); + } + return result; +} diff --git a/src/self-host/service.mjs b/src/self-host/service.mjs index ae11ebb..34cd18f 100644 --- a/src/self-host/service.mjs +++ b/src/self-host/service.mjs @@ -21,6 +21,7 @@ export const CANDIDATE_ACCEPTANCE_SCENARIOS = Object.freeze([ 'capture-stop-resume', 'detach-uninstall', 'combatrig-compatibility', + 'product-surface', ]); function json(path) { diff --git a/test/fleet/cli-scenarios.test.mjs b/test/fleet/cli-scenarios.test.mjs index 22bfdd9..fbf0bcc 100644 --- a/test/fleet/cli-scenarios.test.mjs +++ b/test/fleet/cli-scenarios.test.mjs @@ -37,6 +37,7 @@ function releaseCandidate(root, version) { 'SCN-backlog-lifecycle', 'SCN-native-integration', 'SCN-resource-fifo', 'SCN-fleet-fresh-resume', 'SCN-install-doctor-purge', 'SCN-combatrig-import', 'SCN-cli-combatrig-import', + 'SCN-product-site', 'SCN-console-readonly', ].join('\n'))});\n`); writeFileSync(join(candidateRoot, 'package.json'), `${JSON.stringify({ name: 'torch-agent-fleet', version, bin: { torch: 'bin/torch.mjs' }, diff --git a/test/fleet/product-surface.test.mjs b/test/fleet/product-surface.test.mjs new file mode 100644 index 0000000..bb55315 --- /dev/null +++ b/test/fleet/product-surface.test.mjs @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { createConsoleServer } from '../../src/console/server.mjs'; +import { analyzeRepository } from '../../src/kernel/analyze.mjs'; +import { proposeDomains } from '../../src/kernel/domains.mjs'; +import { inspectRepository } from '../../src/kernel/git.mjs'; +import { installProject } from '../../src/kernel/install.mjs'; +import { observeProject } from '../../src/observability/snapshot.mjs'; + +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'torch-console-')); + execFileSync('git', ['init', '-b', 'main', root]); + execFileSync('git', ['-C', root, 'config', 'user.email', 'torch-test@example.invalid']); + execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']); + writeFileSync(join(root, 'README.md'), '# Console fixture\n'); + execFileSync('git', ['-C', root, 'add', '.']); + execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']); + return root; +} + +test('SCN-product-site: public surface explains the portable fleet and separates the local console', () => { + const html = readFileSync(new URL('../../site/index.html', import.meta.url), 'utf8'); + const consoleHtml = readFileSync(new URL('../../site/console.html', import.meta.url), 'utf8'); + assert.match(html, /One repository[\s\S]*coordinated AI engineering team/); + assert.match(html, /id="route-toggle"/); + assert.match(html, /Exact-commit checks/); + assert.match(html, /Live gate open/); + assert.match(html, /Context savings are a measured hypothesis/); + assert.doesNotMatch(html, /nostr|relay coordination|task lock/i); + assert.match(consoleHtml, /Local Fleet Console/); + assert.match(consoleHtml, /noindex,nofollow/); +}); + +test('SCN-console-readonly: HTTP console serves fixed assets and observes an installed fleet without mutation', async (context) => { + const root = fixture(); + const env = { ...process.env, XDG_DATA_HOME: mkdtempSync(join(tmpdir(), 'torch-console-state-')) }; + const repository = inspectRepository(root); + const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) }); + proposal.review = { + status: 'approved', reviewedAt: '2026-09-27T00:00:00Z', reviewedBy: 'fixture-owner', notes: [], + }; + installProject({ repository, proposal, env, projectId: 'console-fixture' }); + const databasePath = join(env.XDG_DATA_HOME, 'torch', 'projects', 'console-fixture', 'state.db'); + assert.equal(existsSync(databasePath), false); + const before = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' }); + const observed = observeProject({ repositoryRoot: root, env, now: () => new Date('2026-09-27T00:00:00Z') }); + assert.equal(observed.mode, 'installed'); + assert.equal(observed.mutationPerformed, false); + assert.equal(observed.agents.length, proposal.domains.length + 1); + assert.equal(observed.contextLocality.measured, false); + assert.equal(existsSync(databasePath), false); + assert.equal(execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' }), before); + + const server = createConsoleServer({ repositoryRoot: root, env }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + context.after(() => server.close()); + const origin = `http://127.0.0.1:${server.address().port}`; + const page = await fetch(`${origin}/`); + assert.equal(page.status, 200); + assert.match(page.headers.get('content-security-policy'), /default-src 'self'/); + assert.match(await page.text(), /coordinated AI engineering team/); + const snapshot = await fetch(`${origin}/api/snapshot`); + assert.equal(snapshot.status, 200); + assert.equal((await snapshot.json()).mutationPerformed, false); + assert.equal((await fetch(`${origin}/../package.json`)).status, 404); + assert.equal(execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' }), before); +});