From 2efccb9af9970b8810af4ecb50f0873a16481504 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sun, 27 Sep 2026 18:55:04 -0400 Subject: [PATCH] Complete Fleet Console operations view --- README.md | 5 +- docs/PORTABLE_AGENT_FLEET_SPEC.md | 7 ++- ...grity-note-2026-09-27-portable-kernel.yaml | 3 +- site/console.html | 7 +++ site/console.js | 44 +++++++++++++- site/styles.css | 12 ++-- src/kernel/worktree-state.mjs | 7 ++- src/observability/snapshot.mjs | 59 ++++++++++++++++++- test/fleet/product-surface.test.mjs | 20 +++++++ 9 files changed, 151 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6b98545..7aa8ea1 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,10 @@ and the first Claude reference-fleet path: 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. + the same repository and runtime state rather than a separate dashboard model; + the Console covers ownership, domain relationships, worktree drift, + recoverability, acknowledgements, checks, integration, delivery, resources, + schedules, decisions, provider health, and context evidence. - identity-bound context-usage telemetry that separates measurements from estimates and relates cost/cache behavior to verified work; - validated system/session schedules with shell-free actions, authority, diff --git a/docs/PORTABLE_AGENT_FLEET_SPEC.md b/docs/PORTABLE_AGENT_FLEET_SPEC.md index 226b283..eea0e30 100644 --- a/docs/PORTABLE_AGENT_FLEET_SPEC.md +++ b/docs/PORTABLE_AGENT_FLEET_SPEC.md @@ -1089,8 +1089,11 @@ 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 +backlog, check, resource, integration, delivery, schedule, and audit state +without initializing a missing database. The same snapshot exposes approved +ownership and neighbour relationships, managed worktree branch drift, +recoverability, acknowledgement state, tracked decisions, configured runtime +and delivery providers, and forge health. Context-locality output explicitly reports `unavailable` until runtime usage samples exist, preventing estimates from masquerading as measurements. 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 43e2622..7ef6809 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 @@ -217,7 +217,7 @@ test_integrity_note: - 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 + then: ownership, worktree drift, recoverability, decisions, providers, delivery, and operational state are served with restrictive headers while Git and machine-local state remain unchanged - id: SCN-context-locality given: measured provider usage and a separate heuristic estimate for one persistent Fleet identity when: CLI and identity-bound MCP record and aggregate the samples @@ -247,6 +247,7 @@ test_integrity_note: - 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 + - approved ownership graph, managed worktree drift, recoverability grade, decisions, delivery, and provider status in one read-only snapshot - measured and estimated context groups, cache-read ratio, and cost per verified item - schedule plan blockers, subprocess shell flag, retry count, and durable failure message - preservation of project source and modified owner configuration diff --git a/site/console.html b/site/console.html index 210d407..cde20b2 100644 --- a/site/console.html +++ b/site/console.html @@ -25,11 +25,18 @@

Fleet

0 identities
+

Ownership

Domain graph
+

Worktrees

Recoverability

Backlog

0 active

Fleet changes

0 pending

Integration

Exact commits
+

Delivery

Release is not deploy
+

Checks

Exact receipts

Resources

FIFO leases
+

Schedules

Declared lifetimes
+

Providers

Capability health

Messages

0 queued
+

Decisions

Tracked truth

Context locality

Measured, never assumed
diff --git a/site/console.js b/site/console.js index a8950b9..f9c80fe 100644 --- a/site/console.js +++ b/site/console.js @@ -24,6 +24,18 @@ function render(snapshot) {
${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 domains = snapshot.organization?.domains ?? []; + $('#ownership-list').innerHTML = domains.length ? domains.map((domain) => ` +
${safe(domain.title ?? domain.id)}${safe((domain.owned_paths ?? []).join(', ') || 'No paths assigned')}
${safe((domain.neighbours ?? []).join(', ') || 'isolated')}
+ `).join('') : empty('No approved domain ownership graph.'); + + const worktrees = snapshot.worktrees ?? []; + $('#recoverability-state').textContent = snapshot.mode === 'installed' + ? (snapshot.recoverability?.level ?? 'Unknown') : 'Not installed'; + $('#worktree-list').innerHTML = worktrees.length ? worktrees.map((worktree) => ` +
${safe(worktree.area)}${safe(worktree.branch)} · ${safe(worktree.path)}
${safe(`${worktree.ahead ?? 0} ahead / ${worktree.behind ?? 0} behind`)}
+ `).join('') : empty('No managed worktrees have been created.'); + 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) => ` @@ -42,6 +54,16 @@ function render(snapshot) {
${safe(item.sourceArea)}${safe(item.sourceCommit?.slice(0, 9))}
${safe(item.state)}
`).join('') : empty('No commits waiting to land.'); + const deliveries = snapshot.deliveries ?? []; + $('#delivery-list').innerHTML = deliveries.length ? deliveries.map((item) => ` +
${safe(item.label)}${safe(item.sourceArea)} · ${safe(item.commit?.slice(0, 9))}
${safe(item.state)}
+ `).join('') : empty('No delivery records. Integration does not imply deployment.'); + + const receipts = snapshot.checks?.receipts ?? []; + $('#check-list').innerHTML = receipts.length ? receipts.map((receipt) => ` +
${safe(receipt.checkId)}${safe(receipt.areaId)} · ${safe(receipt.commit?.slice(0, 9))}
${safe(receipt.result)}
+ `).join('') : empty('No exact-commit check receipts.'); + const holders = snapshot.resources?.holders ?? []; const waiters = snapshot.resources?.waiters ?? []; $('#resource-list').innerHTML = holders.length || waiters.length @@ -49,12 +71,32 @@ function render(snapshot) { ...waiters.map((item) => `
${safe(item.resourceId)}${safe(item.areaId)} waits
`)].join('') : empty('All configured resources are available.'); + const schedules = snapshot.schedules ?? []; + $('#schedule-list').innerHTML = schedules.length ? schedules.map((schedule) => ` +
${safe(schedule.id)}${safe(schedule.owner)} · ${safe(schedule.lifetime)}
${safe(schedule.behavior)}
+ `).join('') : empty('No schedules configured.'); + + const providers = snapshot.providers ?? {}; + const runtimeProviders = providers.runtimes ?? []; + const deliveryProviders = providers.delivery ?? {}; + const configured = snapshot.mode === 'installed'; + $('#provider-list').innerHTML = [ + `
Forge${safe(configured ? `${providers.forge?.provider ?? 'none'} · ${providers.forge?.status ?? 'unknown'}` : 'not configured')}
`, + ...runtimeProviders.map((runtime) => `
${safe(runtime.name)}runtime configured
`), + `
Release${safe(configured ? (deliveryProviders.release?.provider ?? 'none') : 'not configured')}
`, + `
Deployment${safe(configured ? (deliveryProviders.deployment?.provider ?? 'none') : 'not configured')}
`, + ].join(''); + 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)}
+

${safe(message.sender)} to ${safe(message.recipient)}${safe((message.acknowledgedBy ?? []).length ? `Acknowledged by ${(message.acknowledgedBy ?? []).join(', ')}` : 'Unacknowledged')}

${safe(message.body)}
`).join('') : empty('No durable messages yet.'); + const decisions = snapshot.decisions?.content ?? ''; + $('#decision-list').innerHTML = decisions.trim() + ? `

${safe(decisions)}

` : empty('No tracked decisions.'); + const locality = snapshot.contextLocality; $('#context-detail').innerHTML = locality?.measured ? locality.samples.map((sample) => `
${safe(sample.areaId)}${safe(String(sample.cacheRead ?? 0))} cached tokens
`).join('') diff --git a/site/styles.css b/site/styles.css index b5431c6..403d8ea 100644 --- a/site/styles.css +++ b/site/styles.css @@ -113,9 +113,10 @@ footer div { display: flex; gap: 1.5rem; } .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; } +.console-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 1rem; } +.console-panel { min-height: 210px; padding: 1.2rem; border: 1px solid var(--line); background: var(--paper); } +.agents-panel { grid-column: span 3; } +.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; } @@ -127,7 +128,8 @@ footer div { display: flex; gap: 1.5rem; } .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 small, .message-row small { display: block; color: var(--muted); margin-top: .2rem; } +.decision-copy { white-space: pre-wrap; color: var(--muted); line-height: 1.55; } .list-row > span { color: var(--route); font-size: .8rem; } .message-row { display: block; } .message-row p { margin: 0; font-size: .8rem; color: var(--muted); } @@ -146,6 +148,8 @@ footer div { display: flex; gap: 1.5rem; } .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); } + .console-grid { grid-template-columns: repeat(2, 1fr); } + .agents-panel { grid-column: span 2; } } @media (max-width: 600px) { diff --git a/src/kernel/worktree-state.mjs b/src/kernel/worktree-state.mjs index 48ff522..aba6322 100644 --- a/src/kernel/worktree-state.mjs +++ b/src/kernel/worktree-state.mjs @@ -17,6 +17,11 @@ export function inspectManagedWorktree(root, entry, mainBranch) { const status = git(entry.path, ['status', '--porcelain=v1', '--untracked-files=all']); if (status) problems.push('worktree-dirty'); const ahead = Number(git(root, ['rev-list', '--count', `${mainBranch}..${entry.branch}`]) || 0); + const behind = Number(git(root, ['rev-list', '--count', `${entry.branch}..${mainBranch}`]) || 0); if (ahead > 0) problems.push(`unique-commits:${ahead}`); - return { ...entry, actualBranch, ahead, safeToRemove: problems.length === 0, problems }; + return { + ...entry, actualBranch, ahead, behind, + drift: ahead === 0 && behind === 0 ? 'current' : 'diverged', + safeToRemove: problems.length === 0, problems, + }; } diff --git a/src/observability/snapshot.mjs b/src/observability/snapshot.mjs index 6f90440..d7162e3 100644 --- a/src/observability/snapshot.mjs +++ b/src/observability/snapshot.mjs @@ -7,6 +7,8 @@ import { inspectRepository } from '../kernel/git.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; import { projectStatePath } from '../kernel/paths.mjs'; import { loadProjectConfig } from '../kernel/config.mjs'; +import { inspectManagedWorktree } from '../kernel/worktree-state.mjs'; +import { classifyRecoverability } from '../canonical/local.mjs'; function json(path) { return JSON.parse(readFileSync(path, 'utf8')); @@ -59,8 +61,25 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => const config = loadProjectConfig(repository.root); const roster = json(join(repository.root, '.torch', 'roster.yaml')); const manifest = readInstallManifest(repository.root); + const doctor = diagnoseProject({ repository, env }); const stateRoot = projectStatePath(manifest.projectId, env); const databasePath = join(stateRoot, 'state.db'); + const areas = roster.areas ?? []; + const edges = []; + const seenEdges = new Set(); + for (const area of areas) { + for (const neighbour of area.neighbours ?? []) { + if (neighbour === 'all') continue; + const key = [area.id, neighbour].sort().join('\0'); + if (!seenEdges.has(key)) { + seenEdges.add(key); + edges.push({ from: area.id, to: neighbour }); + } + } + } + const forgeFinding = doctor.findings.find((finding) => [ + 'FORGE_UNAVAILABLE', 'FORGE_SYNCHRONIZATION_PENDING', 'FORGE_STATUS_INVALID', + ].includes(finding.code)); const result = { schema: 'torch.dev/observation/v1alpha1', generatedAt: now().toISOString(), mode: 'installed', mutationPerformed: false, @@ -69,8 +88,13 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => root: repository.root, branch: repository.branch, head: repository.head, dirty: repository.dirtyEntries.length > 0, }, - doctor: diagnoseProject({ repository, env }), - agents: (roster.areas ?? []).map((area) => ({ + doctor, + organization: { + sessionManager: areas.find((area) => area.id === 'session-manager') ?? null, + domains: areas.filter((area) => area.id !== 'session-manager'), + edges, + }, + agents: areas.map((area) => ({ areaId: area.id, title: area.title, state: 'offline', runtime: area.runtime ?? null, runtimeSessionId: null, summary: null, task: null, heartbeatAt: null, })), @@ -82,6 +106,29 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => deliveries: [], fleetChanges: [], schedules: config.schedules ?? [], + worktrees: (manifest.external ?? []) + .filter((entry) => entry.type === 'worktree') + .map((entry) => inspectManagedWorktree(repository.root, entry, config.project.main_branch)), + recoverability: classifyRecoverability({ repositoryRoot: repository.root }), + decisions: { + path: '.torch/decisions.md', + content: readFileSync(join(repository.root, '.torch', 'decisions.md'), 'utf8'), + }, + providers: { + runtimes: Object.entries(config.runtimes) + .filter(([name]) => name !== 'default') + .map(([name, policy]) => ({ name, configured: true, policy })), + forge: config.forge.provider === 'none' + ? { provider: 'none', status: 'local-only', remote: null } + : { + provider: config.forge.provider, remote: config.forge.remote, + status: forgeFinding?.code === 'FORGE_UNAVAILABLE' ? 'degraded' + : (forgeFinding?.code === 'FORGE_SYNCHRONIZATION_PENDING' ? 'pending-sync' + : (forgeFinding ? 'invalid' : 'available')), + finding: forgeFinding ?? null, + }, + delivery: config.delivery.adapters, + }, audit: [], contextLocality: { measured: false, status: 'unavailable', @@ -112,7 +159,13 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => 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(); + `).all().map((message) => ({ + ...message, + acknowledgedBy: hasTable(database, 'message_acks') + ? database.prepare('SELECT area_id FROM message_acks WHERE message_id = ? ORDER BY area_id') + .all(message.id).map((row) => row.area_id) + : [], + })); if (hasTable(database, 'message_acks') && hasTable(database, 'identities')) { result.messages.unacknowledged = database.prepare(` SELECT COUNT(*) AS count FROM messages m diff --git a/test/fleet/product-surface.test.mjs b/test/fleet/product-surface.test.mjs index a7a6751..1f8b9ab 100644 --- a/test/fleet/product-surface.test.mjs +++ b/test/fleet/product-surface.test.mjs @@ -9,6 +9,7 @@ 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 { createWorktrees } from '../../src/kernel/worktrees.mjs'; import { observeProject } from '../../src/observability/snapshot.mjs'; function fixture() { @@ -35,6 +36,9 @@ test('SCN-product-site: public surface explains the portable fleet and separates assert.doesNotMatch(html, /nostr|relay coordination|task lock/i); assert.match(consoleHtml, /Local Fleet Console/); assert.match(consoleHtml, /noindex,nofollow/); + for (const label of ['Ownership', 'Worktrees', 'Delivery', 'Checks', 'Schedules', 'Providers', 'Decisions']) { + assert.match(consoleHtml, new RegExp(`>${label}<`)); + } }); test('SCN-architecture-decisions: every specification decision has an explicit accepted or gated boundary', () => { @@ -57,6 +61,14 @@ test('SCN-console-readonly: HTTP console serves fixed assets and observes an ins status: 'approved', reviewedAt: '2026-09-27T00:00:00Z', reviewedBy: 'fixture-owner', notes: [], }; installProject({ repository, proposal, env, projectId: 'console-fixture' }); + execFileSync('git', ['-C', root, 'add', '.torch']); + execFileSync('git', ['-C', root, 'commit', '-m', 'install torch']); + createWorktrees({ + repository: inspectRepository(root), + parentOverride: mkdtempSync(join(tmpdir(), 'torch-console-worktrees-')), + }); + execFileSync('git', ['-C', root, 'add', '.torch/install-manifest.json']); + execFileSync('git', ['-C', root, 'commit', '-m', 'record worktrees']); 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' }); @@ -64,6 +76,14 @@ test('SCN-console-readonly: HTTP console serves fixed assets and observes an ins assert.equal(observed.mode, 'installed'); assert.equal(observed.mutationPerformed, false); assert.equal(observed.agents.length, proposal.domains.length + 1); + assert.equal(observed.organization.domains.length, proposal.domains.length); + assert.equal(observed.worktrees.length, proposal.domains.length + 1); + assert.equal(observed.worktrees.every((worktree) => Number.isInteger(worktree.behind)), true); + assert.equal(observed.recoverability.level, 'ONE-DISK'); + assert.match(observed.decisions.content, /TORCH decisions/); + assert.equal(observed.providers.forge.status, 'local-only'); + assert.equal(observed.providers.delivery.release.provider, 'none'); + assert.deepEqual(observed.deliveries, []); assert.equal(observed.contextLocality.measured, false); assert.deepEqual(observed.fleetChanges, []); assert.equal(existsSync(databasePath), false);