From 03fcbdd23f03e3d50e731b1494a24014fe525b87 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Wed, 30 Sep 2026 13:28:40 -0400 Subject: [PATCH] Checkpoint portable TORCH fleet, provider adapters and owner dashboard --- .gitignore | 2 + README.md | 424 +++- docs/ARCHITECTURE_DECISIONS.md | 349 ++- docs/PORTABLE_AGENT_FLEET_SPEC.md | 1053 +++++++- docs/TEST_INTEGRITY.md | 2171 +++++++++++++++++ docs/TODO.md | 867 ++++++- package-lock.json | 16 + package.json | 23 +- .../abandoned-check-console-2026-09-30.md | 30 + ...wser-descendant-interruption-2026-09-30.md | 41 + ...frozen-browser-qualification-2026-09-30.md | 50 + .../interrupted-check-recovery-2026-09-30.md | 46 + .../product-alignment-review-2026-09-29.md | 507 ++++ reports/audit/readiness-audit-2026-09-30.md | 102 + ...untime-browser-qualification-2026-09-30.md | 46 + .../audit/self-host-readiness-2026-09-30.md | 116 + .../self-host-roster-review-2026-09-30.md | 151 ++ .../specialist-only-bootstrap-2026-09-30.md | 36 + reports/audit/stale-work-review-2026-09-30.md | 52 + .../torch-dashboard-activity-review.png | Bin 0 -> 55673 bytes .../torch-dashboard-check-evidence.png | Bin 0 -> 119022 bytes .../torch-dashboard-demo-desktop.png | Bin 0 -> 147834 bytes .../torch-dashboard-demo-mobile.png | Bin 0 -> 61683 bytes .../torch-dashboard-live-refresh.png | Bin 0 -> 152082 bytes .../torch-dashboard-manager-wakes.png | Bin 0 -> 35270 bytes .../torch-dashboard-operation-outcomes.png | Bin 0 -> 62240 bytes ...torch-dashboard-owner-briefing-desktop.png | Bin 0 -> 90005 bytes .../torch-dashboard-owner-briefing-mobile.png | Bin 0 -> 83195 bytes .../torch-dashboard-pilot-conclusion.png | Bin 0 -> 63066 bytes .../torch-dashboard-pilot-review.png | Bin 0 -> 2934 bytes schemas/domain-proposal.schema.json | 68 +- scripts/condition-browser-command.mjs | 15 + scripts/condition-browser-runtime.mjs | 86 + scripts/frozen-browser-harness.mjs | 53 + scripts/interrupted-browser-harness.mjs | 54 + scripts/interrupted-check-runner.mjs | 15 + scripts/test-dashboard-demo.mjs | 247 ++ scripts/wait-owned-browser-group.py | 57 + site/app.js | 11 +- site/check-evidence.js | 40 + site/console.html | 312 ++- site/console.js | 1963 ++++++++++++++- site/demo-artifact.svg | 22 + site/demo.html | 10 + site/demo.js | 302 +++ site/index.html | 95 +- site/live-refresh.js | 51 + site/operation-outcomes.js | 29 + site/owner-digest.js | 50 + site/styles.css | 591 ++++- site/task-activity.js | 28 + site/torch-mark.svg | 17 + site/work-progress.js | 40 + site/work-views.js | 144 ++ src/adapters/claude.mjs | 44 +- src/adapters/codex.mjs | 57 +- src/adapters/delivery.mjs | 4 + src/adapters/pi.mjs | 212 ++ src/adapters/pi/extension.mjs | 182 ++ src/adapters/plugins.mjs | 229 ++ src/adapters/registry.mjs | 93 + src/adapters/runtime.mjs | 120 + src/artifacts/service.mjs | 420 ++++ src/backlog/activity.mjs | 90 + src/backlog/service.mjs | 438 +++- src/canonical/fetch.mjs | 100 + src/checks/conditions.mjs | 100 + src/checks/runner.mjs | 30 + src/checks/service.mjs | 264 +- src/checks/snapshot.mjs | 144 ++ src/cli.mjs | 934 ++++++- src/console/server.mjs | 1212 ++++++++- src/control-plane/service.mjs | 270 +- src/delivery/attempts.mjs | 117 + src/delivery/service.mjs | 113 +- src/design/architect.mjs | 123 +- src/design/brief.mjs | 52 +- src/evolution/hierarchy.mjs | 1093 +++++++++ src/evolution/service.mjs | 11 +- src/forge/service.mjs | 80 + src/importers/combatrig.mjs | 67 +- src/integration/service.mjs | 200 +- src/kernel/analyze.mjs | 10 +- src/kernel/config.mjs | 198 +- src/kernel/doctor.mjs | 72 +- src/kernel/domains.mjs | 120 +- src/kernel/git.mjs | 62 +- src/kernel/install.mjs | 69 +- src/kernel/organization.mjs | 242 ++ src/kernel/prompts.mjs | 19 +- src/mcp/server.mjs | 10 +- src/mcp/tools.mjs | 209 +- src/observability/manager-checkin.mjs | 152 ++ src/observability/owner-digest.mjs | 168 ++ src/observability/snapshot.mjs | 377 ++- src/runtime/hierarchy-order.mjs | 64 + src/runtime/lifecycle.mjs | 281 ++- src/schedules/launcher.mjs | 105 +- src/schedules/service.mjs | 324 ++- src/self-host/service.mjs | 26 +- test/fleet/artifacts.test.mjs | 477 ++++ test/fleet/backlog-activity.test.mjs | 85 + test/fleet/backlog.test.mjs | 327 ++- test/fleet/bootstrap-acceptance.test.mjs | 6 +- test/fleet/canonical-fetch.test.mjs | 117 + test/fleet/check-conditions.test.mjs | 123 + test/fleet/check-snapshot.test.mjs | 67 + test/fleet/cli-scenarios.test.mjs | 116 +- test/fleet/combatrig-import.test.mjs | 18 +- test/fleet/console-check-evidence.test.mjs | 95 + test/fleet/console-live-refresh.test.mjs | 34 + .../fleet/console-operation-outcomes.test.mjs | 52 + test/fleet/console-owner-digest.test.mjs | 64 + test/fleet/console-schedule-launcher.test.mjs | 194 ++ test/fleet/console-task-activity.test.mjs | 61 + test/fleet/console-work-progress.test.mjs | 31 + test/fleet/console-work-views.test.mjs | 99 + test/fleet/dashboard-demo.test.mjs | 87 + test/fleet/delivery.test.mjs | 228 ++ test/fleet/domain-analysis.test.mjs | 117 +- test/fleet/evolution.test.mjs | 28 +- test/fleet/forge-migration.test.mjs | 49 +- test/fleet/hierarchy-evolution.test.mjs | 972 ++++++++ test/fleet/integration-canonical.test.mjs | 244 +- test/fleet/kernel-lifecycle.test.mjs | 31 + test/fleet/lifecycle.test.mjs | 210 +- test/fleet/manager-checkin.test.mjs | 189 ++ test/fleet/mcp-adapter.test.mjs | 42 + test/fleet/organization.test.mjs | 156 ++ test/fleet/owner-approval-console.test.mjs | 99 + test/fleet/owner-digest.test.mjs | 175 ++ test/fleet/package-distribution.test.mjs | 16 + test/fleet/pi-adapter.test.mjs | 212 ++ test/fleet/product-surface.test.mjs | 303 ++- test/fleet/runtime-plugins.test.mjs | 237 ++ test/fleet/runtime-profiles.test.mjs | 475 ++++ test/fleet/schedules.test.mjs | 575 ++++- test/fleet/self-host.test.mjs | 94 +- test/fleet/spec-design.test.mjs | 176 ++ test/fleet/verification-resources.test.mjs | 539 +++- 140 files changed, 26152 insertions(+), 757 deletions(-) create mode 100644 docs/TEST_INTEGRITY.md create mode 100644 reports/audit/abandoned-check-console-2026-09-30.md create mode 100644 reports/audit/browser-descendant-interruption-2026-09-30.md create mode 100644 reports/audit/frozen-browser-qualification-2026-09-30.md create mode 100644 reports/audit/interrupted-check-recovery-2026-09-30.md create mode 100644 reports/audit/product-alignment-review-2026-09-29.md create mode 100644 reports/audit/readiness-audit-2026-09-30.md create mode 100644 reports/audit/same-runtime-browser-qualification-2026-09-30.md create mode 100644 reports/audit/self-host-readiness-2026-09-30.md create mode 100644 reports/audit/self-host-roster-review-2026-09-30.md create mode 100644 reports/audit/specialist-only-bootstrap-2026-09-30.md create mode 100644 reports/audit/stale-work-review-2026-09-30.md create mode 100644 reports/design-system/torch-dashboard-activity-review.png create mode 100644 reports/design-system/torch-dashboard-check-evidence.png create mode 100644 reports/design-system/torch-dashboard-demo-desktop.png create mode 100644 reports/design-system/torch-dashboard-demo-mobile.png create mode 100644 reports/design-system/torch-dashboard-live-refresh.png create mode 100644 reports/design-system/torch-dashboard-manager-wakes.png create mode 100644 reports/design-system/torch-dashboard-operation-outcomes.png create mode 100644 reports/design-system/torch-dashboard-owner-briefing-desktop.png create mode 100644 reports/design-system/torch-dashboard-owner-briefing-mobile.png create mode 100644 reports/design-system/torch-dashboard-pilot-conclusion.png create mode 100644 reports/design-system/torch-dashboard-pilot-review.png create mode 100644 scripts/condition-browser-command.mjs create mode 100644 scripts/condition-browser-runtime.mjs create mode 100644 scripts/frozen-browser-harness.mjs create mode 100644 scripts/interrupted-browser-harness.mjs create mode 100644 scripts/interrupted-check-runner.mjs create mode 100644 scripts/test-dashboard-demo.mjs create mode 100644 scripts/wait-owned-browser-group.py create mode 100644 site/check-evidence.js create mode 100644 site/demo-artifact.svg create mode 100644 site/demo.html create mode 100644 site/demo.js create mode 100644 site/live-refresh.js create mode 100644 site/operation-outcomes.js create mode 100644 site/owner-digest.js create mode 100644 site/task-activity.js create mode 100644 site/torch-mark.svg create mode 100644 site/work-progress.js create mode 100644 site/work-views.js create mode 100644 src/adapters/pi.mjs create mode 100644 src/adapters/pi/extension.mjs create mode 100644 src/adapters/plugins.mjs create mode 100644 src/adapters/registry.mjs create mode 100644 src/artifacts/service.mjs create mode 100644 src/backlog/activity.mjs create mode 100644 src/canonical/fetch.mjs create mode 100644 src/checks/conditions.mjs create mode 100644 src/checks/runner.mjs create mode 100644 src/checks/snapshot.mjs create mode 100644 src/delivery/attempts.mjs create mode 100644 src/evolution/hierarchy.mjs create mode 100644 src/kernel/organization.mjs create mode 100644 src/observability/manager-checkin.mjs create mode 100644 src/observability/owner-digest.mjs create mode 100644 src/runtime/hierarchy-order.mjs create mode 100644 test/fleet/artifacts.test.mjs create mode 100644 test/fleet/backlog-activity.test.mjs create mode 100644 test/fleet/canonical-fetch.test.mjs create mode 100644 test/fleet/check-conditions.test.mjs create mode 100644 test/fleet/check-snapshot.test.mjs create mode 100644 test/fleet/console-check-evidence.test.mjs create mode 100644 test/fleet/console-live-refresh.test.mjs create mode 100644 test/fleet/console-operation-outcomes.test.mjs create mode 100644 test/fleet/console-owner-digest.test.mjs create mode 100644 test/fleet/console-schedule-launcher.test.mjs create mode 100644 test/fleet/console-task-activity.test.mjs create mode 100644 test/fleet/console-work-progress.test.mjs create mode 100644 test/fleet/console-work-views.test.mjs create mode 100644 test/fleet/dashboard-demo.test.mjs create mode 100644 test/fleet/hierarchy-evolution.test.mjs create mode 100644 test/fleet/manager-checkin.test.mjs create mode 100644 test/fleet/organization.test.mjs create mode 100644 test/fleet/owner-approval-console.test.mjs create mode 100644 test/fleet/owner-digest.test.mjs create mode 100644 test/fleet/pi-adapter.test.mjs create mode 100644 test/fleet/runtime-plugins.test.mjs create mode 100644 test/fleet/runtime-profiles.test.mjs diff --git a/.gitignore b/.gitignore index 614e682..28a9685 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ task-logs/*.log # Artifacts and temporary files artifacts/ +!src/artifacts/ +!src/artifacts/** reports/performance/hits-*.json reports/performance/hits-*.txt reports/performance/benchmark-*.txt diff --git a/README.md b/README.md index 60dbfa9..bb8657f 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,12 @@ and the first Claude reference-fleet path: - live ownership, neighbours, handoff, coordination, completion, and blocker operations; - equivalent CLI and identity-bound MCP surfaces; - current MCP stdio interoperability through the official protocol SDK; -- an explicit-capability Claude adapter with durable-message fallback; -- an explicit-capability Codex adapter with resumable JSONL thread capture; -- per-domain mixed Claude/Codex runtime planning and identity-bound MCP registration plans; -- invocation-scoped MCP injection on every Claude/Codex create and resume - launch, without changing the user's global runtime configuration; +- explicit-capability Claude, Codex, and Pi adapters with per-identity + model/reasoning selection; +- identity-bound MCP registration for Claude/Codex and a bundled Pi extension + that exposes the same TORCH tool set through Pi's extension API; +- invocation-scoped runtime configuration without changing the user's global + provider settings; - durable-first Codex steering with native `queue` notification when available; - worker-first/Session-Manager-last fresh start and resume planning; - safe wind-down that refuses active work, Git operations, leases, check or @@ -69,11 +70,17 @@ 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; - the Console covers ownership, domain relationships, worktree drift, - recoverability, acknowledgements, checks, integration, delivery, resources, - schedules, decisions, provider health, and context evidence. +- a redesigned public product site and local Fleet Console backed by the same + repository and runtime state rather than a separate dashboard model, with a + read-only observation surface and narrowly scoped, preview-confirmed owner + actions for feedback, task proposals, next-launch profiles, and schedule-timer + setup/refresh; + the Console covers ownership, domain relationships, hierarchy-derived + startup/wind-down order and blockers, worktree drift, recoverability, + acknowledgements, checks, integration, delivery, resources, schedules, + decisions, provider health, and context evidence. Owners can save + project-scoped browser views over the existing backlog without creating a + second queue or changing task state. - 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, @@ -101,6 +108,57 @@ in [docs/ARCHITECTURE_DECISIONS.md](docs/ARCHITECTURE_DECISIONS.md). ## Development +`torch digest build` prints an owner-first Markdown report; add `--json` for +structured data. It uses a rolling UTC window (24 hours by default), distinguishes +landing from receipt-reported deployment/live verification, and puts pending +owner approvals and unresolved delivery operations first. It reports blocked +work, completed items, recorded approval decisions and neglected owner requests. +Missing sources, truncated results and incomplete Git history are explicit; +unstructured decision documents are not claimed as recent decisions. + +`torch digest publish --by owner --yes` stores a local report. `torch digest +latest --json` and the local console's read-only `/api/digest` return the latest +published report; console snapshots include it too. Automatic publication can +be enabled in approved config with `owner_digest: { enabled: true, +window_hours: 24, max_items: 30 }` and a system coordination schedule whose +action is `{ type: "owner-digest" }`, required authority is `owner`, and retry +limit is one. Choose cadence through the existing schedule trigger; no timer +is installed by this setting. This deterministic action wakes no AI and sends +nothing externally. The dashboard's Briefing view displays the saved report, +with publication time, evidence coverage and a link to current approvals. The +same renderer works in the isolated dashboard demo. Unpublished, stale and +unknown evidence stays explicit; published reports are not silently rewritten +when live approvals change. External notifications remain pending; all reports +are local/private by default. + +Delivery adapters now retain durable operation and attempt records, including +failures. Inspect `torch delivery operations --json` and `torch delivery attempts +--id --json`; local console snapshots include bounded recent +records. Approved `delivery.retry.max_attempts` is 1–5 (default 1). Automatic +retry requires the adapter to declare `retrySafety[operation]` as `idempotent` +or `read-only`, plus a structured failed receipt reporting +`failure: { classification: "transient", effects: "not-applied" }`. +Permanent failures, missing safety and uncertain effects never auto-retry. +Attempts share one operation idempotency key; adapters must actually honor it. +Policy changes stop further attempts. Existing high-impact approvals still apply. + +Thrown/malformed responses and interrupted executions remain unresolved and +block another caller. After independent provider inspection confirms no effect, +the owner can use `torch delivery recover-not-applied --operation --by +owner --runtime-stopped --evidence --yes`. This is explicitly +owner-attested, not sensor proof: it neither executes an adapter nor advances +delivery state, and retains original uncertain attempts. Do not use it for an +operation that actually succeeded. Instead preview `torch delivery +recover-succeeded --operation --by owner --dry-run --json`, then confirm +with `--runtime-stopped --evidence --yes`. This reconciles a +saved successful receipt without invoking an adapter, checks exact state, +unchanged destination settings and current owner authority, and applies once. +It also handles a saved success receipt whose parent operation flag was not +updated. Legacy records without destination hashes require the full original +policy hash. Provider receipt truth and stopped-executor confirmation still +need live qualification. Real provider/CLI adapter loading remains development +work; configuring a provider name alone does not install an adapter. + Requirements: - Node.js 22 or later @@ -157,7 +215,10 @@ evidence, the deterministic baseline, unassigned components, unresolved ownership, collision questions, and the required pending-proposal contract. A Codex, Claude, or other planning session can reason over that artifact without TORCH choosing a provider or incurring an implicit model call. `torch design` -remains the deterministic baseline and fallback. +remains the deterministic baseline and fallback. Architect proposals must also +justify a flat fleet or evidence-backed coordination roles, spell out integrated +outcomes and authority boundaries, and preserve direct specialist communication; +they remain advisory until separate owner review and approval. Preview an isolated Claude or Codex architecture call, validate an externally produced proposal, or explicitly execute the call: @@ -197,8 +258,8 @@ Review the evidence in `fleet-proposal.json`, edit its `review.status` to `approved`, and identify the reviewer. Then inspect and apply the installation: ```bash -node bin/torch.mjs install --proposal fleet-proposal.json --runtime claude --dry-run --json -node bin/torch.mjs install --proposal fleet-proposal.json --runtime claude --yes --json +node bin/torch.mjs install --proposal fleet-proposal.json --runtime codex --dry-run --json +node bin/torch.mjs install --proposal fleet-proposal.json --runtime codex --yes --json node bin/torch.mjs doctor --json node bin/torch.mjs worktrees --dry-run --json node bin/torch.mjs worktrees --yes --json @@ -279,6 +340,122 @@ Local verification and convergence use `torch checks`, `torch resources`, and `torch integrate`. A project without a forge can preview and create its bare canonical store with `torch canonical plan` and `torch canonical create --yes`. +Specialists can claim their next routed task without a manager round trip when +the owner enables `backlog.self_claim` in approved project configuration: + +```json +{ "backlog": { "self_claim": { "enabled": true, "areas": ["frontend"] } } } +``` + +This policy is disabled by default. Review `torch backlog claim-next --area +frontend --dry-run --json`, then use the same command with `--yes` to claim. +The bound MCP equivalents are `torch_plan_backlog_claim` and +`torch_claim_next_backlog_task`. Existing assignments resume first; new claims +select explicitly routed ready work in priority/FIFO order and refuse dirty +worktrees, pending checks, held/waiting resources, unresolved approvals or +unfinished integration. Completion and landing authority are unchanged. + +Inspect commit-linked progress with `torch backlog activity --json` (or MCP +`torch_backlog_activity`). Approved `backlog.activity` configuration can set +`stale_days` (default 3) and `max_commits` (default 1000). The bounded scan reads +local canonical and managed worktree branches, prioritizes owner requests, and +reports coverage gaps as unknown rather than declaring work neglected. Mention +the exact TORCH task ID in progress commits. Metadata edits do not reset this +activity clock. Commit dates and messages are reported Git metadata, not proof +of meaningful work. Standalone `Closes: TASK-id` lines record intent only; +automatic landed completion is disabled by default. Enable approved +`backlog.activity.auto_close_after_landing: true` to reconcile trailers from +the exact submitted tip after a successful landing. Only tasks already +`ready_to_integrate`, owned by the source specialist, naming that exact commit +and carrying evidence qualify; current required checks and canonical ancestry +are rechecked. Unknown or mismatched tasks stay open. A closure failure leaves +the successful landing intact and reports `needs-review`. + +For crash/lock recovery, Fleet Operations can preview `torch backlog +close-landed --integration --area session-manager --dry-run --json`, then +confirm with `--yes`. Replaying completion does not create another transition. +MCP offers `torch_plan_landed_task_closure` and `torch_reconcile_landed_tasks`; +the latter requires `approved: true`. This does not grant deployment authority. + +Long-waiting checks can opt into frozen inputs. In the approved check definition, +set `snapshot.paths` to explicit relative inputs (for example `dist` and the +test harness) and `snapshot.source_commit_file` to a captured build marker +containing the full source SHA. Keep generated build outputs Git-ignored and +write that marker during the build. Then, from a clean specialist worktree: + +```bash +torch checks prepare --id browser-test --area frontend --yes --json +# Save the returned prepared ID before requesting a browser slot. +torch resources acquire --id browser --area frontend --json +# Once acquired, use the frozen subject rather than the live build directory: +torch checks run-prepared --prepared --area frontend --yes --json +torch resources release --id browser --area frontend --json +``` + +Use `checks prepared` to inspect durable prepared/running/finished records, or +`checks cancel-prepared --prepared --area frontend --yes` to remove an +unstarted owned copy. Resource release/cancellation remains explicit and is +not implied by cancelling a check. Copies use reflinks or independent copies, +never hardlinks. The specialist can keep rebuilding after preparation; receipts +identify the captured SHA, not its newer worktree tip. Checks must read their +captured working directory, not absolute live paths or an unrelated server. +Interrupted running checks are retained for inspection, not silently rerun. + +Checks can also declare conditions that must hold before measurements: + +```json +{ + "conditions": { + "command": "node", + "args": ["tools/report-test-conditions.mjs"], + "timeout_seconds": 30, + "required": [ + { "id": "clock-moving", "equals": true }, + { "id": "visible", "equals": true }, + { "id": "invincible", "equals": false } + ] + } +} +``` + +Your probe reports `schema: "torch.dev/check-conditions/v1alpha1"`, +`subject: { commit, inputDigest }` (digest required for frozen inputs), and +`conditions: { "clock-moving": true, "visible": true, "invincible": false }`. +It must inspect the tested runtime, not echo expected values. TORCH supplies +the expected SHA/digest in `TORCH_CHECK_COMMIT`/`TORCH_CHECK_INPUT_DIGEST` to the +probe. Include the probe and its dependencies in frozen input paths. + +Missing/mismatched preflight conditions stop measurements. Invalid postflight +conditions invalidate otherwise green measurements. Receipts retain both +reports and a diagnostic line preceding measurement output. Conditions are +project-reported evidence; your harness must inspect the same runtime and +detect drift during measurements. Projects choose their own conditions. + +The dashboard expands check receipts to show recorded frozen-input commit/digest, +copy strategy, invalidation reasons and before/after condition observations. +Prepared checks appear alongside receipts; a running record is labeled completion +unconfirmed, never assumed live or automatically restarted. Legacy receipts +explicitly show missing condition evidence. The view is read-only and bounded to +the latest 40 receipts/preparations; private capture paths are not displayed. + +Integration and release also shows recorded canonical fetches and delivery +attempts. Unknown effects and interrupted running records require review; adapter +success is not independent proof that a release is live. The view does not offer +automatic retry/recovery, and raw receipt references/error text stay out of it. + +The dashboard refreshes every 15 seconds while visible; Pause updates stops +automatic reads. Edited/focused forms and active previews keep their DOM while +unrelated panels update. Retained panels are explicitly marked potentially older; +server-side approval/revision checks still apply. Manual refresh also preserves +drafts, and completing an action releases only its own form. Failed reads keep +the existing evidence and report the failure. Polling never wakes an AI agent. + +The work board's Activity review shows stale owner requests first, separately +from tasks whose progress is unknown because managed Git history is incomplete. +Blocked items retain waiting context. This uses named commits, not metadata edits +or uncommitted work, and never changes backlog state. Daily automatic stale-work +review and live installed-fleet qualification remain open. + An equivalent Git remote can later become the optional canonical forge without changing TORCH identity or coordination semantics: @@ -286,16 +463,26 @@ changing TORCH identity or coordination semantics: torch forge plan --remote origin --json torch forge attach --remote origin --provider generic-git --yes --json torch forge status --json +torch forge sync plan --json +torch forge fetch plan --json +torch forge fetch --yes --attempts 3 --json +torch forge fetch status --json +torch forge sync --yes --json torch forge detach --dry-run --json torch forge detach --yes --json ``` Attach requires the selected remote's main ref to equal local canonical main -exactly and never pushes or fetches implicitly. During a forge outage, status -and doctor expose pending synchronization while local identities, messages, -checks, backlog, worktrees, and policy remain operational. Detach works during -that outage and restores the prior local canonical configuration; it does not -delete a user-owned remote. +exactly and never pushes or fetches implicitly. Integration lands locally +through TORCH's serialized FIFO queue; it does not publish as a side effect. +After reviewing the read-only sync plan, the owner may explicitly publish the +landed canonical commit. Sync is fast-forward-only, never force-pushes, and +blocks if the remote is ahead or has diverged; it does not fetch or merge +implicitly. During a forge outage, status and doctor expose pending +synchronization while local identities, messages, checks, backlog, worktrees, +and policy remain operational. Detach works during that outage and restores +the prior local canonical configuration; it does not delete a user-owned +remote. Delivery is separate from integration. A clean domain tip starts as implemented; exact-commit checks gate verification, and a landed integration @@ -342,15 +529,66 @@ The candidate gate requires named evidence for backlog-through-integration and COMBATRIG compatibility, not merely a green process exit. Bootstrapping an installed stable TORCH to manage this repository remains open. +The npm artifact bundles TORCH's pinned production dependencies. The package +distribution gate installs that tarball offline in a clean consumer without +depending on a previously warmed machine-level npm cache. + Persistent domain sessions also create a context-locality opportunity: each specialist can keep its relevant working set hot instead of one general session reloading unrelated project areas. TORCH treats cache/token savings as a measurement hypothesis until provider usage and verified task outcomes prove it. -This is still an alpha. Runtime lifecycle is scenario-tested with virtualized -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. +This is still an alpha. Claude, Codex, and Pi launch plans are scenario-tested; +the Pi extension's full TORCH MCP tool bridge is exercised against a local +fixture, and an installed-Pi offline/no-session startup smoke test verifies +the bundled extension loads without a provider turn. Pi's `typebox` package is +declared as a TORCH runtime dependency because the extension API requires its +schema type. The tests make no paid provider calls. A live mixed-provider, +multi-domain acceptance run remains an explicit owner-authorized qualification +gate. + +Manager check-in schedules can wake configured agents with a shared daily +invocation limit. For subscription/local runtimes, explicitly set +`action.wake` to `{"enabled":true,"budget_mode":"invocation-count"}` and set +`runtime_wake_budget.max_invocations_per_day` on the project. This limits wake +invocations, not dollar spending. For a runtime that supports enforced spending +limits, use `budget_mode: "usd-hard-cap"` with `max_usd_per_invocation`; built-in +adapters currently reject that mode. Wakes remain disabled by default and +timer installation is separate. Pi requires your chosen provider/model. + +Hierarchy pilots retain their baseline and success/stop criteria. The Session +Manager can record a comparison with `torch fleet hierarchy-review-pilot +--proposal --review --from session-manager --yes`; inspect +the durable history with `torch fleet hierarchy-pilot-reviews --proposal `. +The review JSON supplies `summary`, `recommendation` (`adopt`, `adjust`, +`reverse`, or `inconclusive`), `metrics` (name, value, unit, classification and +evidence), and `success`/`stop` assessments (criterion, outcome and evidence). +Cover every baseline metric and criterion exactly once. Classifications are +`measured`, `qualitative`, or `unavailable`; outcomes are `met`, `not-met`, or +`unknown`. Evidence is reviewer-reported, not independently verified. An +adoption recommendation needs a complete pilot window and reported measured +success; it does not adopt the organization or start any agent. + +To conclude a pilot, review `torch fleet hierarchy-conclusion-plan --proposal + --decision adopt --reason ""`, then confirm with +`torch fleet hierarchy-conclude` using the same arguments plus +`--plan-hash --by owner --yes`. Use `--decision reverse` to +restore prior reporting relationships at a new revision. Reversal preserves +implementation owners, sessions, worktrees and backlog work; obsolete manager +schedules remain for explicit review. Neither action starts/stops agents or +changes systemd. A changed plan must be reviewed again. +The Console offers the same preview/confirm conclusion workflow beside a +piloting hierarchy proposal. It shows blockers and exact work/graph/schedule +impact; an unsupported adoption plan cannot be confirmed. + +Inspect uncertain manager launches with `torch schedules wakes --json`. +An interrupted reservation remains locked until the owner independently checks +and stops the runtime, confirms the manager is offline, and runs +`torch schedules recover-wake --reservation --runtime-stopped --note +"" --yes`. This records your attestation, not automatic +proof of termination. It never launches an agent, acknowledges a message, +refunds the daily budget, or permits replay of that check-in. Review/handle +the old inbox message before a fresh check-in can wake the manager. Run the redesigned public surface and local operational console on loopback: @@ -358,13 +596,60 @@ Run the redesigned public surface and local operational console on loopback: 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: +The landing page's **View dashboard demo** link opens `/console.html?demo=1`: +the actual Console using an isolated sample project. Its actions stay in the +browser tab. Explore tasks, approvals, agent requests, image feedback, and +runtime profile changes; **Reset demo** restores the sample project. +`npm run test:dashboard` verifies these workflows in Chromium (install +Playwright's Chromium browser for this developer check). + +Generated roles inherit the selected installation runtime. For a mixed fleet, +use `--runtime codex,claude,pi --default-runtime codex`; explicit per-role +assignments are preserved. Codex inherits `gpt-6-luna` with high reasoning; +Claude inherits `sonnet`. Installation dry-runs show the resolved default. + +The public explanation is at `/`; `/console` does not repair or silently alter +Fleet state. Its backlog filters and saved named views are browser-local +preferences scoped to the project identity; they do not change task data. +Optional feature and milestone labels stay on authoritative backlog tasks; the +Console groups those tasks for progress visibility without inferring labels or +treating progress as test, integration, or release evidence. The snapshot API +remains read-only. The Console's narrowly scoped write actions include owner +feedback, priority/task proposals, per-identity next-launch profiles, and +schedule-timer installation or refresh. Each consequential change has a fresh +preview and separate confirmation. New tasks stay proposed and unassigned +until the Session Manager triages them. Timer review lists every affected +system schedule and clearly states that enabling the timer may dispatch due +work; the snapshot only verifies TORCH-owned unit files and their config +digest, not live systemd state. Profile changes never reconfigure running +sessions or start a runtime. +Automation can obtain the read-only observation with: ```bash node bin/torch.mjs console snapshot --repo /path/to/project --json ``` +Agent sessions can publish task-bound screenshots into private project state; +the local gallery checks their stored digest when serving them: + +```bash +torch artifacts publish --area frontend --task TASK-123 --title "Settings page" \ + --alt "Settings page at narrow width" --file screenshots/settings-mobile.png \ + --commit "$(git rev-parse HEAD)" --json +torch artifacts list --json +``` + +Owner feedback first prints a target/effect preview. Add `--yes` only after +reviewing it; confirmation routes one durable message to the responsible +identity. The local Console also offers a narrowly scoped feedback form with a +same-origin, single-use preview/confirm step. Its snapshot endpoint remains +read-only; feedback does not change task status or grant other Fleet authority: + +```bash +torch artifacts comment --artifact --body "Keep the compact header" --by owner --json +torch artifacts comment --artifact --body "Keep the compact header" --by owner --yes --json +``` + Runtime adapters and agents can report cache locality without conflating provider measurements and estimates: @@ -394,6 +679,40 @@ torch schedules launcher status --json torch schedules launcher remove --yes --json ``` +Automatic FIFO integration is opt-in. A project may add this JSON object to the +`schedules` array in `.torch/torch.yaml` after reviewing its exact effect (the +file currently uses JSON syntax despite its `.yaml` suffix): + +```json +{ + "id": "integration-drain", + "title": "Drain individually authorized integration requests", + "owner": "owner", + "lifetime": "system", + "trigger": { "type": "interval", "seconds": 60 }, + "behavior": "mutating", + "action": { + "type": "integration-drain", + "landing_authority_id": "session-manager", + "limit": 20 + }, + "required_authority": ["owner"], + "retry": { "max_attempts": 1 }, + "source_of_truth": "owner-reviewed integration policy" +} +``` + +This periodic action uses the same serialized FIFO drain as +`torch integrate drain`. It cannot converge branches, push to a remote, relax +exact-commit checks, or start an AI runtime. Each request still needs a passing +check receipt on its exact source commit and separate authorization from the +configured landing authority. Installing the system timer is a standing owner +approval for the listed system schedules to run; an authorized change may land +on the next tick without another owner prompt. Defining the schedule alone does +not install or start that timer. Inspect `torch schedules list --actor owner` +and `torch schedules launcher plan` before installation. The timer dispatches +all due system schedules, not just integration. + ## Product principles - Git is required; GitHub is optional. @@ -410,6 +729,63 @@ torch schedules launcher remove --yes --json - Installation is reversible and refuses unsafe deletion. - Stable TORCH must be able to build its successor. +### Changing an agent's AI profile + +New Codex identities default to `gpt-6-luna` with high reasoning effort; new +Claude identities default to `sonnet`. After installation, edit `.torch/torch.yaml` +to change `runtimes.codex.model` / `runtimes.codex.reasoning` or +`runtimes.claude.model`. Set `model` and/or `reasoning` on +`session_manager` or a specific entry in `domains` to override just that +identity. These are project-local settings; TORCH does not rewrite global +Codex or Claude configuration, and changing a profile does not launch agents. +You can also inspect or change one installed identity from the CLI: + +```sh +torch profile show --area session-manager +torch profile set --area session-manager --runtime codex --model gpt-6-luna --reasoning high --yes +torch profile defaults --runtime codex --model gpt-6-luna --reasoning high --yes +torch profile set --area session-manager --reset --yes +``` + +`profile set` requires `--yes`, validates the resulting project config, and only +changes future launch plans; it does not start, stop, or reconfigure a running +session. `--reasoning none` clears the identity's reasoning override (or the +runtime default's explicit value), falling back to the runtime's built-in +behavior. `--reset` clears an identity's model/reasoning overrides while +keeping its selected runtime. Use `profile defaults` to change or reset the +project-wide defaults for one runtime; identity-specific values continue to +take precedence. +For Pi, choose a model explicitly using Pi's `provider/model` identifier; TORCH +does not infer a provider or account. Pi sessions run one resumable turn at a +time, save transcripts under private project state, and start the TORCH MCP +server only while a TORCH tool call is executing. + +Other local runtimes can be added as explicit adapter plugins. A trust preview +prints the exact module hash without executing the module; confirm only after +reviewing that path and digest: + +```sh +torch runtimes list +torch runtimes trust --name my-agent --module /absolute/path/my-agent.cjs --json +torch runtimes trust --name my-agent --module /absolute/path/my-agent.cjs \ + --yes --sha256 +torch profile set --area documentation --runtime my-agent \ + --model provider/model --yes +``` + +The user-local allowlist lives outside the repository in +`$XDG_CONFIG_HOME/torch/runtime-adapters.json` (or `~/.config/torch/`). Plugins +must export `createTorchRuntimeAdapter({ env })` from a canonical absolute +`.cjs` entrypoint and satisfy TORCH's declared lifecycle contract. Project +configuration can select a trusted adapter but cannot grant trust or choose a +module path. The exact entrypoint SHA-256 is checked before each registry load; +the trusted module and its imported dependencies run with the current user's OS +permissions, and the digest pins the entrypoint only. `torch runtimes list`, +`torch doctor`, and `torch profile show` inspect trust/profile metadata without +loading plugins. Install/start/profile-change operations load the selected +trusted plugin to validate or plan that adapter, including a dry-run that uses +it. Revoking trust does not stop a session already running in that process. + ## License MIT diff --git a/docs/ARCHITECTURE_DECISIONS.md b/docs/ARCHITECTURE_DECISIONS.md index c0a45ac..aa87f63 100644 --- a/docs/ARCHITECTURE_DECISIONS.md +++ b/docs/ARCHITECTURE_DECISIONS.md @@ -26,9 +26,11 @@ tests; until then, durable records are append-preserving. The service layer is an in-process API. The agent-facing transport is MCP over stdio and the non-MCP fallback is the structured CLI. The Fleet Console exposes -a read-only loopback HTTP projection of the same services. There is no required -daemon, Unix socket, network message queue, or hosted database. A future socket -transport must preserve identity binding and cannot become the data model. +a read-only loopback HTTP observation projection, plus only specifically +approved local owner actions documented in separate decisions. There is no +required daemon, Unix socket, network message queue, or hosted database. A +future socket transport must preserve identity binding and cannot become the +data model. ## ADR-003: Project identity and repository moves @@ -118,14 +120,25 @@ claim supply-chain authenticity. **Status:** gated. -The current organization model has one required `session-manager` identity and -flat implementation domains. TORCH's Fleet-evolution service can propose -specialist additions, retirements, and domain-boundary changes, but it does not -yet model Program Directors, Fleet Operations, multiple levels of domain leads, -or role promotion as durable authority-bearing relationships. Its current -read-only assessment does not yet produce owner-facing hierarchy proposals. -Do not represent that capability as implemented or encode COMBATRIG's proposed -departments as the default organization. +The installed organization config now accepts a versioned role graph, and +TORCH persists sustained-evidence proposals, exposes them for owner review, +and prepares a read-only pilot plan. A separate owner-confirmed CLI activation +now commits the approved graph, derived manager schedules, ownership-manifest +hashes, and pilot record after freshness checks. It does not launch an identity +or modify a system timer. Doctor reports graph roles bound to missing +identities, and the read-only Console renders approved roles, identity status, +reporting links, coordination links, and implementation ownership. Fresh, +resumed, and on-demand identity briefs now include the current role, +responsibility, authority, reporting, direct-report, coordination, and +implementation-ownership context from the active graph, bound into the prompt +digest. A new read-only hierarchy assessment measures recurring cross-domain +backlog, coordination-request, manager-approval, and handoff events in each +project's configured observation window; it recommends review but never creates +or activates a role. Real-fleet measurement, roster binding across every +activation path, pilot outcome measurement, adoption, and reversal remain +incomplete. +Do not represent the full adaptive organization as implemented or encode +COMBATRIG's proposed departments as the default organization. The target design is an owner-approved, versioned organization graph generated from each project's repository and specification. It separates @@ -136,20 +149,312 @@ communication. Reporting links do not confer worktree access, task reassignment, or release authority. One shared backlog remains authoritative; program and domain documents are summaries, not task queues. -Before implementation, a follow-up decision and acceptance scenarios must -define role schemas and permissions, Session Manager compatibility, how the -Fleet assessment detects sustained coordination pressure and deduplicates -owner-facing proposals, how the Session Architect compares role changes with -process or specialist changes, owner approval, safe promotion/reassignment, -active-task and branch preservation, staged provisioning, pilot measures, -startup/wind-down order, rollback, and hierarchy-aware doctor and console -views. A proposed lead must own a concrete integrated outcome, and headcount -alone must never trigger an added management layer. +A proposed lead must own a concrete integrated outcome. In this design, +headcount alone must never trigger an added management layer. Before this ADR +can be closed, TORCH still needs real fleet-pressure assessment, pilot outcome +measurement, adoption, and reversal. Owner-approved pilot activation is +available through the CLI and the local Console's fresh-plan, same-origin +preview/confirm flow. Active tasks, branches, messages, and implementation +ownership must remain preserved throughout. + +## ADR-012: Per-identity runtime and model portability + +**Status:** accepted as a product requirement; implementation gated. + +TORCH's policy, identity, backlog, and inter-session communication are +provider-independent. Each persistent identity must be assignable an +owner-reviewed runtime profile, including adapter, model identifier, optional +reasoning settings, and launch policy. A single Fleet may mix Claude, Codex, +Pi, other local agent CLIs, and different model tiers at once. Provider/model +labels remain project configuration rather than TORCH-wide assumptions. + +An adapter registry must combine built-in and explicitly installed local +plugins. Plugins declare lifecycle capabilities and unsupported settings; +TORCH may not silently substitute a runtime. Durable TORCH messages remain the +inter-session source of truth when a runtime lacks native messaging or live +steering. Runtime-profile changes preserve identity, backlog, worktree, and +resume state, and remain separate from explicit process startup. + +The current implementation is partial: install/config, doctor, and lifecycle +startup share a capability-declaring registry; identities can select registered +adapters and model/reasoning profiles; unsupported settings block launch instead +of falling back. Built-in adapters include Claude, Codex, and Pi. The Pi adapter +uses explicit provider/model profiles, Pi's extension API, and an identity-bound +TORCH MCP server started only for individual tool calls; it runs one resumable +turn per process and stores Pi transcripts outside the repository. Pi's local +CLI contract and bridge are covered by regression tests, and installed Pi +0.80.3 passes an offline, no-session extension startup without a provider turn. +Other Pi versions and live provider resume remain qualification gates. Custom +adapter registration still requires an +embedding caller for programmatic use. Profile review and changes are available +through `torch profile show`, `torch profile +set`, and `torch profile defaults`; they validate and write only project-local +config and do not launch runtimes. A user-managed local plugin trust flow now +pins an absolute CommonJS entrypoint to an explicitly reviewed SHA-256 outside +the repository, validates it before registry loading, and lets profile changes +select newly trusted adapters without editing TORCH source. Project config +cannot grant plugin trust, `runtimes list` does not execute plugins, and +revocation does not affect an already-running process. Launch policy uses a +nested `launchPolicy` object in a runtime default (`runtimes.`) and/or +identity (`session_manager` or `domains[]`). Identity keys override matching +default keys; omitted keys inherit. Adapters declare exact supported +field/value sets. Unknown fields/values, unsafe modes, and incompatible +combinations block doctor and startup planning before a runtime process is +executed. Custom adapters without `capabilities.launchPolicy.fields` accept no +overrides. `torch profile show`, `profile set`, and `profile defaults` expose +effective policy; edits require `--yes` and change future plans only. + +The local Console also supports per-identity changes through a same-origin, +single-use owner preview followed by explicit confirmation. Preview runs the +same CLI validation path as profile edits; confirmation rechecks the exact +configuration digest before writing and records an owner audit event. The UI +shows current versus next-launch state and does not start or alter a running +session. Only built-in and verified, explicitly user-trusted adapters are +offered as available choices. + +Codex supports `sandbox=read-only|workspace-write|danger-full-access` and +`approval=on-request|never|approve-for-me`. `approve-for-me` maps only to +`--approve-for-me` and conflicts with an explicit sandbox because the flag +implies workspace-write; other approval modes map to `--ask-for-approval`. +Fresh defaults preserve the previous invocation with `approval=approve-for-me` +and no redundant sandbox field. The historical generated flat pair +`sandbox=workspace-write`, `approval=approve-for-me` is interpreted as that same +effective mode and migrated by the profile CLI before policy edits. Claude +supports only `permissionMode=default|acceptEdits|auto|manual|dontAsk|plan`; +`bypassPermissions` is excluded. Pi exposes no policy overrides and retains +TORCH's fixed `--no-approve`, `--no-extensions`, and `--no-context-files`. + +Remaining portability gates: supported-version Pi runtime matrix coverage and +live mixed-provider resume evidence. +Pi extensions execute with Pi's operating-system permissions, so only the +bundled reviewed bridge is loaded for TORCH sessions; project extension +discovery and project trust are disabled for these invocations. + +Built-in starting profiles are Codex `gpt-6-luna` with high reasoning effort +and Claude `sonnet`. These are editable defaults, not fixed policy: after +install, change `runtimes.codex.model` / `runtimes.codex.reasoning` or +`runtimes.claude.model` in `.torch/torch.yaml`; set `model` and/or `reasoning` +on `session_manager` or an individual `domains[]` identity to override that +runtime's default. Profile edits do not start, stop, or replace sessions. +Codex receives the selected reasoning effort as an invocation-scoped +`model_reasoning_effort` override, leaving the user's global Codex config +untouched. + +Runtime defaults and per-identity overrides are resolved consistently by +startup planning, doctor, the live identity/agent interfaces, and dashboard +snapshots. Agents can query their own effective profile without relying on a +stale prompt copy. Owners can use `torch profile show`, `torch profile set`, and +`torch profile defaults` to review or edit installed project-local profiles. +Each write requires explicit `--yes`, validates the full project config, and +only changes future launch plans; it never starts or reconfigures a running +session. + +## ADR-013: Repository analysis snapshot and approval freshness + +**Status:** accepted. + +Repository analysis operates on the live working tree: tracked files plus +unignored, non-symlink regular files not yet tracked. It reports tracked and +untracked coverage separately and never executes project content. A domain +proposal binds Git history, `HEAD`, external specification hashes, and a +working-tree fingerprint over visible paths plus analysis-relevant source and +manifest contents. Installation recomputes this fingerprint; any relevant +working-tree drift invalidates approval even when the commit has not changed. +Ignored files are excluded, and the fingerprint is provenance only—it is not +an authorization token. + +The deterministic baseline assigns a shared path to a domain only when the +repository graph shows that domain importing that shared component. Other +repository-level candidates remain visible for Session Architect and owner +review; they are not blanket-granted as editable shared paths to every domain. + +## ADR-014: Local visual evidence and owner feedback + +**Status:** accepted. + +Visual review evidence is registered only against an existing backlog task owned +by the publishing Fleet identity, with a full local Git commit SHA and the +runtime session ID captured at publication time. TORCH accepts bounded PNG, +JPEG, WebP, and GIF files from that identity's worktree, rejects symbolic-link +paths, and copies image bytes into the project's private external local state. +The catalog stores content hashes and exposes images only through an +integrity-checking local Console route; project repositories do not gain image +blobs or a second artifact ledger. + +Owner comments use the durable message stream as their source of truth. A +comment preview identifies the image, task, target identity, commit, and +message effect before an explicit owner confirmation creates the message. The +Console's loopback feedback endpoint requires a same-origin JSON request and a +single-use, five-minute token bound to the exact preview; confirmation +recomputes the plan, while stale, cross-origin, expired, or replayed requests +fail without creating a message. The snapshot API remains GET-only, and the +Console gains no general task, identity, or lifecycle write access. Private +model reasoning is never exposed. Missing or changed image bytes appear +unavailable, not as verified evidence. + +## ADR-015: Browser-local saved backlog views + +**Status:** accepted. + +Named Console views are bounded, schema-versioned browser preferences scoped by +stable project identity. A view stores only a name and filters over fields +already present in the authoritative backlog snapshot. It does not duplicate +tasks, statuses, ownership, or queue ordering, and it never calls a write API. +Storage errors or malformed data fall back to the unfiltered backlog with an +explanatory status. View edits are explicit: owners may save a new view, update +the selected one, or delete it; each action affects only this browser's saved +presentation preferences. + +## ADR-016: Backlog-derived feature and milestone progress + +**Status:** accepted. + +Backlog tasks may carry optional bounded `feature` and `milestone` labels. +The Session Manager may set labels at task creation or update them with a +revision-checked, reasoned classification action; task-local history and the +control-plane audit record preserve the change. Labels never change task state, +owner, dependencies, evidence, or queue order. The Console derives group status +and completion from the same task records and does not create initiative +records, another task ledger, or inferred project taxonomy. Canceled work is +reported but omitted from the completion denominator. Test, integration, +release, deployment, and live-verification gates remain distinct evidence and +cannot be inferred from progress percentages. + +## ADR-017: Preview-confirmed owner priority changes + +**Status:** accepted. + +The loopback Console may expose one bounded owner control: change a task's +priority. Every change requires a same-origin loopback request, an explicit +reason, a revision-bound single-use preview, and separate confirmation. The +service records the owner, reason, prior/new values, timestamp, revision, and +audit event. The action cannot change state, ownership, evidence, dependencies, +integration, or release status. The snapshot endpoint remains GET-only and no +general-purpose Console mutation API is introduced. + +## ADR-018: Preview-confirmed owner task proposals + +**Status:** accepted. + +The loopback Console may let the configured project owner propose one new +backlog task through an exact, same-origin, five-minute preview and a separate +confirmation. Confirmation creates the task as `proposed` and unassigned; +selected domains are routing suggestions only. The Session Manager retains +triage, readiness, dependency, and assignment authority. Creation records an +owner audit event and is idempotent for a repeated confirmation token. It does +not start, stop, or steer any runtime. The task remains in the existing +authoritative backlog and the observation endpoint remains GET-only. + +## ADR-019: Serialized canonical integration + +**Status:** accepted. + +TORCH owns one serialized landing authority per project. Landing acquires a +cross-process project-state write lock before revalidating the request, target +branch, exact check receipts, authorization, and fast-forward eligibility. The +lock remains held through the canonical Git ref update and durable result +record. A competing lander fails closed with a retryable busy result and must +recompute its plan; after target advancement, stale candidates converge and +rerun required checks on the exact resulting commit. Authorized requests are +drainable in durable insertion order through one explicit `torch integrate +drain` invocation. After each landing, later requests are re-evaluated; sibling +tips that omit the new target become `needs_convergence` until their owner +merges the current target and records fresh exact-commit checks. The drain is +one-shot by default, requires explicit confirmation, and is not installed as a +daemon or timer. An owner may separately define an exact-config system schedule +to invoke the same drain no more frequently than once per minute; each ticket +still needs its own landing-authority authorization and fresh exact-commit +receipts. The schedule cannot merge/converge worktrees, push remotes, or start +AI runtimes. Specialists do not push directly to the canonical branch. +External forge credentials can bypass this local queue, so branch +protection/credential isolation remains optional defense in depth rather than a +false claim of local enforcement. + +## ADR-020: Current instruction rehydration on every resume + +**Status:** accepted. + +Every fresh start and resume composes a digest-addressed instruction bundle +from the canonical project checkout. TORCH delivers that current bundle through +the selected adapter's supported resume mechanism and exposes the same text and +digest in the Fleet brief. Current instructions override historical conversation +content, and agents re-read the current brief before each new work item. The +bundle is never sourced from a potentially stale specialist worktree. If an +adapter cannot apply the bundle, TORCH must disclose or block rather than claim +the resumed agent has current instructions. + +## ADR-021: Hierarchy-aware manager check-ins + +**Status:** accepted; organization-aware inspection, durable queue, typed +system-schedule dispatch, default Session Manager cadence, and an opt-in +adapter-mediated wake with an atomic project-wide daily invocation ceiling are +implemented. Installing a persistent timer and live provider qualification +remain gated. + +Periodic check-ins derive each manager's direct reports from the active +organization graph. They inspect durable presence/task state, unacknowledged +coordination messages, and explicit approval waits; uncertain natural-language +summaries are not treated as approval facts. Approval requests persist with a +named requester and approver, task/evidence context, state, revision, and +decision metadata. Named AI approvers receive durable inbox messages and may +decide only their own requests; the configured owner decides through explicit +CLI confirmation. Decisions are audited and returned durably to the requester. +Manager check-ins classify direct-report requests by whether the manager, a +peer AI, or the owner must act; they route but never decide on another +approver's behalf. The check-in creates a durable, +bounded request for the manager to inspect and advance its reports. It never +approves, reassigns, edits backlog state, creates identities, or starts provider +sessions by default. If the manager is offline, the event remains queued. An +optional adapter-mediated wake is disabled by default; owner-reviewed schedule +configuration plus a project-wide budget can enable it with an atomic maximum +invocation count shared across manager schedules per UTC day. TORCH uses the configured identity adapter/profile and skips active +managers. This count ceiling is not equivalent to a USD spend ceiling. No +persistent timer is installed without owner approval. New +deterministic project proposals include one 15-minute system schedule for the +Session Manager. Additional manager identities require their own explicitly +configured check-in schedule; project configuration validation rejects an +explicit hierarchy that leaves a manager identity without one. Automatic +schedule generation follows hierarchy planning and activation; activation +surfaces schedule additions and obsolete candidates but does not install or +reconcile a system timer. + +## ADR-022: Explicit fast-forward-only forge publication + +**Status:** accepted. + +Integration and forge publication are separate operations. Specialists submit +verified commits to TORCH's local FIFO integration queue; the landing authority +updates local canonical main. Publishing that landed canonical commit to an +attached forge requires a separate owner-confirmed `torch forge sync --yes` +after a read-only `torch forge sync plan`. TORCH pushes only the exact current +canonical commit with normal Git fast-forward semantics and records the +owner's action and observed result. It never force-pushes, fetches/merges +implicitly, or publishes from the integration drain or its timer. A remote-ahead +or divergent result blocks and requires explicit owner convergence. The +remote's compare-and-update semantics reject a race that advances its ref +after planning. Local integration remains available during forge outages. + +## ADR-023: Organization-aware lifecycle ordering + +**Status:** accepted. + +Startup is topologically ordered by the active reporting graph after role +relationships are projected onto persistent Fleet identities. Managers start +before direct reports. Wind-down reverses that identity order and routes each +active identity's final status to its direct manager identities, preserving +bottom-up reporting as the fleet grows. An identity-level cycle or missing +manager blocks both plans; an acyclic role graph is not sufficient if multiple +roles collapse into a cyclic identity graph. A partial startup leaves already +started managers available for coordination and reports the exact identities +that did start; TORCH does not silently stop them or continue launching after a +failure. Planning alone never invokes a provider. Legacy +`session_manager.start_last: true` remains accepted in existing configs but is +ignored in favor of the active organization graph. ## Consequences These decisions keep the first usable release local-first and provider- independent. They also make current boundaries explicit: repository moves, multi-machine control, non-Linux persistent launchers, published update -channels, and multi-level organization management remain unavailable rather -than silently approximated. +channels, and measurement/adoption of live multi-level organization pilots +remain unavailable or owner-gated rather than silently approximated. diff --git a/docs/PORTABLE_AGENT_FLEET_SPEC.md b/docs/PORTABLE_AGENT_FLEET_SPEC.md index 2311b15..0e20481 100644 --- a/docs/PORTABLE_AGENT_FLEET_SPEC.md +++ b/docs/PORTABLE_AGENT_FLEET_SPEC.md @@ -199,12 +199,15 @@ responsibility. A domain manager is staff; temporary subagents are helpers. **Runtime** -The agent harness used to host a session, such as Claude Code, Codex, or -OpenCode. +The agent harness used to host a session, such as Claude Code, Codex, OpenCode, +Pi, or another locally available agent system. Runtime selection is per +persistent identity, not one project-wide provider choice. **Runtime adapter** -The implementation that maps TORCH session operations to one runtime. +The implementation that maps TORCH session operations to one runtime. Adapters +may be built in or installed as local plugins, and must report capabilities +honestly. **Control plane** @@ -260,9 +263,9 @@ TORCH has four logical layers: │ handoffs, status and session registry │ └───────────────┬────────────────┬────────────────────────┘ │ │ - ┌───────▼──────┐ ┌──────▼───────┐ ... - │Claude adapter│ │ Codex adapter │ - └──────────────┘ └──────────────┘ + ┌───────▼──────┐ ┌──────▼───────┐ ┌────▼─────┐ ... + │Claude adapter│ │ Codex adapter │ │Pi adapter│ + └──────────────┘ └──────────────┘ └──────────┘ ``` The installed engine MUST expose one internal service layer. The CLI, MCP @@ -370,6 +373,15 @@ inside the tracked source tree. detects existing TORCH state, identifies the canonical branch and remotes, and reports what analysis would inspect. +Repository analysis MUST inspect the current working tree, including tracked +modifications and unignored, non-symlink regular files that are not yet tracked; +Git-ignored files are excluded. It MUST report tracked and untracked file +coverage separately and MUST NOT execute repository content. An approved domain +proposal MUST bind the Git history and `HEAD`, external specification hashes, +and a content fingerprint of the working-tree paths and analysis-relevant source +and manifest files. Validation immediately before installation MUST recompute +that fingerprint and reject added, removed, or changed analysis inputs. + It MUST NOT create worktrees, branches, sessions, hooks, remotes, or tracked files before approval. @@ -501,6 +513,15 @@ A proposed domain SHOULD exhibit: 3. independent verification: meaningful evidence can be produced locally; 4. persistent expertise: the area benefits from durable specialist context. +Specification language may guide the Session Architect, but generic keyword +overlap MUST NOT assign a prose-only requirement to an existing implementation +domain. The deterministic baseline may merge a specification responsibility +into a repository domain only when that signal cites a concrete path that +exists inside the analyzed project. Otherwise the signal remains visible for +architect/owner review. A specification-only project may receive provisional +responsibility groups, but without repository paths they MUST remain explicitly +unowned and block installation until ownership is reviewed. + A directory alone is not sufficient justification. Domains such as "programming" are too broad; domains such as "button colors" are normally too narrow. Small projects may need four domains. Large projects may need twenty @@ -521,6 +542,19 @@ code ownership and authority, and preserve direct peer links. Producing the brief MUST NOT itself call a paid provider, execute project content, or approve the resulting organization. +The Session Architect output MUST include a pending `organization_assessment` +that chooses `flat` or `coordination_roles`, states its rationale, and cites +evidence present in the brief. It MUST name proposed coordination roles only +when the evidence supports an integrated outcome; each role MUST state the +domains it coordinates, its bounded decision scope, decisions escalated to the +owner, and evidence. A coordination role MUST NOT receive implementation paths +or owner-approval authority. The assessment MUST separately describe +implementation ownership, coordination responsibility, fleet-operations +authority, project-priority authority, independent-review authority, and +owner-only decisions. It MUST preserve direct peer communication. These +recommendations are advisory: persistent role creation or graph changes use the +separate owner-approved Fleet evolution or hierarchy-pilot flow. + `torch architect plan` MUST show the selected provider, model, isolation, structured-output schema, prompt size, and budget boundary without invoking a model. `torch architect run` MUST require explicit quota authorization, isolate @@ -596,6 +630,34 @@ shared paths, exclusions, neighbours, required checks, scarce resources, authority limits, project invariants, an explicit empty initial-backlog state, and first-move instructions. Runtime prompt composition still combines this reviewable domain materialization with the common Fleet rules. +Generated specialist instructions MUST distinguish domain-specific required +checks from project-wide integration gates. Every specialist receives the +exact configured project-gate IDs and command/argument vectors; this visibility +MUST NOT imply code ownership over those gates or derive a test assignment from +keyword overlap. + +Every start and resume MUST rebuild the effective instruction bundle from the +current canonical project checkout, not a possibly stale worker worktree copy. +TORCH records a content digest, supplies the bundle to the selected runtime on +resume using that adapter's supported mechanism, and makes the digest visible in +`torch brief --area --json`. Before taking each new backlog item, an +identity MUST read the current brief. The latest canonical instructions override +older conversation history; a restart prompt is a pointer to current policy, +not a second policy source. The adapter's resume path must fail closed or +explicitly report when it cannot deliver the current bundle. + +Fleet startup order MUST be derived from the active organization graph so +manager identities launch before their direct reports. Wind-down MUST use the +reverse identity-level order and deliver each active identity's final status to +its direct reporting manager identities, rather than routing every report to a +hard-coded Session Manager. Ordering is computed across roles bound to the same +persistent identity; if collapsing an acyclic role graph creates an identity +cycle, or a non-owner manager identity is absent from the roster, startup and +wind-down plans MUST fail closed. Selective startup preserves the relative +order of selected identities and may intentionally omit managers. Planning +these sequences MUST NOT itself start or stop runtimes. Legacy +`session_manager.start_last: true` values are accepted for compatibility but +ignored; the organization graph is authoritative. ### 10.7 Worktree creation @@ -662,7 +724,6 @@ forge: session_manager: id: session-manager runtime: claude - start_last: true landing_authority: true git: @@ -717,6 +778,13 @@ schedules: owner: session-manager lifetime: session every: 60m + session-manager-check-in: + owner: session-manager + lifetime: system + every: 15m + behavior: coordination + action: manager-check-in + manager_id: session-manager release: provider: filesystem @@ -898,16 +966,21 @@ owner decision → migration plan → bounded pilot measure results → adopt, revise, or reverse ``` -The Session Manager SHOULD run the read-only Fleet assessment at startup and -when recurring coordination friction appears. TORCH SHOULD evaluate evidence -over a project-configured observation window and require a sustained pattern -or multiple independent signals; a single large task or temporary incident -MUST NOT trigger a management proposal. Useful signals include repeated work -across the same domains, coordination requests and handoffs, time waiting for -cross-domain decisions, dependency-related blocked time, rework from late -integration, and the dispatcher repeatedly switching among unrelated areas. -Where measurements are unavailable, the proposal MUST label the evidence as -qualitative rather than imply measured improvement. +The Session Manager SHOULD run the read-only hierarchy assessment at startup +and when recurring coordination friction appears. Each project MUST configure +`organization_assessment.observation_window_days` (initial default: 30 days) +and `organization_assessment.minimum_recurrences` (initial default: 3). +TORCH MUST evaluate evidence inside that window and require either a sustained +pattern meeting the recurrence threshold or multiple independent signal types; +a single large task or temporary incident MUST NOT trigger a management +proposal. The assessment reports dated backlog tasks spanning the same domains, +coordination requests, named approvals awaiting the Session Manager, and +handoffs routed to it. The assessment is advisory and read-only: it never +creates a proposal or changes the active organization. Before proposing, the +Session Manager should also inspect qualitative context such as dependency +rework, late integration, and repeated context switching. Where measurements +are unavailable, the proposal MUST label the evidence as qualitative rather +than imply measured improvement. Before proposing a lead, the Session Manager MUST consider whether a clearer interface, direct peer agreement, backlog sequencing, a new implementation @@ -926,8 +999,15 @@ the project's single owner-facing coordination role and include: No repeated assessment should create or repeatedly resend the same proposal. TORCH SHOULD deduplicate against open organization proposals and present only -material updates to the owner. The owner may approve, reject, or defer it; a -recommendation alone never changes roles, authority, routing, or sessions. +material updates to the owner. An exact retry is idempotent; materially new +evidence with a later observation-window end MUST revise the existing open +proposal, preserve its earlier proposal revision, and invalidate any stale +owner-review preview. A material update without a later evidence window MUST +be rejected. Once an owner has approved, rejected, or deferred a proposal, a +later evidence window MAY create a new proposal for the same organization graph; +a terminal decision MUST NOT suppress future reconsideration. The owner may +approve, reject, or defer it; a recommendation alone never changes roles, +authority, routing, or sessions. The Session Manager or a designated organization planner MAY propose a change, but MUST NOT activate it unilaterally. The owner approves the intended @@ -940,6 +1020,32 @@ session provisioning, and runtime launch are separate steps; runtime launch remains an explicit action. Existing specialist-to-specialist communication and backlog history MUST survive a hierarchy change. +For a pilot that changes only existing identity relationships, `hierarchy-plan` +MUST show the fresh graph delta, resulting manager-check-in schedule set, +obsolete schedule candidates, active work, and timer reconciliation requirement. +After owner approval, a separate `hierarchy-activate --by owner --yes` action +or an equivalent owner-confirmed local Console action may apply the approved +graph and newly required manager schedules to `.torch/torch.yaml`, update the +ownership manifest, record the pilot, and commit those tracked changes. +Activation MUST recheck the base commit, graph digest, clean worktree, owner +identity, active identities, and resulting config schema. +The Console action MUST preview the exact migration plan, require same-origin +loopback access, bind confirmation to a short-lived one-use plan digest, and +refuse stale state before committing; replaying the same confirmation MUST be +idempotent. +It MUST preserve existing cadence, leave obsolete schedules for explicit owner +review, preserve implementation ownership and active identities, and MUST NOT +create a session, provision a worktree, start a runtime, or change systemd +state. If an exact-config system timer is already installed, the plan MUST +report that it becomes stale; the owner separately reviews and runs schedule +launcher reconciliation after activation. Measuring the pilot, adopting it, or +reversing it remains a separately reviewed owner action. + +Organization proposals MUST reference identities already present in the active +Fleet roster. A genuinely new persistent identity follows the separate +owner-approved Fleet-evolution and provisioning path; a hierarchy proposal +cannot smuggle in a new session or executable. + The pilot MUST start with the smallest affected group and retain a way to continue under the prior organization. At the end of its review window, the owner-facing role receives a comparison with the baseline using the proposal's @@ -947,6 +1053,62 @@ success criteria. TORCH may recommend adoption, adjustment, or reversal; only the owner approves the next organizational change. A successful pilot is not permission to add unrelated layers automatically. +Pilot comparisons are recorded through `torch fleet hierarchy-review-pilot +--proposal --review --from session-manager --yes` and read +through `torch fleet hierarchy-pilot-reviews --proposal `. Reviews MUST +cover every baseline metric and success/stop criterion exactly once, preserve +the baseline units, and link evidence for each observation. Each metric is +explicitly measured, qualitative, or unavailable; values and criterion +assessments are reviewer-reported evidence, not independently verified facts. +Review records retain the activation commit and active graph digest, observed +values beside baseline values, review-window maturity and recommendation. +Before recommending adoption, the configured pilot duration must elapse, all +comparisons must be reported as measured, success criteria must be met and no +stop criterion may be met or unknown. Interim inconclusive/adjustment/reversal +reviews remain possible. Exact repeated evidence is idempotent and review +history is durable and audited. Recording a review does not adopt or reverse +the pilot, change the graph or tracked files, or start a runtime. + +The read-only Console projects the latest bounded pilot-review history on each +hierarchy proposal, displaying baseline and observed values side by side, +units, evidence classifications, interim/full-window status, criterion +assessments and references. Reviewer-reported evidence MUST NOT be labelled +independently verified, and the view MUST distinguish a recommendation from an +owner conclusion. Reading the snapshot or opening review history MUST NOT +modify reviews, organizational state, or runtimes. + +Owner conclusions use `hierarchy-conclusion-plan --proposal --decision + --reason ` followed by `hierarchy-conclude` with the same +arguments, `--plan-hash --by owner --yes`. The plan MUST bind the +current commit, active graph, owned pilot record, manifest, latest review, +active work and resulting relationships. A dirty repository, changed graph, +stale plan hash or missing owned record blocks the change. Adoption requires +the latest review to support adoption and leaves the active graph unchanged. +Reversal restores prior relationships from the proposal's base commit as a +new monotonic graph revision; implementation ownership must still match and +all referenced identities must exist. It preserves unrelated config, all +identities, worktrees, backlog history and communication records. Existing +schedules are retained, with obsolete candidates and any required timer +reconciliation reported for separate owner review. + +The conclusion updates the owned tracked pilot/config/manifest files in a +scoped commit and records the owner decision in a local audit transaction. +Failed tracked commits restore the files and index. If the tracked commit +succeeds but the local transaction is interrupted, the same owner confirmation +may reconcile the committed, manifest-verified receipt against the current +graph; it MUST NOT repeat the commit. Confirmed retries are idempotent. Neither +adoption nor reversal launches/stops a runtime or modifies systemd state. + +The local Console exposes the same separate owner conclusions for piloting +proposals. Preview MUST show the exact conclusion plan, affected work, +resulting graph, preserved schedules and blockers. Blocked plans receive no +confirmation token. Same-origin loopback access, short-lived one-use previews, +target/operation/action/reason binding and fresh plan revalidation are required. +Confirmation uses the underlying owner-authorized service plan hash; replaying +the same confirmed request is idempotent, while altered payloads or a token +from a different operation MUST be rejected. Adoption MUST recheck calendar +maturity at conclusion time, not merely trust a stored full-window flag. + On wind-down, coordination roles SHOULD record concise domain or program state, while Fleet Operations preserves session and restart state. This reporting supplements the backlog and does not replace it. @@ -1005,6 +1167,7 @@ The control plane MUST provide provider-independent operations for: - durable messages and acknowledgements; - coordination requests and handoffs; - status, completion, and blocker reports; +- durable approval requests, scoped visibility, and named-approver decisions; - backlog references; - resource leases; - check and integration state queries. @@ -1024,6 +1187,9 @@ torch_ack_message torch_report_status torch_report_complete torch_report_blocked +torch_request_approval +torch_list_approvals +torch_decide_approval torch_request_coordination torch_request_handoff ``` @@ -1082,28 +1248,147 @@ turn or polling cycle. Claude is the first reference adapter because COMBATRIG proves its operating model. Codex is the second required adapter and is the portability proof. -OpenCode and other runtimes follow the same contract. +OpenCode, Pi, and other runtimes follow the same contract. TORCH policy and +durable inter-session communication remain provider-independent; runtime-native +messaging is optional and cannot replace the TORCH control plane. + +Every adapter MUST declare `perInvocationCostCeiling` as separate supported +mode identifiers for `createSession` and `resumeSession`. When a schedule +requires a maximum USD spend, TORCH passes that amount into the selected exact +operation and accepts a launch plan only if the adapter returns an enforced +receipt containing the same amount and declared mode. Missing, mismatched, or +unsupported declarations/receipts block before invocation and before daily +budget reservation. Telemetry or an estimated price is not enforcement. +Adapters must not claim support unless the selected runtime mode itself can +enforce the limit. The built-in Claude, Codex, and Pi adapters currently +declare no supported hard-cap modes. They MAY run owner-enabled count-limited +manager wakes; USD-hard-cap mode remains unavailable until a verified +enforcement implementation exists. Count-limited mode must never claim a +dollar spending ceiling. A single Fleet MAY mix runtimes and models by domain. Git provider choice and AI runtime choice are independent. +### 14.1 Per-session runtime and model profiles + +Generated proposals MUST use `runtime: "default"` for roles without an explicit +provider assignment. Installation resolves those roles to its selected default +adapter. `--runtime codex` MUST permit a Codex-only installation; selecting +multiple adapters MUST preserve explicit per-identity assignments. The +`--default-runtime` option selects the inherited adapter independently of the +available adapter list. Without that option, an explicit Session Manager +assignment takes precedence, followed by the first selected adapter. With no +selection or explicit assignment, the existing CLI compatibility default is +Claude. The dry-run MUST show the resolved default. Pending Session Architect +designs may name Pi or a custom adapter; installation MUST still validate +availability and plugin trust before writing project state. + +Every persistent identity, including the Session Manager and optional +coordination roles, MUST be assignable its own owner-approved runtime profile. +A profile selects a locally available runtime adapter, model identifier, +reasoning/effort setting when supported, and launch policy. Different sessions +in one Fleet MAY use different providers and models at the same time—for +example, Claude, Codex, Pi, or separate model tiers such as `luna` and `astra`. +These labels are configuration values, not hard-coded TORCH model assumptions. + +For a new installation, the built-in defaults SHOULD be Codex +`gpt-6-luna` at high reasoning effort and Claude `sonnet`. Users MUST be able +to change runtime defaults and per-identity overrides in the installed project +configuration without modifying TORCH source or global provider settings. +Changing a profile MUST NOT start or stop a session. + +Launch policy MUST be a nested `launchPolicy` object on a runtime default +(`runtimes.`) and optionally an identity. Identity fields override +matching defaults; omitted fields inherit. Each adapter MUST declare exact +supported field/value sets. Nonempty policy on a custom adapter without that +declaration, unknown values, unsupported fields, unsafe settings, or +incompatible combinations MUST block doctor and planning before runtime +execution. `torch profile show` MUST expose effective policy; profile set and +defaults MUST accept repeated `--launch-policy field=value` and +`--reset-launch-policy`, require `--yes`, and affect future plans only. +The local Console MUST offer the same per-identity profile choices through a +same-origin owner preview and separate confirmation. It MUST show the exact +current and next-launch profiles, revalidate configuration freshness before +writing, and audit the owner action. It MUST NOT start, stop, or reconfigure a +running session. Runtime adapters shown as choices MUST be built-in or +explicitly user-trusted; an unavailable or changed plugin MUST be visibly +unavailable rather than silently substituted. + +Codex supports `sandbox=read-only|workspace-write|danger-full-access` and +`approval=on-request|never|approve-for-me`. `approve-for-me` maps to +`--approve-for-me` and cannot combine with explicit `sandbox`; other approval +values map to `--ask-for-approval`, and sandbox maps to `--sandbox`. The fresh +default remains `approve-for-me` (implicitly workspace-write), preserving the +current invocation. Historical generated flat `sandbox=workspace-write` plus +`approval=approve-for-me` is recognized as that effective policy and migrated +to nested form on an owner policy edit; incompatible legacy values fail closed. +Claude supports only `permissionMode=default|acceptEdits|auto|manual|dontAsk|plan`; +`bypassPermissions` and danger-skip flags are prohibited. Pi accepts no +overrides and MUST keep TORCH's fixed `--no-approve`, `--no-extensions`, and +`--no-context-files` flags. + +An installed Fleet MUST detect unavailable adapters before startup and explain +which identities cannot launch. It MUST NOT silently substitute a provider or +model. Adapters declare unsupported settings and lifecycle capabilities; TORCH +uses durable messages, acknowledgements, and resume state when native steering +is unavailable. A runtime-profile change is tracked and owner-reviewed, +preserves Fleet identity and backlog, and does not itself launch a process. +Runtime starts remain explicit actions with configured authorization and +budget boundaries. + +TORCH MUST NOT restrict adapters to a hard-coded vendor list. An explicitly +installed local plugin may participate when it satisfies the adapter contract +and can receive TORCH's scoped identity-bound MCP or equivalent control-plane +endpoint. Plugin installation and executable allowlisting are explicit local +configuration choices; model proposals cannot add arbitrary executable paths +or launch commands. + +The first local plugin flow uses a user-owned trust store outside the project: +`$XDG_CONFIG_HOME/torch/runtime-adapters.json` (or `~/.config/torch/` when +`XDG_CONFIG_HOME` is unset). The initial adapter entrypoint contract is a +canonical absolute `.cjs` file exporting +`createTorchRuntimeAdapter({ env })`. `torch runtimes trust` first displays the +entrypoint path and SHA-256 without executing it; approval must echo that exact +hash with `--yes --sha256`. A changed entrypoint invalidates the reviewed hash +and fails closed. Project-tracked configuration may select an adapter already +trusted by the user, but cannot add, change, or authorize a module. `torch +runtimes list`, doctor, and profile inspection must not load plugin code. +Loading a trusted plugin executes it and its imported dependencies with the +current user's operating-system privileges; the entrypoint hash does not pin +those dependencies. Install/start/profile-change operations may load the +selected adapter to validate or plan it, including a dry-run that uses it. +Revocation affects future registry loads and does not stop an already-running +process. The user must stop a session separately before revoking its adapter. + ## 15. Session lifecycle ### 15.1 Fresh start `torch up --fresh` creates new runtime conversations while preserving Fleet identities. It combines the common prompt, domain prompt, current approved -configuration, and initial resume instructions. Runtime IDs are captured as -soon as they are available. +configuration, and initial resume instructions. The generated instruction +bundle MUST include the current identity's organization roles, +responsibilities, authority, reporting line, direct reports, coordination links, +and implementation ownership from the active organization graph. This +structured context is metadata, not an authority grant; it is included in the +instruction digest so changes reach the identity on its next start or resume. +Runtime IDs are captured as soon as they are available. -The Session Manager starts last so workers and the control plane exist before -dispatch begins. +Runtime identities start in parent-before-report order derived from the active +organization graph, so managers are available before their direct reports. +Wind-down reverses that order and routes each active identity's final status to +its direct manager identities. If multiple roles mapped to one persistent +identity create an identity-level cycle, TORCH blocks the plan rather than +guessing. Planning remains read-only; actual runtime startup still requires the +explicit owner-approved launch operation. ### 15.2 Resume Normal `torch up` resumes known runtime sessions when safe. If a conversation cannot be resumed, TORCH creates a replacement runtime session for the same -Fleet identity and supplies durable state. Loss of a conversation MUST NOT +Fleet identity and supplies durable state. The current organization metadata is +recomputed from canonical project configuration on each resume; stale generated +or worktree prompt copies MUST NOT override it. Loss of a conversation MUST NOT erase ownership or unfinished work. ### 15.3 Presence @@ -1224,7 +1509,16 @@ cancelled Items SHOULD identify owner, dependencies, affected domains, acceptance criteria, evidence, the commit where the issue was observed, and current commit -when applicable. +when applicable. A task MAY also carry free-text `feature` and `milestone` +labels, each bounded to 120 characters. These labels group existing work for +program visibility; they do not create a second ticket, initiative registry, or +queue. + +The Session Manager MAY set those labels when creating work or classify an +existing item using its current revision, with a reason recorded in task-local +classification history and the project audit stream. Classification MUST NOT +change task state, assignment, evidence, or dependencies. Specialists cannot +change initiative labels. Labels may be explicitly cleared. The queue is singular across Program Director, domain lead, Session Manager, and specialist views. Management layers sequence and route this queue; they do @@ -1239,12 +1533,84 @@ item. This invariant is serialized across different task files, not merely protected by per-task revision locks. Blocked work is durable but not runnable; it may be reconsidered explicitly when its dependency evidence changes. +### Policy-controlled specialist self-claim + +Manager assignment remains the default. An owner-reviewed project configuration +MAY enable `backlog.self_claim: { enabled: true, areas: [...] }` for explicit +specialist identities. Omitted policy and new installations are disabled. +Coordination/manager roles do not gain implementation ownership through this +policy. Current canonical configuration, not a conversation or cached service +copy, determines eligibility on each request. + +`backlog claim-next --dry-run` / `torch_plan_backlog_claim` previews eligibility. +`backlog claim-next --yes` / `torch_claim_next_backlog_task` resolves existing +active work first without mutation, then atomically selects and assigns one +eligible ready item. Selection honors priority (urgent, high, normal, low), then +creation time and task ID. It MUST NOT select proposed, blocked, dependency-gated, +unrouted work or work reserved for another owner. Self-claim requires the caller +to be explicitly listed in both project policy and the task's affected domains. +General unrouted requests remain manager-routed work; no filler is invented. + +Before a new assignment, verify the managed specialist worktree is present, +clean and on its mapped branch, with no active Git operation, check/measurement/ +pin guard, prepared/running check, unreleased resource, waiting resource request, +unfinished integration request or pending named approval for that identity. +Blocked plans return concrete reasons and do not change work or release waits. +An existing assignment remains resumable even if permission to claim new work +has since been revoked; this is observation, not a new grant of authority. + +Manager assignment, self-claim and all transitions into active assignment +states share the same project-wide assignment lock and task revision checks. +A blocked item MUST NOT reactivate as a second active task while its owner has +other active work. Lock contention fails closed; it never steals a stale lock. +Claim history and audit identify the actual specialist, not a fabricated +manager identity. Self-claim does not send acknowledgement-only assignment +messages to itself or authorize completion, release, deployment or ownership +changes. The existing evidence and landed-commit completion rules remain. + +After an item is finished and its work/tests/integration are settled, specialists +re-read current instructions and resolve their next item. Routine progress is +recorded in commits, tasks and durable status. Escalate decisions outside scope, +cross-domain conflicts, owner-sensitive approvals, main regressions and at-risk +work. Direct specialist communication remains available. + Backlog health is read-only and reports exceptional conditions, including multiple active assignments, stale assigned work, resolved dependencies on a blocked item, missing or retired areas, old observation commits, ready work without a live eligible specialist, and active work whose runtime session is missing. Health never rewrites or reassigns work automatically. +### Commit-linked activity + +Read-only activity observation scans canonical and managed local branch history +within approved `backlog.activity.stale_days` and `max_commits` limits (defaults +3 days and 1000 commits). Exact task IDs in commit messages link activity to +the single authoritative backlog. Metadata-only task edits MUST NOT refresh +the commit-activity clock. Owner-originated requests sort first; blocked work +is identified as expected waiting, not silently revived. Completed/cancelled +items are excluded from stale findings. Missing branches or truncated coverage +MUST yield unknown absence of activity, not a false assertion of neglect. +Future-dated commits do not count as current progress. These observations are +Git-reported metadata, not independent evidence of meaningful implementation. + +Standalone `Closes: TASK-id[, TASK-id...]` trailers express completion intent, +never authority. Activity observation never mutates task state. Approved +`backlog.activity.auto_close_after_landing` enables post-landing reconciliation +of trailers from the exact submitted tip; default is disabled. Reconciliation +MUST require matching project/target landed integration evidence and canonical +ancestry, current approved required checks, specialist ownership, exact task +commit, task evidence and `ready_to_integrate` state. It uses the normal +revision-checked completion transition under a task lock. Unknown or mismatched +items stay open; exact already-completed items are unchanged on replay. + +Fleet Operations can preview and explicitly reconcile a landed request through +CLI/MCP after interruption. Automatic reconciliation failures MUST NOT reverse +or misreport a successful Git landing; they surface `needs-review` and a +durable audit when available. Partial completion is retryable per task, not an +atomic transaction across all mentioned items. A commit message alone MUST NOT +close a task or authorize deployment. Daily reviews and dashboard surfacing +remain separate pending work; observation installs no timer. + Owner decisions affecting future work MUST be recorded durably. Chat messages alone are insufficient. Project source, tests, approved configuration, and accepted decision records outrank summaries and generated prompts. @@ -1283,8 +1649,111 @@ Projects SHOULD retain scenario-level checks at meaningful system boundaries. Fleet infrastructure itself MUST have acceptance tests that demonstrate both success and failure behavior. +### Frozen inputs for queued checks + +A project MAY declare `snapshot: { paths: [...], source_commit_file: "..." }` +on a check. Paths are explicit repository-relative inputs, including ignored +generated build files when appropriate. The project writes the full source +commit SHA to the marker as part of its build. The marker MUST match the clean +source worktree commit and MUST be included in the captured inputs. This is +project-declared provenance, not independent proof that its builder is correct. + +`checks prepare` / `torch_prepare_check` captures inputs before the specialist +requests a scarce-resource slot. Copies use reflinks when supported or ordinary +independent copies; they MUST NOT hardlink mutable build files. Captured files +are hashed, input files are read-only, and source/copy changes during capture +fail closed. Symlinks, traversal, Git metadata, and nonregular inputs are +rejected. Frozen state lives outside the worktree in TORCH-owned local state. + +After preparation, acquire the configured resources through the existing FIFO +resource service, then run `checks run-prepared` / `torch_run_prepared_check`. +The run MUST verify captured content, unchanged approved check policy, bound +identity and active resource leases. It executes from the captured input root; +later specialist commits do not invalidate that historical subject. A passing +receipt applies only to the captured SHA and policy, never a later worktree +tip. A changed snapshot, lost lease or failed executor yields nonpassing +evidence. Output-writing checks should put their outputs outside the declared +input paths. This mechanism is input isolation, not a process sandbox: approved +commands must use their captured working directory instead of absolute live +build paths or an unrelated existing server. + +Preparation and run claim are durable. Completed/cancelled runs remove only +the exact owned input copy while retaining its manifest/digest and receipt. +An interrupted `running` record is not automatically rerun or cancelled: +unknown process state requires operational inspection. Resource acquisition +and release remain explicit operations; preparing/cancelling a check does not +steal, release or cancel an independently held resource. No background runner +or persistent timer is implied by preparing inputs. + +Interrupted prepared checks MUST remain non-runnable until explicitly resolved; +age, a missing heartbeat or an observation timeout is not stopped-executor proof. +The runner identity is recorded before measurement. Linux PID start/boot identity +can distinguish reused PIDs; other process observations remain conservative. +Recorded live runners MUST block recovery. Missing, foreign-host or legacy runner +metadata is unknown, not independent evidence that executors stopped. + +`torch checks recovery-plan --prepared ` is an owner-only read-only preview. +`torch checks recover-prepared --prepared --executors-stopped --evidence + --yes` requires explicit owner evidence covering the runner and all child +executors. It records terminal `abandoned` state plus an audit atomically, never +creates a receipt, never resumes execution and never releases resource leases. +Cleanup follows only for the exact owned snapshot; ownership mismatch leaves +cleanup pending and explicit repeat recovery can retry without duplicate audit. +The owner attestation is not an automatic descendant sensor. Future measurements +require newly prepared inputs and independent lease handling. +Cleanup outcomes MUST persist for observation. Interrupted outcome persistence +retains pending rather than claiming completion. The Console MUST retain abandoned +check visibility with completed/pending/unconfirmed cleanup labels; missing or +unreadable legacy metadata stays unconfirmed. Projection MUST omit private input +paths, owner evidence and process IDs, and MUST NOT probe files, recover checks, +or imply independently verified descendant termination. Replay updates cleanup +outcome without repeating the original owner-authority audit. + +Frozen receipts MUST link to a finished prepared operation before exact-pass +qualification. Receipt persistence alone after a partial/crashed run is not enough. + +### Project-defined test conditions + +A check MAY declare a `conditions` policy with a shell-free probe command, +arguments, bounded `timeout_seconds` (default 30, maximum 300), and nonempty +unique `required` conditions of `{ id, equals }`. Expected values are literal +strings, booleans, finite numbers or null; no coercion is permitted. TORCH does +not hardcode game-specific switches, GPU, clock or visibility rules. + +The probe MUST return one bounded JSON object with schema +`torch.dev/check-conditions/v1alpha1`, a `subject` naming its actual full commit, +and a `conditions` object. For frozen inputs, `subject.inputDigest` MUST match +the captured digest. TORCH provides `TORCH_CHECK_COMMIT` and +`TORCH_CHECK_INPUT_DIGEST` to both probes and measurement commands as expected subject metadata. A project +probe MUST observe the tested build/runtime, not simply echo expectations. + +A report MAY include a `runtimeId` (nonblank string, at most 256 characters). +If preflight supplies one, postflight MUST supply the same identity; a missing or +changed identity invalidates even otherwise healthy conditions. Supplied malformed +identities invalidate preflight. Projects SHOULD use an identity generated by the +actual tested runtime, not a constant derived from its build. Legacy reports with +no identity in either probe remain supported but do not prove runtime continuity. + +TORCH probes after resource acquisition and before measurement execution. A +failed, missing, oversized, malformed, mismatched-subject or mismatched-condition +report MUST prevent measurements from starting and produce an incomplete +receipt, not a product-test failure. It probes again after execution; an invalid +postflight MUST invalidate a green measurement. The harness is responsible for +observing the same page/runtime used for measurements and for detecting +condition changes during the measurement interval; endpoint probes cannot prove +that conditions remained stable at every instant. + +The retained output places a `TORCH_CHECK_CONDITIONS` JSON diagnostic before +measurement output and a postflight line after it. Before/after reports, reasons, +policy and subject identity are retained in the receipt and artifact. Evidence +is explicitly `project-reported`, not independently verified sensor truth. +Exact-pass qualification requires valid before/after condition evidence and +matching policy, not merely exit zero. Legacy checks remain supported without +this optional policy; this is not a claim that they observe test conditions. + ## 19. Shared-resource coordination + Resources such as browsers, GPUs, ports, databases, devices, API quotas, and deployment accounts are declared with capacity and policy. @@ -1341,6 +1810,36 @@ Before landing, TORCH MUST verify: Fleet policy MUST be enforceable without provider branch protection. A forge adapter MAY mirror the same policy as defense in depth. +The canonical branch is single-writer. Specialist identities MUST NOT push +their branches directly to the canonical branch or its remote; all landings go +through TORCH's integration authority. The final target re-check and +fast-forward MUST be serialized across TORCH processes with a project-scoped +lock/transaction. Authorized requests MUST be drainable in durable insertion +order by one landing-authority invocation; direct specialist pushes remain +prohibited. The FIFO drain MUST re-evaluate each request against the latest +target after every landing. A candidate that omits a newly landed canonical +commit MUST move to `needs_convergence` and remain unlanded until its owner +converges and reruns exact-commit checks. The drain may continue to other +independently ready requests; it MUST NOT weaken checks or reassign the stale +candidate. A direct `integrate land` request MUST also respect the same FIFO +position and cannot skip an earlier ready request. `torch integrate drain` is +an explicit main-mutating operation and requires confirmation. This one-shot +processor is not itself a daemon or timer. A project MAY define a typed +`integration-drain` system schedule, but it MUST be periodic, owner-authorized, +use a configured landing-authority identity, and run no more often than once +per minute. It processes only requests already authorized by that identity and +whose exact required checks still pass. The schedule is mutating: it does +nothing until the owner separately installs the exact-config system timer, and +removing the timer does not revoke per-request audit history. An absent or +uninstalled schedule leaves integration manual. The scheduled drain MUST NOT +perform convergence, push branches/remotes, weaken check requirements, or +invoke an AI provider. A concurrent contender must fail closed or wait, then +recompute its plan against +the new target and rerun exact-commit checks after convergence. A lost race +never justifies weakening checks. TORCH cannot prevent a user or agent with +independent forge credentials from bypassing its local queue; hosted branch +protection or credential isolation is defense in depth when available. + ## 21. Doctor and observability `torch doctor` is an observer and diagnostician, not an automatic repair tool. @@ -1433,7 +1932,185 @@ definitions in TORCH, suppresses duplicate minute runs, and refuses to run after tracked configuration changes because its command is bound to the exact configuration digest. Unit files are recorded in the installation ownership manifest and removal refuses changed files. Installing that persistent -launcher on a real machine remains an explicit owner action. +launcher on a real machine remains an explicit owner action. When a reviewed +configuration change changes its digest, `torch schedules launcher +plan-reconcile` previews the exact owned-unit refresh and `reconcile --yes` +updates only unchanged TORCH-owned units, reloads the user systemd manager, and +updates the ownership manifest. Modified, missing, or mismatched units block +reconciliation; a failed reload restores the previous unit contents and +manifest. This refresh does not alter the timer cadence or start an AI runtime. + +Manager check-ins are a required schedule capability. Each manager-level +identity MUST have a configurable cadence and a scope derived from its direct +reports in the active organization graph (not a hard-coded list of agent +names). Interval-based manager check-ins MUST be at least 60 seconds apart; +cron schedules retain their minute-level resolution. A check-in observes +durable status, current task, unacknowledged +coordination/blocker/handoff messages, and explicit approval-wait records when +available; ambiguous `waiting` summaries are surfaced as unclassified rather +than guessed. It prepares a concise, durable check-in request for that manager +with affected identities, evidence, and the next useful action. Check-ins MUST +not grant approval, reassign work, alter a backlog item, or create a new agent. +If the manager is offline, the check-in remains queued for resume. Waking a +runtime to consume the check-in is an adapter-mediated provider invocation. +It MUST be disabled by default and, if enabled by the owner in the reviewed +schedule, MUST select an explicit budget policy and a durable project-wide +maximum invocation count shared across all manager schedules per UTC day. +`action.wake.budget_mode: invocation-count` supports subscription and local +runtimes and bounds the number of invocations, not dollar spending. +`action.wake.budget_mode: usd-hard-cap` additionally requires a positive +`action.wake.max_usd_per_invocation` and an adapter-enforced USD ceiling for the +exact create/resume mode. Legacy configurations providing the positive USD +ceiling without a mode retain USD-hard-cap behavior. An enabled wake without +either policy is invalid. Count mode cannot specify a USD ceiling. +TORCH MUST skip a wake when the manager is already active, +preflight the exact configured identity and any required ceiling, reserve the daily count +atomically only after that preflight, and invoke using the same prepared plan, +adapter, and profile. A failed or unavailable adapter MUST leave the durable +check-in queued and report the failed wake. A missing or mismatched hard-cap +receipt in USD-hard-cap mode MUST fail closed before reservation/invocation. The daily invocation +count and per-invocation USD ceiling are independent safeguards; neither +replaces the other. The portable base behavior is durable delivery, not silent +provider use. The owner configures the shared cap through +`runtime_wake_budget.max_invocations_per_day`. Timer installation remains a +separate owner-approved operation. + +Adapters MUST NOT claim a hard cost ceiling for subscription or account-based +usage unless the underlying runtime provides an enforceable limit. The current +built-in Claude, Codex, and Pi adapters declare no hard-cap-capable launch +modes; their USD-hard-cap scheduled wakes are rejected during preflight and +check-ins remain queued. Explicit count-limited wakes can use their ordinary +configured launch/resume modes. Pi still requires an explicit provider/model. + +When adapter preflight was unavailable, a later timer tick MUST be able to wake +the existing pending check-in without creating a duplicate message. Each +message has at most one reserved invocation; retries must not silently repeat +a possibly executed provider call. Reservation MUST atomically reject another +in-flight wake for the same manager, and recheck current manager state before +invoking. Unknown or interrupted reserved invocations remain visible for +operational recovery rather than being cleared based on age alone. + +`torch schedules wakes [--manager ]` provides read-only reservation +inspection, including outcome, message, manager state and whether a reservation +blocks another wake. Recovery uses `torch schedules recover-wake --reservation + --runtime-stopped --note --yes`. Only the configured owner may +recover an interrupted `reserved` record, and the manager must be offline. +The owner MUST first inspect and stop any associated runtime outside this +command; an offline heartbeat alone is not process proof. TORCH records this +as an operator attestation, not verified runtime termination. Recovery is +atomic and audited, retains the budget charge and same-message replay guard, +and neither acknowledges the check-in nor launches a provider. A fresh check-in +can wake only through the ordinary schedule after the owner handles the old +message. Age, message acknowledgement, and routine timer ticks never recover +unknown launches automatically. + +The read-only project snapshot MUST expose initialized manager launch records +and the total number of blocking unknown launches, or explicitly report that +records are unavailable. A bounded history prioritizes blocking reservations. +The Console surfaces them in owner attention and schedule operations with +manager, schedule, message, reservation and presence details. Navigation to +these details opens any collapsed ancestor panel. Observation MUST NOT clear, +acknowledge, recover or retry a reservation, and offline presence MUST NOT be +presented as independent runtime termination evidence. + +When TORCH proposes an organization graph, it MUST derive a check-in schedule +for every non-owner identity with direct reports. Existing schedules and their +configured cadence are preserved; a newly required manager receives a +15-minute owner-authorized system coordination schedule in the proposal. A +hierarchy-change plan MUST show schedule additions and obsolete schedule +candidates. Obsolete timer definitions are not removed automatically, and a +proposed schedule is not an installed timer: activation must preserve the +separate owner approval and installation boundary. One project timer may +dispatch multiple manager schedules; each schedule queues work only for its +own manager and direct-report set. +The owner-facing hierarchy review preview MUST show that same schedule delta, +label timer installation as unverified, and state that changes to the tracked +configuration require separate reconciliation of any exact-config timer +launcher before it is trusted. The owner can inspect and explicitly apply that +refresh through the launcher reconciliation commands; hierarchy planning and +activation MUST NOT silently change user systemd state. Showing a schedule MUST NOT imply that a +manager runtime will be awakened; provider invocation requires its own +explicitly enabled budget and policy. + +Every generated start/resume instruction bundle for an identity with direct +reports MUST explain its manager-check-in responsibility: inspect durable +check-in messages, refresh current state through the identity-bound +`torch_plan_manager_check_in` tool, act only within the manager's authority, +and route approval requests to their named approver. Specialist-only bundles +MUST NOT imply manager authority. Rebuilding the bundle from the current +organization graph ensures a promoted identity receives these instructions on +its next start or resume, without silently changing a running session. + +Approval waits MUST be represented as durable structured requests rather than +inferred from presence summaries or free-text messages. A request names its +requester, exactly one approver (a configured owner or active Fleet identity), +optional task and evidence references, a bounded title and summary, state, +revision, and decision metadata. Only the named approver may decide it. A +decision is revision-checked, audited, and durably returned to the requester. +An owner decision requires the explicit owner CLI confirmation path; AI +identities cannot claim or decide as the owner. A manager check-in includes +pending requests made by direct reports and classifies the wait target as that +manager, another AI identity, or the owner. Managers may route or escalate a +request but MUST NOT approve it for a different named approver. The owner and +each AI identity may list only requests they made or are named to approve; +manager check-in may inspect requests made by its direct reports. + +Implementation checkpoint (2026-09-28): `planManagerCheckIn` derives direct +reports from the active organization graph and reports offline/stale/waiting +identities plus unacknowledged direct-report messages. `queueManagerCheckIn` +creates at most one unacknowledged self-directed manager-check-in message, does +not queue when no attention is needed, and never invokes a runtime. Formal +approval requests now persist in SQLite, are identity-scoped, notify named AI +approvers, and allow only the named approver to record an audited, +revision-checked decision. Owner decisions require explicit CLI confirmation. +Check-in plans include direct-report approval waits classified by target. +Queued check-ins include a capture timestamp and explicitly identify their +findings as a potentially stale snapshot. Before taking action, the manager +MUST refresh direct-report presence, unacknowledged messages, and structured +approval waits from current durable state through the read-only, +identity-bound `torch_plan_manager_check_in` MCP tool. The tool derives scope +from the active organization graph, accepts no caller-selected manager ID, and +grants no decision authority; named approvers remain the only identities that +may decide their requests. Pending-message deduplication therefore cannot make +a later approval or blocker invisible to the manager's fresh check-in. +A manager-check-in action MAY opt into `stale_work` review bounds: +`stale_days` (1–365), `max_commits` (1–10000) and `max_items` (1–100), defaulting +to approved backlog activity bounds and 30 items. An owner-approved daily cron +schedule can deliver this review; cron evaluates host-local time. No such review +schedule or host timer is installed automatically. + +Reviews MUST use the authoritative existing backlog, prioritize owner requests, +scope assigned tasks to direct reports, and reserve unassigned intake for Fleet +Operations. Blocked items MUST remain marked as expected waiting; incomplete +Git coverage is unknown, not evidence of neglected work. Output MUST be bounded +and MUST NOT modify task state, ownership, approvals or completion evidence. +Stale reviews deduplicate separately from routine check-ins so an outstanding +routine message cannot swallow the daily review. Before acting, managers refresh +`torch_plan_manager_check_in` with `stale_work_review: true` and the stated bounds. +Revival, deferral, dependency resolution and evidenced closure remain explicit +decisions through the existing task/approval mechanisms. Optional runtime wakes +retain existing owner approval and budget guards; inbox delivery alone never +starts an AI session. +New deterministic proposals derive 15-minute schedules for each manager +identity with direct reports. Hierarchy pilot plans now show missing manager +schedule additions and obsolete schedule candidates without applying either. +Configuration validation requires every configured manager to have its own +typed schedule. The typed coordination action is handled by `ScheduleService` +and dispatched by the existing owner-approved system timer. The timer only +queues a durable check-in; it does not wake a live AI runtime. Applying a +hierarchy change and reconciling installed timer state remain separate gates. +The local Console includes Flow watch for managers, direct reports, approval +targets, and configured cadence; it explicitly reports timer installation as +unverified. Snapshot observation remains read-only. For a pending request whose +named approver is the owner, the Console MAY offer a same-origin loopback +preview-and-confirm decision control. Preview MUST show the request revision, +requester, evidence, selected decision, note, and exact effect. Confirmation +MUST re-check the named approver and request revision, use the existing audited +`decideApproval` transaction, and notify the requester exactly once; stale or +replayed requests MUST NOT apply a second decision. The owner Console MUST NOT +decide requests assigned to a manager or peer AI, and MUST make that boundary +visible. Explicit adapter-mediated runtime wake with a per-project budget +remains separate and disabled by default. ## 23. Repository and forge operating modes @@ -1477,6 +2154,21 @@ Valid configurations include GitHub for mirroring while TORCH remains the integration and check provider, or GitHub Actions for checks while TORCH keeps its own backlog and integration queue. +Publishing the already-landed local canonical branch is a separate owner-only +operation; it is not part of specialist integration or the scheduled +integration drain. `torch forge sync plan` MUST be read-only, and +`torch forge sync --yes` MUST require explicit confirmation, verify that the +configured forge remote is still the canonical remote, and publish only the +current canonical commit. The operation MUST use a normal fast-forward-only +Git push, MUST NOT force-push, and MUST record the owner, exact commit, remote, +branch, and observed result in the local audit log. A remote-ahead or divergent +branch MUST block publication; TORCH MUST NOT fetch-and-merge, overwrite, or +silently select one history. The owner must first fetch and explicitly +converge the canonical branch, then rerun any required checks invalidated by +that convergence. A push race MUST fail without rewriting the remote. Forge +unavailability MUST leave local integration and coordination operational; +publication can be retried after service returns. + ### 23.3 Degraded operation Loss of a forge MUST NOT destroy local identities, messages, commits, checks, @@ -1523,8 +2215,152 @@ default. The CLI and read-only Fleet observation surface are implemented and scenario-tested with a virtual adapter; no live release or deployment was performed. +### 24.1 Durable delivery attempts and bounded retries + +Before external execution, TORCH persists a per-delivery unresolved operation +reservation and a running attempt. Independent callers MUST NOT overlap that +delivery's effects. Each attempt records operation, exact commit, provider, +ordinal, timestamps, result and structured receipt. Failed attempts persist +without advancing delivery state. Successful receipt application, delivery +state, lifecycle event and audit are committed together; if local application +fails after external success, the unresolved successful operation prevents +duplicate execution. Console observation includes bounded recent operations +and attempts; detailed receipts are available through CLI. + +Approved `delivery.retry.max_attempts` is 1–5, default 1. Retry requires both an +adapter declaration of `retrySafety[operation]` (`idempotent` or `read-only`) +and a structured failed receipt reporting transient classification and effects +`not-applied`. All bounded attempts share one idempotency key. Adapters must +actually honor their safety declaration/key and omit secrets from receipts; +TORCH does not independently prove provider-side behavior. Permanent failures, +undeclared safety, unknown effects, thrown exceptions and malformed receipts +MUST NOT auto-retry. Fresh canonical policy is checked before execution and +every retry; changed policy stops further effects. Existing authority/evidence +and high-impact owner-approval gates still apply. This boundary is synchronous; +Promise-returning adapters are unsupported and treated as uncertain. + +Unresolved interrupted/unknown execution remains blocked for review. Only the +owner, after confirming the executor is stopped and independently verifying no +external effect, may explicitly attest `not-applied` with evidence. This clears +the reservation without executing anything or advancing lifecycle state, and +preserves the original uncertain attempt plus owner-attested provenance. +Do not use this for succeeded operations. Saved successful receipts whose +local lifecycle application failed have a separate owner-only preview and +explicit executor-stopped reconciliation path. It MUST invoke no adapter, +require the exact delivery commit/from-state/next-state and latest successful +receipt, check current owner authority and unchanged destination configuration, +and commit lifecycle/event/audit/operation application atomically once. A running +parent with a durably saved successful latest attempt can be reconciled after +the owner confirms its executor stopped; unknown/failed receipts cannot. +Replays are unchanged and partial local recovery rolls back without losing the +receipt. Destination hashes are additive metadata; older records without them +require the complete original policy hash. Unrelated configuration changes do +not invalidate newer destination-bound records. Saved adapter success and owner +review are reported provenance, not independent live deployment verification. +Actual provider adapters/idempotency qualification, canonical fetch retry and +dashboard details remain open. +No configured provider name alone installs or executes an adapter. + ## 25. Security and trust boundaries +### Owner-first digest checkpoint (2026-09-30) + +TORCH provides read-only Markdown/JSON owner digest generation over a configurable +rolling UTC window (1–168 hours, default 24) with bounded section results +(1–100 items, default 30). Owner approval waits come first, followed by unresolved +delivery review, latest deployment operation, receipt-reported shipping, landed +work, completed tasks, structured approval decisions, blockers and neglected +owner requests. Implemented or landed work MUST NOT be relabelled as shipped. +Missing tables and truncated source/results MUST be explicit; incomplete managed +Git history cannot prove neglected requests. Project text is escaped for Markdown. +Database reads share a transaction; tracked worktree/Git state remains a bounded +observation, not an atomic repository snapshot or independent live verification. + +Approved publication persists an immutable local report plus audit atomically. +Owner publication requires explicit approval; Fleet Operations automatic +publication requires current `owner_digest.enabled` policy. A typed +`owner-digest` system coordination schedule requires owner authority and one +attempt, honors policy revocation and wakes no agent or external command. +No timer is installed by configuration or report publication. Latest reports +are available through CLI, console snapshot and read-only `/api/digest` JSON. +Unstructured decision documents are not time-indexed and cannot be presented +as complete recent management calls. The actual Console and isolated demo share +a read-only owner briefing renderer and navigation anchor. It shows saved +publication time/window, owner attention first, delivery uncertainty, distinct +recorded shipping/landing/completion, expandable decisions/blockers and coverage. +Unpublished, stale, future-clock and unavailable/truncated evidence are explicit. +Project strings are escaped as text; report Markdown is never interpreted as +trusted HTML. A saved report is not silently rewritten after a live approval +changes. Mobile anchors clear sticky navigation, and expanded evidence fits the +viewport. Explicit external notification adapters and installed-cadence +qualification remain pending. Local publication MUST NOT imply public release or external +delivery. The authoritative backlog remains unchanged by reporting. + +Check-evidence dashboard checkpoint (2026-09-30): the Console and isolated +demo share an escaped, expandable view of exact-commit receipts, captured-input +digests/copy strategy, invalidation reasons and project-reported pre/post +condition observations. Project-scoped read-only database projection supports +legacy receipts without condition columns; malformed or absent evidence is +explicit. Preparation records remain visible without assuming a recorded +running state proves a live process. No automatic retry or recovery authority +is introduced. History is bounded to the latest 40 receipts and preparations; +linked captured-input evidence is resolved independently of the preparation +display limit. Local filesystem paths and per-file inventories are not exposed +by this view. Actual GPU/browser qualification remains a separate gate. + +Activity dashboard checkpoint (2026-09-30): read-only snapshots reuse managed +Git commit observation with approved project stale-day/scan bounds. Task creation +time and originating owner identity are preserved in the projection. The shared +Console/demo Activity review groups stale work separately from unknown activity, +puts owner requests first and retains expected blocked-wait context. Neglected +owner requests surface in the attention list. Commit closure mentions are intent, +not completion evidence; metadata edits do not reset inactivity. Observation +neither creates a second queue nor changes task state. A configured daily review +and non-disruptive live refresh still need implementation/qualification. + +Canonical fetch checkpoint (2026-09-30): `torch forge fetch plan` resolves the +configured canonical branch, then owner-confirmed `forge fetch --yes` imports +that exact commit's objects without moving branches, updating FETCH_HEAD, +recursing into submodules or publishing remotely. Default attempt limit is three, +explicitly bounded to one through five. Only narrowly recognized transient +transport failures are retried; authentication, configuration, certificate and +unclassified failures stop. Head discovery itself fails closed without retry. +Each attempt is reserved durably before execution; sanitized results survive +reopening and are available through `forge fetch status` and local snapshots. +Destination URL hashes and canonical policy are checked before each attempt; +execution uses the resolved URL, not a subsequently mutable remote alias. +Terminal state and owner audit persist atomically. Raw errors/URLs are not +stored. A running record after interruption is unconfirmed executor state, not +proof of a live fetch. Object presence plus Git exit status is local import +evidence, never shipping evidence or release/deploy permission. Real remote +classification and dashboard outcome presentation remain qualification work. + +Operation-outcome dashboard follow-up (2026-09-30): the actual Console and +isolated demo now expose the latest 20 fetch operations, 40 delivery operations +and 80 delivery attempts with explicit coverage bounds. Read-only project-scoped +projection retains safe receipt status/classification/effects and recognized +TORCH error categories, not arbitrary references, raw errors or credential-bearing +metadata. Malformed receipts remain unavailable. Unknown/running/pending-success +operations surface in owner attention; views never authorize retries or recovery. +Object import success is distinct from deployment; adapter success is distinct +from independently verified live release state. Real adapters remain unqualified. + +Live dashboard refresh checkpoint (2026-09-30): snapshots refresh every 15 +seconds while visible, with an owner pause/resume control and a ten-second read +deadline. Hidden tabs do not poll; pagehide tears down the timer and persisted +pageshow restores it without duplicating intervals. Requests are single-flight; +background ticks coalesce and cannot starve slow reads, while explicit newer +requests discard older responses. Failed reads leave existing evidence visible. +Snapshot rendering protects edited or focused controls and active preview DOM, +updates unrelated evidence and preserves matching expanded detail sections. +The refresh status explicitly warns that retained panels may show older state. +Successful actions release only their own editor; unrelated drafts remain. +Manual refresh uses the same protection. No preview is silently reissued, +confirmed or persisted; existing server-side plan/revision/token checks remain +authoritative. Polling neither wakes agents nor installs host timers. Controlled +clock/browser lifecycle regression is local evidence; installed-fleet and native +background/back-cache qualification remain separate work. + TORCH MUST: - keep secrets outside repositories; @@ -1568,7 +2404,9 @@ the live development fleet. For a Git source checkout, candidate staging MUST bind the artifact to the exact clean tracked commit. It may include the installed dependency tree needed to execute acceptance, but MUST exclude arbitrary untracked files and reject -modified tracked input. Artifact-local relative dependency links are permitted; +modified tracked input. Candidate plans MUST list non-ignored untracked paths +that will be excluded, so review makes the exact candidate boundary visible; +ignored local files remain excluded without being listed. Artifact-local relative dependency links are permitted; links that escape the candidate root are refused. Non-Git inputs are treated as already-prepared release artifacts and every contained entry is inventoried. @@ -1586,6 +2424,11 @@ Candidate acceptance includes: - capture, stop, and resume; - detach and uninstall cleanly. +If a candidate check fails, its acceptance report MUST include bounded stdout +and stderr excerpts for failed checks while omitting log copies from successful +checks. A nonzero check without its actionable diagnostics is not sufficient +candidate evidence. + ### 26.3 Atomic upgrade and rollback Versions are installed side by side: @@ -1659,6 +2502,121 @@ The operational console SHOULD expose: The console is an operational view over the same core service, not a separate source of truth. +The owner-facing console SHOULD open with a project pulse and an explicit +attention queue, then provide project-aware views over the authoritative +backlog, persistent identities, durable inter-session communications, evidence, +integration, and release gates. Kanban lanes are projections of backlog states; +they MUST NOT create another queue. The roster and enabled capabilities drive +navigation and grouping; the interface MUST NOT assume COMBATRIG departments or +another project's role names. + +The organization view SHOULD also show the exact manager-aware startup order +and reverse wind-down order derived from the approved reporting graph. It MUST +use the same ordering policy as runtime lifecycle planning, expose missing +identity or reporting-cycle blockers, and remain a read-only preview; it MUST +NOT start or stop sessions or infer live presence from the planned order. + +The console SHOULD let an owner filter the backlog by project-observed task, +owner, state, priority, domain, feature, and milestone values, then save, update, +select, or delete named views. Saved views are presentation preferences stored in browser-local +storage under a stable project identity; they MUST NOT write to the repository, +control plane, or task records. Unavailable browser storage MUST leave the +backlog usable and explain that views could not be saved. Saved view data MUST +be schema-validated and bounded, and malformed preferences MUST fail closed to +the unfiltered authoritative backlog. + +The Console SHOULD summarize each explicitly labeled feature and milestone +from its linked backlog task states, including completion, active work, review, +and blockers. Canceled tasks MUST be visible but excluded from completion +denominators. The summary MUST disclose that it is a task-state projection; +checks, integration, release, deployment, and live verification remain separate +gates and MUST NOT be inferred from a percentage or completed task count. Work +without labels remains ungrouped; the Console MUST NOT infer or invent feature +names from file paths or domain titles. + +The local Console MAY let the project owner change a backlog task's priority. +Such an action MUST be restricted to same-origin loopback requests, require an +explicit reason, bind a short-lived single-use preview to the exact task, +revision, old/new priority, and reason, and require a separate confirmation. +Applying it MUST re-check the task revision and record an audit/history event. +It MUST NOT change task state, owner, dependencies, evidence, or integration +gates. Stale, expired, replayed, cross-origin, and malformed requests MUST +fail without mutation. This narrow control does not grant general backlog, +identity, lifecycle, or provider-launch access. + +Screenshots and review artifacts MUST carry task, identity/session, and commit +provenance. Owner comments and annotations MUST route through the responsible +identity or become durable backlog work; the UI MUST NOT expose private model +chain-of-thought. Test receipts, integration, release readiness, deployment, +and live verification are separate evidence states. The console MUST label +stale or unavailable data rather than presenting it as current. + +The initial local artifact catalog stores bounded PNG, JPEG, WebP, or GIF image +bytes in private external project state, not in the repository. A publisher +MUST own the referenced backlog task, use a managed identity worktree, provide +the full commit SHA and stable identity, and have the active runtime session ID +captured when available. TORCH MUST reject path traversal and symbolic links, +record a content digest, and verify that digest before serving an image. Owner +feedback MUST preview the artifact, task, responsible identity, commit, and +durable-message effect; only the configured owner may confirm it. Feedback is +stored in the durable message stream rather than a parallel comment ledger. + +The observation endpoint remains GET-only. The local Console may expose a +narrow owner feedback action: a same-origin loopback preview issues a +single-use token bound to the exact message effect, and confirmation must +recompute and match that preview within five minutes. Cross-origin requests, +stale previews, expired tokens, and replayed confirmations MUST fail without +creating a message. This is not general write access to the Fleet. + +Other owner controls MUST preview the target, effects, evidence, and authority +required before invoking a separately authorized control-plane operation. A +dashboard button MUST NOT itself bypass authorization for runtime launch, task +reassignment, worktree removal, canonical integration, release, deployment, +or spending. Preserve the read-only observation API and test UI preview and +API authorization separately. + +The owner Console MAY accept or reject Fleet-evolution proposals and may +approve a hierarchy proposal for bounded-pilot planning, defer it, or reject +it. These actions MUST update the existing durable proposal records rather +than create a dashboard-only queue. Each action MUST use a same-origin loopback +preview and a short-lived, single-use confirmation bound to the exact proposal, +decision, reason, active organization graph, and repository head. Confirmation +MUST recompute that scope and fail closed if it changed. Approving a Fleet +change authorizes only a later activation review; it MUST NOT provision an +identity, create or remove worktrees, or start a runtime. Approving a hierarchy +proposal authorizes planning only; it MUST NOT activate the proposed graph or +start/create identities. A distinct owner-confirmed CLI activation requires a +fresh plan and commits only the approved graph, derived check-in schedules, +install-manifest hashes, and pilot record; it still MUST NOT provision or launch +identities or modify user systemd state. Timer reconciliation is a separate +owner-confirmed command. Decisions MUST retain their durable audit trail. + +The owner Console MAY also expose persistent schedule-timer setup and refresh. +It MUST distinguish configured schedules from TORCH-owned unit files and MUST +state that file ownership/digest does not verify the timer's live systemd +state. Before installation or reconciliation it MUST show every affected +system schedule, its trigger, behavior, action, wake policy, unit paths, and +configuration digest. Only a same-origin loopback preview followed by a +single-use, five-minute confirmation bound to the exact action and current +configuration may write units, reload systemd, or enable/start the timer. +Confirmation MUST recheck exact unit ownership and refuse stale, modified, +missing, or mismatched units. The operation MUST record owner audit evidence. +Starting the timer may dispatch configured due system schedules; it MUST NOT +silently enable provider wake, weaken action authority, or start a Fleet +identity directly. + +The owner Console MAY also let the owner send a bounded request to one +identity in the approved Fleet roster, optionally linked to an existing active +backlog item. It MUST preview the exact recipient, message, task reference, +owner authority, active organization, and repository head before confirmation. +Confirmation MUST recompute that scope and use a short-lived single-use token; +stale, expired, replayed, cross-origin, and malformed requests MUST fail +without writing. A confirmed request is one ordinary durable inbox message in +the existing conversation stream, with owner identity and audit provenance. +It MUST NOT create or change a task, transfer ownership, change presence, wake +or start a runtime, or expand the recipient's authority. The request remains +subject to the agent's scope and all existing approval gates. + ### 27.3 Public website The public website MUST be redesigned around the new product. It MUST NOT @@ -1681,6 +2639,16 @@ The site SHOULD separate product explanation and documentation from the local Fleet Console. A distinctive routing or dispatch-board visualization should demonstrate the system more clearly than a wall of feature cards. +The dashboard demo MUST open the actual Fleet Console in a separate browser +tab, using the same markup, renderer, and controls with sample project data. +Its link label is `View dashboard demo`; browser-tab behavior does not belong +in the visible label or page title. Sample actions MUST stay in the visitor's +tab and MUST NOT contact live project APIs or start agents, timers, or provider +invocations. The demo MUST label its sample workspace, support reset, and +demonstrate project work, agents, direct communication, review artifacts, +owner decisions, and runtime profiles. A separate miniature dashboard is not +an acceptable substitute for the product interface. + Recommended product language: > One repository. A coordinated AI engineering team. @@ -1742,10 +2710,18 @@ unresolved path ownership; a separate repository fixture continues to prove code-derived architecture and collision boundaries. Both paths remain read-only until an approved install. +Repository analysis also includes unignored working-tree source files and binds +proposals to a recomputed working-tree fingerprint. Drift in tracked or +untracked analysis inputs invalidates approval even when `HEAD` has not moved. + `torch bootstrap` now emits the provider-independent Session Architect brief. Its scenario proves that a mixed code/spec project exposes omitted components and unmatched goals for AI reasoning, forbids copying COMBATRIG or a generic -catalog, and preserves owner-only approval without mutating the project. +catalog, and preserves owner-only approval without mutating the project. The +brief also requires an evidence-backed flat-versus-coordination assessment; +proposal validation requires integrated outcomes and authority boundaries for +any lead, and rejects implementation or owner-approval grants while preserving +direct peer communication (`SCN-architect-organization-assessment`). `torch architect plan`, `validate`, and explicitly authorized `run` now carry that brief through isolated Claude or Codex planning and semantic validation. @@ -1803,6 +2779,15 @@ the automated suite intentionally incurs no provider usage. - atomic upgrade and rollback; - TORCH's own approved roster and worktrees. +Candidate acceptance MUST run with a disposable HOME, TMPDIR, and XDG data, +config, cache, state, and runtime directories that are distinct from the active +installation/version store. The candidate gates MUST use this isolated +environment for test, lint, and syntax subprocesses, then remove it after they +finish. They MUST NOT inherit a user or candidate-install XDG data directory as +test state; doing so can make tests share or mutate the installation under +qualification. The acceptance receipt MUST include regression coverage for +this isolation boundary. + Exit gate: stable TORCH manages development, testing, and promotion of its successor in local-only mode. @@ -1835,6 +2820,16 @@ pending: 56 blocking ownership/exclusion gaps affect all 33 areas. The exit gate remains open until those owner-reviewed boundaries are resolved and COMBATRIG is safely installed and operated through portable TORCH. +The importer also provides an assessment-only backlog migration view. It MUST +report legacy-area counts and unmapped area IDs, preserve the distinction +between an area and a free-form `assignedTo` display name, and MUST NOT create +Torch tasks or infer a runtime identity. Legacy `done` is not sufficient to +mark a Torch item `completed`: the target state requires commit evidence and a +verifiable landed integration request. The view MUST identify manager-area +items and malformed or absent `observedAt` values without silently dropping +them. Backlog task creation and state transfer require a separate reviewed +migration; until then the original queue remains the source of truth. + ### Stage 8: Product surface - new public website and documentation; diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md new file mode 100644 index 0000000..087b3a4 --- /dev/null +++ b/docs/TEST_INTEGRITY.md @@ -0,0 +1,2171 @@ +# Test Integrity Notes + +## 2026-09-30 — Specialist-only project bootstrap + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-bootstrap-specialist-only + given: Disposable test-only, release-only, combined and empty repositories + when: Repository analysis generates an initial pending roster + then: Evidenced QA/release roles have no conflicting catch-all owner and only empty evidence receives Core fallback + observable_outcomes: + - Exact generated domain IDs and successful approved-proposal validation + - Git working tree remains unchanged after analysis and validation + determinism_controls: + - Real disposable committed Git histories and local fixtures + - No provider calls, timers, external network, retries or sleeps + anti_cheat_rationale: + prevents: + - Proposing a default owner that overlaps every subsequently added specialist + - Suppressing real QA or release roles to avoid ownership rejection + - Removing the fallback needed for an empty initial project + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +The new scenario first reproduced Core plus QA on a test-only repository. +Production now adds the fallback after evidenced horizontal specialists, not +before them. No previous assertion or collision guard was weakened. + +## 2026-09-30 — Bootstrap primary ownership collisions + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-bootstrap-primary-ownership + given: Two independently evidenced implementation domains in a disposable Git repository + when: Pending or approved proposals duplicate primary paths or claim a recursive parent of another owner + then: Validation rejects collisions while shared consultation and disjoint primary ownership remain valid + observable_outcomes: + - Pending validator problems and approved-install rejection with PROPOSAL_NOT_APPROVED + - Recursive planned ownership conflicts rejected even before files exist + - Backslash-normalized duplicate ownership rejected + - Repository status remains unchanged + determinism_controls: + - Disposable committed Git repository with two source files per domain + - No provider calls, external network, timers, retries or sleeps + anti_cheat_rationale: + prevents: + - Accepting two implementation owners because their prose claims different outcomes + - Treating a collision coordination note as authorization for duplicate ownership + - Rejecting legitimate shared consultative access + - Checking only existing files and ignoring planned recursive collisions + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +The first new fixture used one file per component; baseline design deliberately +does not create a specialist for every one-file directory. The fixture now has +two real files per domain, matching its stated precondition. No existing tests +or expected ownership behavior changed. The check proves exact and literal +recursive-parent conflicts, not arbitrary glob-language intersection. + +## 2026-09-30 — Real Chromium surviving runner interruption + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-browser-descendant-interruption + given: Real Chromium serves captured inputs through an actual prepared measurement subprocess + when: Only the disposable TORCH runner dies, then the owned harness is explicitly stopped + then: Browser initially remains usable, recovery without stopped-executor confirmation is refused, captured process-group terminal events precede abandonment + observable_outcomes: + - Real browser DOM, captured commit and visibility after runner exit + - Pre-opened kernel handles for multiple Chromium group members and the owned harness + - Terminal group check, no receipts, unchanged resource lease and original build retention + determinism_controls: + - Isolated test-owned Linux browser group with PID birth identity validation + - HTTP readiness and pidfd events, no sleep or retry loops + - Handles opened before shutdown avoid PID disappearance or reuse races + - Qualification requires explicit browser and interruption flags + anti_cheat_rationale: + prevents: + - Equating runner termination with browser termination + - Treating a stopped-runner preview as automatic all-executor proof + - Waiting on or signalling unrelated browser processes + - Generating a pass or releasing leases through abandonment + - Proving only a fake browser or a single process with no descendants + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Dashboard abandoned-check cleanup evidence + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-console-abandoned-check-cleanup + given: Project-scoped pending, completed and malformed legacy recovery metadata + when: Read-only snapshot and shared dashboard renderer observe it + then: Cleanup states remain distinct, abandoned checks remain visible and private evidence is omitted + - id: SCN-hard-killed-prepared-check + given: Owned interrupted check with failed then replayed cleanup + when: CheckService reopens after each outcome + then: Pending and completed cleanup outcomes are durable and replay preserves one authority audit + observable_outcomes: + - Stored recovery metadata and unchanged read-only projection database + - Escaped dashboard HTML, absent process IDs and private owner evidence + - Real desktop/mobile Chromium demo with no live API requests or recovery buttons + determinism_controls: + - In-memory project-isolation/legacy database scenarios + - Actual disposable interruption gated explicitly on Linux + - Browser lifecycle controls and no sleep/retry masking + anti_cheat_rationale: + prevents: + - Hiding abandoned work because it no longer has running state + - Claiming cleanup complete from missing metadata + - Leaking private process or input evidence through observations + - Informational dashboard buttons performing automatic recovery + - Losing cleanup outcomes after reopening + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Hard-interrupted check recovery and orphan receipts + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-hard-killed-prepared-check + given: A real measurement subprocess blocked inside a disposable prepared check + when: Test-owned runner and executor are terminated and the owner explicitly recovers + then: Snapshot survives interruption, live recovery is rejected, abandonment is audited once and no receipt or lease release occurs + - id: SCN-frozen-resource-check + given: A passing frozen receipt whose prepared-operation linkage is interrupted + when: Exact-pass qualification evaluates it + then: Orphaned passing output cannot qualify until terminal linkage is restored + observable_outcomes: + - Runner and real measurement PID handshake, runner exit and pidfd terminal notification + - Durable abandoned state, original recovery audit, exact-pass rejection and unchanged lease + - Owner CLI approval refusal, idempotent replay and owned cleanup preservation + determinism_controls: + - Disposable projects with actual subprocesses, no live agents + - Event-driven handshake and kernel pidfd wait, no timed sleeps or retries + - Linux interruption qualification explicitly enabled; ordinary skips are not proof + anti_cheat_rationale: + prevents: + - Treating a running database row or stale heartbeat as proof of process death + - Inventing a pass from interrupted work or orphaned output + - Deleting an unowned directory during cleanup + - Automatic resource stealing or executor replay + - Repeated recovery producing duplicate authority audit events + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +The hard-kill qualification uses Python's Linux pidfd API solely to terminate and +await the exact disposable executor. This is a qualification dependency, not a +TORCH runtime dependency. Normal portable kernel tests do not require it. + +## 2026-09-30 — Owner-first scheduled stale-work review + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-manager-stale-work-review + given: Owner and routine tasks, including blocked work, in a real managed Git fleet + when: Manager previews or queues a bounded activity review + then: Owner requests lead, task files remain unchanged, incomplete coverage is unknown and duplicate delivery is suppressed + - id: SCN-scheduled-stale-work-review + given: Approved daily local-time cron action with explicit stale-work bounds + when: Schedule plans the due minute and executes with or without approval + then: Only approved delivery queues one review, no runtime invokes and task state is preserved + observable_outcomes: + - Durable message body and kind, task file bytes and read-only MCP response + - Actual schedule receipts, exact due-minute behavior and no provider calls + - Output truncation, scoped task counts and blocked waiting context + determinism_controls: + - Disposable installed Git projects with real managed branches + - Fixed task creation time and explicit local calendar time for cron + - No host timers, network calls, retries or sleeps + anti_cheat_rationale: + prevents: + - Prioritizing routine tasks above owner requests + - Treating incomplete commit scans as proven inactivity + - Automatically reopening blocked tasks or closing old tasks + - Routine pending check-ins swallowing stale review delivery + - Invoking AI or shell commands through an informational review + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +The new cadence test initially used a UTC timestamp despite existing host-local +cron semantics, and read the check-in payload outside its existing `checkIn` +receipt envelope. Fixture time/output addressing were corrected; strict minute, +authority, deduplication and task-preservation assertions were retained. No +pre-existing expectations were changed. + +## 2026-09-30 — Same-runtime browser conditions and identity continuity + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-real-browser-conditions + given: One persistent Chromium page with a frame-driven simulation clock + when: Real subprocess probes surround healthy, frozen, drifting or replacement-runtime measurements + then: Only healthy work gets an exact pass, preflight-frozen work is never measured and green invalid results remain incomplete + - id: SCN-check-runtime-continuity + given: Preflight reports a runtime identity + when: Postflight loses or changes it, or preflight provides a malformed identity + then: Green output cannot pass and malformed preflight never measures + observable_outcomes: + - Browser-generated runtime identity in probes and measurement output + - Actual frame-driven clock movement, HTTP measurement counts and exact receipts + - Retained invalid reasons and snapshot cleanup + - Historical green flags do not bypass current report continuity validation + determinism_controls: + - Disposable projects and loopback-only persistent browser service + - IPC readiness and frame completion without retries or sleeps + - Explicit browser qualification opt-in; skipped scenarios are not proof + anti_cheat_rationale: + prevents: + - Probing an independent healthy browser instead of the measured runtime + - Claiming green command exit proves valid clock conditions + - Accepting a replacement page with identical build and healthy conditions + - Executing measurements after invalid preflight + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Real frozen browser and measurement subject identity + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-real-frozen-browser + given: Captured ignored build queued behind a peer browser lease + when: Specialist commits newer source and overwrites live output before promotion + then: Real Chromium sees captured HTML and SHA, newer SHA receives no pass, new-build negative control fails + - id: SCN-check-measurement-subject + given: Checks with and without condition policies + when: Actual child measurement commands inspect their subject environment + then: Both receive the exact commit and input digest supplied to probes + observable_outcomes: + - Browser DOM, retained stdout, exact-SHA receipt lookup and snapshot cleanup + - Actual child process output rather than a mocked executor + determinism_controls: + - Disposable Git repositories and loopback-only browser requests + - Explicit resource lease release rather than timed waits + - Browser frame completion awaited without sleeps or retries + - Browser qualification explicitly enabled and never counted when skipped + anti_cheat_rationale: + prevents: + - Passing by reading the live build instead of captured inputs + - Unsafe hardlink capture surviving only atomic rebuilds + - Crediting the newer source SHA with an old build pass + - Echoing fixed success instead of browser-observed output + - Supplying identity only to probes but not actual measurements + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Console owner-confirmed pilot conclusions + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-console-hierarchy-conclusion + given: An activated pilot and same-origin Console access + when: The owner previews and confirms reversal + then: Fresh exact plans commit once while missing tokens, altered decisions, other operations, cross-site requests and changed main are rejected + - id: SCN-console-hierarchy-adoption + given: A completed pilot with supporting reviewer-reported evidence + when: The owner separately confirms adoption through the Console API + then: Adoption succeeds without changing the active graph or starting a runtime + - id: SCN-hierarchy-pilot-review + given: A stored completed review but current time precedes pilot maturity + when: Adoption is planned + then: The current calendar gate blocks adoption + - id: SCN-dashboard-demo-browser + given: The actual Console and an isolated qualitative pilot sample + when: Adoption is previewed, then reversal is previewed and confirmed + then: Adoption has no confirmation, reversal shows the scoped plan and changes sample state only + observable_outcomes: + - Blocked previews receive no confirmation token + - Tokens bind target, operation, action and reason before replay handling + - Existing stale-preview error contracts remain unchanged + - Current-time maturity is rechecked at conclusion rather than inferred from stored flags + - Browser demo preserves agents and backlog and contacts no live API + - Candidate acceptance requires both adoption and reversal API evidence + determinism_controls: + - Disposable Git/SQLite fixtures and isolated loopback servers + - Injected hierarchy calendar clocks with no sleeps + - In-memory demo transport and Chromium + anti_cheat_rationale: + prevents: + - Recommendations or preview requests changing organization state + - Cross-operation replay bypassing an exact owner confirmation + - Future-dated stored evidence bypassing present calendar maturity + - A different mock dashboard substituting for actual controls + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Read-only pilot comparison dashboard + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-hierarchy-pilot-review + given: Persisted interim and completed pilot reviews in a disposable project + when: The Console snapshot reads hierarchy proposals + then: Latest review comparisons and reviewer-reported provenance appear without changing history + - id: SCN-dashboard-demo-browser + given: The actual Console with a sample qualitative inconclusive pilot + when: Its comparison history opens + then: Baseline, observed value, units and evidence quality render as separate columns and no recommendation-execution button is offered + observable_outcomes: + - Snapshot exposes latest bounded review records with stable identifiers + - Rendered recommendations explicitly do not change the organization + - Reviewer-reported evidence is not presented as independently verified + - Mobile page remains within its viewport and demo makes no live API requests + determinism_controls: + - Disposable Git/SQLite fixture and controlled review timestamps + - In-memory sample transport and loopback Chromium browser + anti_cheat_rationale: + prevents: + - Read-only dashboard observation modifying review records + - A qualitative observation rendered as measured or independently verified + - Source-text assertions substituting for rendered comparison behavior + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Owner-confirmed hierarchy conclusions + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-hierarchy-pilot-review + given: An activated pilot with a complete supporting review + when: An owner confirms adoption against a fresh plan hash + then: The pilot is adopted without changing the graph, and unauthorized, unconfirmed or stale confirmations fail + - id: SCN-hierarchy-pilot-reversal + given: An activated pilot with actual worktrees, assigned backlog work and durable messages + when: The owner previews and confirms reversal + then: Prior relationships return at a new revision without changing implementation ownership, identities, worktrees, tasks, messages or schedules + observable_outcomes: + - CLI plan hashes agree with service plans and CLI requires explicit owner confirmation + - An unrelated new commit invalidates an earlier plan + - Failed Git commits restore tracked file bytes and the index + - Injected audit failure rolls back local state but retains the tracked receipt + - Repeated confirmation reconciles that receipt without committing again + - Confirmed replays do not duplicate the owner audit + - Candidate acceptance rejects absent pilot-reversal evidence + determinism_controls: + - Disposable Git/worktree and SQLite fixtures + - Fixed clocks, failing Git hook and injected local audit trigger + - No real provider or system timer calls + anti_cheat_rationale: + prevents: + - Quietly dropping in-flight work while removing management roles + - Reusing a stale owner plan after main moves + - Repeating a committed conclusion after local state recording fails + - Allowing review recommendations to bypass owner confirmation + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Durable hierarchy pilot comparisons + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-hierarchy-pilot-review + given: An owner-activated hierarchy pilot with baseline metrics and success/stop criteria + when: Session Manager records interim and final review evidence + then: Evidence covers the complete baseline and criteria, matching units and maturity gates are enforced, history is durable and idempotent, and the organization remains unchanged + observable_outcomes: + - Inactive pilots and specialist reviewers cannot submit review records + - Missing or duplicate metrics, mismatched units, premature adoption and unsupported success claims are rejected + - Qualitative or unavailable observations cannot justify adoption recommendations + - Review records label evidence reviewer-reported rather than verified + - CLI lists persisted history and audits are emitted once per unique review + - Configuration bytes, Git HEAD and piloting state remain unchanged + - Candidate acceptance rejects missing pilot-review evidence + determinism_controls: + - Disposable installed Git repository and SQLite store + - Controlled activation and review timestamps with no sleeps or providers + anti_cheat_rationale: + prevents: + - Relabeling qualitative evidence as a proven improvement + - An interim success recommendation automatically adopting management changes + - Silent omission of unfavorable pilot criteria + - Passing review storage by mutating the active organization + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Visible unknown manager launches + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-manager-wake-recovery + given: An interrupted reservation in the project's real SQLite store + when: The dashboard snapshot observes it before and after owner recovery + then: Unknown launches are counted and identified, recovery updates the view, and observation does not change reservation records + - id: SCN-dashboard-demo-browser + given: The actual Console with a sample unknown manager launch + when: The owner follows its attention link + then: The collapsed Operations panel opens and shows the exact launch record without a restart or unlocking control + observable_outcomes: + - Presence is displayed separately from unknown launch outcome + - Runtime termination is never claimed by the read-only projection + - Attention navigation makes hidden launch details visible + - Demo still has zero live API requests and no browser errors + determinism_controls: + - Disposable Git and SQLite fixture with simulated crash-window state + - In-memory sample project and isolated loopback browser server + anti_cheat_rationale: + prevents: + - A dashboard read silently unlocking or retrying a launch + - An attention link pointing into inaccessible collapsed content + - A passing source-string assertion substituting for rendered UI evidence + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Self-host acceptance requires portable wake and provider evidence + +```yaml +test_integrity_note: + change_type: [new_tests, refactor_tests] + scenarios: + - id: SCN-candidate-acceptance-evidence + given: Green command results but missing portable wake-policy, reservation, recovery or provider-inheritance scenario evidence + when: Candidate acceptance evaluates the output + then: Acceptance fails for each omitted marker + observable_outcomes: + - Self-host receipts require all new portability and manager safety scenarios + - Provider inheritance has a unique scenario marker rather than sharing runtime-selection evidence + determinism_controls: + - Controlled command results at the candidate validation boundary + anti_cheat_rationale: + prevents: + - Green exit codes standing in for missing requirement evidence + - Duplicate markers masking an absent provider-inheritance scenario + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Interrupted manager wake inspection and recovery + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-manager-wake-recovery + given: A persisted reservation representing a crash after a simulated provider launch + when: A caller inspects or tries to recover the reservation through service and CLI + then: Only an approving owner with stopped-runtime evidence and an offline manager can recover it, without invoking a provider or bypassing budget and replay guards + observable_outcomes: + - Inspection distinguishes blocking unknown launches from completed records without claiming runtime termination proof + - Missing approval or evidence, non-owner callers, live managers and completed launches are rejected + - Recovery and its evidence audit commit atomically and repeated confirmation is idempotent + - The recovered message still cannot be invoked again and its daily budget charge remains + - CLI inspection and recovery approval boundaries match the service + determinism_controls: + - Disposable installed Git repository and SQLite store + - Fixed UTC clock and simulated provider invocation + - Crash-window state seeded at the durable reservation boundary + anti_cheat_rationale: + prevents: + - Age-based unlocking or silent invocation retries + - Recovery that refunds budget or bypasses message replay protection + - Claiming an offline heartbeat proves runtime termination + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-30 — Explicit portable manager wake policies + +This supersedes the earlier unconditional USD-cap requirement. The owner +requires a fleet usable with Codex, Claude, Pi and local/subscription runtimes; +requiring a provider feature absent from every built-in runtime prevented the +requested timer behavior. Enabling count mode is a separate explicit policy +choice. Existing hard-cap scenarios and their strict assertions are unchanged. + +```yaml +test_integrity_note: + change_type: [new_tests, spec_correction] + scenarios: + - id: SCN-manager-check-in-count-mode + given: An owner-enabled count-limited manager schedule and configured Codex, Claude or Pi identity + when: A tick recovers an existing pending check-in and prepares a wake + then: Real adapter plans succeed without a fabricated dollar-cap receipt, simulated invocation respects the shared UTC-day budget, and repeat ticks do not duplicate it + - id: SCN-manager-wake-reservation + given: One manager invocation is reserved and another schedule targets that manager + when: The second schedule tries to reserve another wake + then: The in-flight reservation blocks the second invocation without consuming another budget slot + - id: SCN-manager-wake-policy + given: Manager runtime wake configuration + when: Budget mode or daily limit is absent, or count mode also promises a dollar ceiling + then: Configuration validation rejects the ambiguous or unbounded policy + observable_outcomes: + - Disabled wakes remain disabled and timer installation is separate + - Codex and Claude default profiles and explicit Pi provider/model can prepare count-limited wakes + - The output explicitly reports dollarCapEnforced false in count mode + - A previously unavailable preflight can retry pending delivery without creating a duplicate message + - Daily limits apply across schedules and reset only across the UTC-day boundary + - Concurrent manager reservations and duplicate message attempts are refused atomically + - Legacy positive USD ceilings still select hard-cap mode and unsupported or mismatched receipts fail closed + - Console timer review displays the shared daily limit and the selected wake policy + determinism_controls: + - Fixed clock, disposable Git/worktree/XDG fixtures, real adapter planning and simulated wake callbacks + - No provider session, host timer, remote Git mutation or dollar expenditure + anti_cheat_rationale: + prevents: + - Claiming invocation counts enforce dollar spending + - Disabling hard-cap checks to obtain a passing subscription-runtime scenario + - Counting two schedules as separate daily budgets + - Stranding unreserved pending messages after an unavailable preflight + - Launching overlapping manager turns or consuming quota before preflight + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Product demo and provider portability review corrections + +The owner explicitly clarified that the dashboard demo is the actual TORCH +tool with sample data, opened separately, with no browser-tab wording in its +label. This supersedes the earlier miniature four-panel preview assertions. +The replacement checks strengthen the scenario through actual Console browser +interactions. Provider inheritance corrects the Codex-only installation +failure; explicit mixed-runtime assignments remain protected by existing tests. + +```yaml +test_integrity_note: + change_type: [spec_correction, new_tests] + scenarios: + - id: SCN-dashboard-demo + given: A visitor opens the landing page with no connected project + when: They open the actual dashboard demo and review or confirm sample actions + then: The shared Console renders and updates only tab-local sample state with zero live API calls + - id: SCN-install-runtime-selection + given: Generated roles inherit an installation default and explicit assignments may coexist + when: The owner selects Codex alone or mixed adapters with an explicit default + then: Installation respects the selected default and preserves explicit provider assignments + - id: SCN-cli-install-provider-default + given: A reviewed generated proposal + when: The CLI previews Codex-only installation then installs a mixed fleet with Codex as default + then: The dry-run is read-only and the installed identities and model defaults match the selected adapters + - id: SCN-domain-runtime-inheritance + given: A Codex-only Fleet identifies recurring work needing a new domain + when: The manager proposes a specialist with runtime omitted or inherited + then: The pending domain uses Codex and proposing does not create or start an identity + observable_outcomes: + - Real browser opens a separate page through the cleanly named landing link + - Ready backlog items appear in Queue, matching the authoritative backlog state model + - Priorities, requests, artifact feedback, approvals, and profiles update the sample project only after confirmation + - Replay does not duplicate sample actions; payload drift and unreviewed actions are rejected + - Reset restores sample records; mobile has no document overflow; browser emits no runtime errors + - Codex inherits gpt-6-luna/high and Claude inherits sonnet + - Pending architect designs accept valid Pi/custom adapter names without authorizing installation or invocation + - Malformed runtime identifiers are rejected and installation retains adapter validation + determinism_controls: + - In-memory demo transport, fixed test clock, temporary Git fixtures, loopback browser server + - Browser test intercepts and fails on every live project API request + - No provider, timer, remote Git write, or persistent fleet activation + anti_cheat_rationale: + prevents: + - Shipping a separate mock interface that diverges from the actual Console + - Passing source-only markup checks while real browser controls fail + - Silently forcing generated roles to Claude or losing explicit mixed assignments + - Displaying ready work in an unknown-state lane + relaxation: + did_relax_any_assertion: true + if_true_explain_spec_basis: The rejected static four-panel implementation assertions were replaced by stricter shared-Console browser and isolated-state behavior checks based on the owner's explicit correction. +``` + +## 2026-09-29 — Fail-closed manager wake cost ceilings + +```yaml +test_integrity_note: + change_type: [new_tests, runtime_budget_safety] + scenarios: + - id: SCN-runtime-cost-ceiling + given: A scheduled identity is configured with a maximum USD amount for one wake + when: TORCH plans the exact create or resume operation for its selected runtime + then: The adapter must declare that launch mode and return a matching enforced-cap receipt, otherwise no launch action is produced + - id: SCN-manager-check-in-cost-ceiling + given: A manager check-in wake has a daily invocation budget and a per-invocation USD ceiling + when: Its runtime cannot prove support for that ceiling + then: The check-in remains queued, the wake is skipped, and no daily invocation reservation is consumed + observable_outcomes: + - Enabling a schedule wake without a positive max_usd_per_invocation is rejected + - Runtime adapters declare cap-supported mode identifiers separately for create and resume + - Built-in Codex, Claude, and Pi modes declare no hard spend-cap modes + - A trusted fixture adapter receives the exact cap and emits a matching launch-mode receipt + - A missing, unsupported, or mismatched receipt prevents the manager wake + - Successful schedule evidence records the enforced mode and configured USD cap + determinism_controls: + - Temporary Git project, fixture adapters, fake wake executor, no provider invocation, timer installation, or real billing + anti_cheat_rationale: + prevents: + - Treating a daily launch-count budget as a dollar budget + - Treating estimated prices or post-hoc telemetry as a hard ceiling + - Starting persistent/background sessions with only a print-mode budget flag + - Consuming invocation quota for a wake that cannot meet its required cap + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Dedicated interactive dashboard demo page + +```yaml +test_integrity_note: + change_type: [new_tests, static_product_demo] + scenarios: + - id: SCN-dashboard-demo + given: A visitor opens the public TORCH landing page without a connected project + when: The visitor opens the dashboard demo in a new tab and selects Overview, Work board, Agent messages, or Release gates + then: The matching illustrative sample view is shown without connecting to project state or invoking an action + observable_outcomes: + - The landing page contains no dashboard demo workspace; its link targets a dedicated page in a new tab + - The dedicated demo page is noindex and labels its records as sample data with no live project connection + - All four tab controls target an existing, labelled panel + - Tab state and panel visibility are updated together, with keyboard navigation declared + - The demo uses a page-scoped script with no fetch or XMLHttpRequest path + - Release gate examples visibly distinguish passed local evidence from open live-provider and owner-review gates + determinism_controls: + - Source-level public-site regression with fixed sample markup; no provider, repository API, runtime, or schedule is contacted + anti_cheat_rationale: + prevents: + - Presenting invented dashboard records as live fleet telemetry + - Coupling a standalone product demo to the landing-page scroll or making it read or mutate a visitor's project + - Implying local checks equal live provider or release qualification + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Preview-confirmed Console schedule timer setup + +```yaml +test_integrity_note: + change_type: [new_tests, schedule_launcher_control] + scenarios: + - id: SCN-console-schedule-launcher + given: An installed Fleet has persistent system schedules and no TORCH schedule launcher installed + when: The owner reviews the exact plan and confirms timer installation in the loopback Console + then: Preview is read-only, stale or cross-origin confirmation is refused, only TORCH-owned unit files are installed, and the operation is audited exactly once + observable_outcomes: + - Every configured system schedule including behavior and exact action is included in the preview + - Unit paths, content digests, project schedule digest, and timer-start effect are visible before confirmation + - A changed schedule configuration after preview prevents unit creation or systemd calls + - Confirmation alone performs bounded file writes and the expected systemd enable/start calls + - Replay returns the same receipt without repeating systemd calls or audit events + - Snapshot reports installed-file ownership/digest without claiming that systemd currently has the timer active + determinism_controls: + - Temporary Git project, temporary XDG data/config directories, loopback HTTP server, fake systemd executor, no provider, timer, or live user-systemd call + anti_cheat_rationale: + prevents: + - Claiming a configured cadence is already installed + - Applying a stale or cross-origin timer plan + - Hiding mutating or provider-wake schedule actions from the review + - Replaying a single-use owner confirmation + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Owner-confirmed Console runtime profile changes + +```yaml +test_integrity_note: + change_type: [new_tests, dashboard_mutation] + scenarios: + - id: SCN-console-runtime-profile + given: An installed Fleet has an identity assigned to one runtime and the owner wants to change its next-launch provider/model profile + when: The owner previews and confirms the profile change in the loopback Console + then: The preview is read-only and exact, stale configuration is refused, the confirmed change is audited, and running sessions are untouched + observable_outcomes: + - Cross-origin preview requests are rejected + - The exact current and proposed runtime/model/reasoning settings are shown before confirmation + - Switching adapters removes the old adapter-specific identity launch-policy override and validates the selected adapter's effective defaults + - Configuration digest drift between preview and confirmation requires a fresh review + - Replaying confirmation returns the same receipt without recording another profile change + - The resulting owner audit event records the prior and next identity profile; no provider or runtime is started + - The Console keeps profile review, confirmation, and edit as three distinct explicit controls + determinism_controls: + - Temporary installed Git fixture, local Console loopback server, built-in runtime adapters, no AI provider, runtime process, timer, or hosted service + anti_cheat_rationale: + prevents: + - Bypassing the CLI's runtime capability and launch-policy validation from the dashboard + - Applying a stale or cross-origin provider/model change + - Implying that profile edits reconfigure an already-running session + - Silently applying a profile merely because a selector changed + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Owner-confirmed fast-forward-only forge publication + +```yaml +test_integrity_note: + change_type: [new_tests, forge_publication] + scenarios: + - id: SCN-forge-sync + given: An attached local forge mirrors the project's canonical branch and local canonical main advances + when: The owner reviews and confirms forge synchronization + then: Only the exact local canonical commit is published by fast-forward, and remote-ahead or unavailable ancestry blocks without rewriting remote history + observable_outcomes: + - Plan is read-only and identifies local and remote commit IDs + - Missing confirmation cannot update the remote ref + - Confirmed synchronization publishes the expected commit and records an owner audit event + - A remote tip not present locally requires an explicit fetch before ancestry can be assessed + - A remote-ahead tip remains unchanged and sync fails closed after fetch + - Forge loss degrades synchronization status but local integration remains operational + determinism_controls: + - Temporary local bare Git remotes and a side clone; no hosted forge, provider, timer, or persistent user remote + anti_cheat_rationale: + prevents: + - Concurrent direct pushes from overwriting a newer canonical remote ref + - Treating a plan-time remote observation as a force-push authorization + - Coupling local integration availability to forge availability + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — FIFO canonical integration drain + +```yaml +test_integrity_note: + change_type: [new_tests, integration_queue] + scenarios: + - id: SCN-integration-fifo-drain + given: Multiple authorized integration requests were verified against the same canonical tip + when: The landing authority drains the persistent integration queue + then: Requests are considered in insertion order, each successful landing serializes, and later tips that omit the new canonical commit move to needs_convergence rather than racing or weakening checks + - id: SCN-cli-integration-drain + given: An installed project has an empty integration queue + when: The owner invokes the CLI queue drain with and without explicit confirmation + then: The unconfirmed operation is refused without changing the canonical commit, while a confirmed one-shot drain returns an empty result and still does not start a background process + observable_outcomes: + - Only the configured landing authority can drain or directly land a request + - One authorized drain invocation is the single FIFO landing process for its ready queue + - Read-only Console snapshot exposes pending queue insertion order and authorization identity + - The first current candidate lands through the existing clean-target fast-forward gate + - A stale later request is visibly deferred and never changes canonical main + - After its owner converges the latest main and reruns exact-commit checks, a new authorized request can land + determinism_controls: + - Temporary Git worktrees, local SQLite state, passing fixture check, no remote, provider, background process, or timer + anti_cheat_rationale: + prevents: + - Concurrent authorized requests from racing the canonical branch + - Treating a check on an old source SHA as evidence for a converged tip + - Silently merging a branch that omits a canonical commit landed ahead of it + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Owner approval decisions in the local Console + +```yaml +test_integrity_note: + change_type: [new_tests, dashboard_mutation] + scenarios: + - id: SCN-console-owner-approval + given: Pending structured approvals are assigned either to the project owner or to a manager AI + when: The owner previews and confirms a decision in the local Console + then: Only owner-addressed requests can be decided, the exact current revision is rechecked, the requester is notified once, and stale, cross-origin, or replayed actions do not apply + observable_outcomes: + - Owner decision preview is mutation-free and presents requester, evidence, revision, note, and effect + - Confirmation delegates to the existing audited approval transaction + - A non-owner approver remains the sole decision-maker for its request + - Concurrent completion invalidates the preview and repeated confirmation is idempotent + determinism_controls: + - Temporary Git repository, local SQLite state, loopback HTTP server, no provider, timer, or runtime wake + anti_cheat_rationale: + prevents: + - Owner UI from silently overriding a manager or peer-AI approval target + - Stale review from deciding a changed request + - A retry from creating duplicate approval decisions or requester notifications + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Project integration gates in specialist prompts + +```yaml +test_integrity_note: + change_type: [spec_clarification, new_tests] + scenarios: + - id: SCN-project-gates-in-domain-prompt + given: A project has repository-wide checks but no evidence-backed domain-specific check mapping + when: TORCH generates an implementation specialist prompt + then: The prompt shows exact project integration check IDs and argv separately without claiming specialist ownership + observable_outcomes: + - Every specialist receives repository-wide integration gates configured for the project + - Domain-specific required_checks remain unchanged unless separately reviewed and evidenced + determinism_controls: + - Temporary Git fixture with a package script and no AI provider or runtime command execution + anti_cheat_rationale: + prevents: + - Hiding the gates all domains must pass before integration + - Inventing test ownership from names or keyword similarity + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Manager check-in cadence floor + +```yaml +test_integrity_note: + change_type: [new_tests, spec_correction] + scenarios: + - id: SCN-manager-check-in-cadence-floor + given: A project configuration has an owner-authorized manager check-in interval + when: The configured cadence is 59 seconds or exactly 60 seconds + then: The 59-second cadence is rejected at the project-config boundary and the 60-second cadence remains valid + observable_outcomes: + - Invalid high-frequency polling produces a path-specific configuration error before timer reconciliation or runtime wake + - One-minute manager polling remains configurable and ordinary schedules are unchanged + determinism_controls: + - In-memory project-config fixture; no timer, provider, runtime, or wall-clock access + anti_cheat_rationale: + prevents: + - Installing accidentally sub-minute manager polling that can flood durable check-ins + - Rejecting the 60-second minimum already enforced by the generated-schedule helper + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Candidate untracked-input visibility + +```yaml +test_integrity_note: + change_type: [new_tests, spec_clarification] + scenarios: + - id: SCN-candidate-untracked-visibility + given: A clean committed Git candidate source has one ignored local file and one non-ignored untracked source file + when: The owner reviews and stages the candidate + then: The plan lists the untracked source path, the staged artifact excludes it, and ignored local data remains neither listed nor copied + observable_outcomes: + - Candidate source identity remains bound to the exact Git commit + - The review plan exposes every non-ignored untracked path omitted from the candidate + - Ignored local files and untracked source files do not enter the staged artifact + determinism_controls: + - Temporary Git candidate repositories with a committed ignore file and fixed local files; no network, provider, or current checkout mutation + anti_cheat_rationale: + prevents: + - Mistaking the exact committed candidate for a copy of the full working tree + - Silently hiding excluded, non-ignored source changes from the candidate review + - Leaking ignored local data into the installed artifact + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Specification responsibility attribution + +```yaml +test_integrity_note: + change_type: [spec_correction, new_tests] + scenarios: + - id: SCN-spec-evidence-grounding + given: A repository has an implementation domain and its external specification has a similarly named prose-only responsibility with no repository path reference + when: TORCH creates the deterministic baseline and Session Architect brief + then: The requirement remains an unrepresented review signal and is not appended to the implementation domain based on keyword overlap + observable_outcomes: + - Existing repository ownership stays grounded in actual analyzed paths + - Pathless specification responsibilities remain visible for semantic architect and owner review + - Specification-only projects may propose provisional groups but must retain missing ownership status + determinism_controls: + - Temporary Git fixture, fixed external Markdown specification, local filesystem path checks, no AI provider or project mutation + anti_cheat_rationale: + prevents: + - Converting overlapping terminology into unsupported implementation ownership + - Hiding specification responsibilities that need human or architect review + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Serialized landing and resume instruction freshness + +```yaml +test_integrity_note: + change_type: [spec_correction, new_tests] + scenarios: + - id: SCN-integration-landing-serialization + given: Two independent control-plane connections target the same project landing queue + when: One holds the project SQLite write transaction and the other attempts to land an authorized request + then: The competing lander fails with a retryable busy result without moving the canonical branch, and can land after the lock is released + - id: SCN-current-instructions-on-resume + given: A resumed identity has stale prompt copies in its worktree while canonical project prompts have changed + when: TORCH plans and executes the resume and creates the Fleet brief + then: The digest-addressed latest canonical bundle includes the current organization role/report context, is delivered, and the brief exposes the same digest + observable_outcomes: + - Competing landing cannot perform a stale preflight and mutate the canonical ref during another project writer's transaction + - Claude and Pi receive current prompt files; Codex receives the current instruction envelope in its resume turn + - Prompt digest changes when authoritative instructions change and stale worktree files are ignored + - A changed owner-approved organization graph updates generated role context before resume without changing implementation ownership + determinism_controls: + - Temporary Git repositories, independent SQLite handles, fake runtime executors, and changed local prompt fixtures; no provider or network access + anti_cheat_rationale: + prevents: + - Treating sequential readiness checks as sufficient concurrency control + - Reusing old runtime conversation instructions after policy changes + - Treating a stale worktree prompt as authoritative + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Organization-aware lifecycle ordering + +```yaml +test_integrity_note: + change_type: [spec_correction, new_tests] + scenarios: + - id: SCN-hierarchy-aware-start-stop + given: A project has an owner-facing manager, a middle manager, and a specialist reporting through that manager + when: TORCH plans startup and wind-down and executes them with fake runtimes + then: Managers start before direct reports, stop after them, and final status is routed to each direct reporting manager + - id: SCN-hierarchy-startup-cycle + given: An acyclic role graph binds multiple roles to identities whose collapsed reporting graph contains a cycle + when: TORCH plans startup or wind-down + then: Both plans expose an identity-cycle blocker and no runtime executor is called + observable_outcomes: + - The returned startup and shutdown order follows the active organization graph rather than roster order or a hard-coded manager exception + - Each planned identity lists its direct manager identities + - Wind-down status messages go to direct managers, allowing reports to reach managers level by level + - Identity cycles block launch and shutdown before runtime mutation + - Legacy session_manager.start_last configuration is accepted but does not override graph ordering + determinism_controls: + - Temporary Git repositories/worktrees, one three-level organization fixture, a role graph whose identity projection cycles, and fake runtime executors + - No real provider, system timer, or live Fleet process + anti_cheat_rationale: + prevents: + - Passing lifecycle checks using flat roster order while ignoring middle management + - Routing every specialist wind-down report only to the Session Manager + - Launching a partially ordered Fleet when persistent identities collapse an acyclic role graph into a cycle + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Organization-aware manager check-in queue + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-manager-check-in + given: A manager has direct reports with waiting status and unacknowledged blocker messages + when: TORCH plans and queues a manager check-in + then: Findings are scoped to the active reporting graph, structured approval waits name the requester and approver, unstructured status is not treated as approval, one durable self-directed request is queued, and a pending request is deduplicated + - id: SCN-manager-check-in-refresh + given: A durable manager check-in is pending and new direct-report approval requests arrive after its timestamped snapshot + when: The bound manager reads the wake and refreshes current check-in state + then: A read-only identity-bound MCP tool returns the latest reports and named approval waits; specialists cannot use it to inspect manager reports + - id: SCN-manager-check-in-specialist + given: A specialist identity has no direct reports + when: TORCH builds its check-in plan + then: The plan has no direct reports and creates no spurious attention + - id: SCN-manager-schedule-coverage + given: An active organization graph has multiple manager identities with direct reports + when: TORCH validates the installed schedule configuration + then: Every manager identity must have its own owner-authorized system check-in cadence or configuration is rejected + - id: SCN-manager-check-in-schedule + given: An owner-approved system schedule names a manager check-in coordination action and its manager is offline + when: The system dispatcher reaches the configured interval + then: TORCH queues the direct-report request, records run evidence, preserves offline identity state, and makes no provider or shell command call + - id: SCN-manager-check-in-budgeted-wake + given: An owner-reviewed manager schedule explicitly enables runtime wake with a daily invocation cap + when: A check-in is queued for an offline manager and the cap is later exhausted + then: An injected runtime waker is called at most within the atomic cap; a missing waker blocks dispatch, active managers are not awakened, and durable check-ins remain queued + - id: SCN-default-manager-checkin-config + given: A deterministic project fleet proposal is generated + when: The owner reviews its validated schedules + then: It includes a 15-minute Session Manager check-in schedule but does not install or start a system timer + - id: SCN-manager-schedule-growth + given: A proposed organization graph adds or removes direct reports under managers + when: TORCH plans schedule coverage for the hierarchy + then: Missing managers receive a default owner-authorized cadence, existing cadence is preserved, and obsolete schedules are surfaced without removal or activation + observable_outcomes: + - Read-only planning reports direct-report status, tasks, blockers, and unacknowledged messages + - Pending direct-report requests are classified by named manager, peer-AI, or owner approver + - A queued check-in labels its snapshot as potentially stale and instructs the manager to refresh current durable state before acting + - The read-only manager refresh is scoped to the bound identity and active direct-report graph; it grants no approval authority + - Only the named approver can decide; decisions are revision-checked, audited, and durably returned + - Owner decisions require explicit CLI confirmation and cannot be claimed through a Fleet MCP identity + - Queuing a request does not launch or steer an AI runtime + - A schedule tick is authority-gated and duplicate pending requests do not create duplicate inbox entries + - Runtime wake is disabled by default, runtime/provider calls are represented only by an injected test waker, and one durable atomic daily invocation reservation is shared across manager schedules + - Deterministic project proposals include a default check-in schedule while timer installation remains a separate owner action + - Hierarchy planning derives coverage for every manager identity while preserving configured cadence and requiring separate activation + determinism_controls: + - Temporary local Git fixtures, fixed project graph, local SQLite state, fake command and wake executors, and no provider runtime or timer installation + anti_cheat_rationale: + prevents: + - Scanning agents outside the manager's direct-report scope + - Treating free-text waiting summaries as authorization or approval state + - Allowing a manager to approve a request assigned to another identity + - Duplicating unacknowledged check-in messages on repeated checks + - Claiming a provider session was awakened by durable inbox delivery + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Read-only manager and approval flow watch + +```yaml +test_integrity_note: + change_type: [new_tests, dashboard_observability] + scenarios: + - id: SCN-console-manager-check-ins + given: An installed organization has a waiting direct report with a structured approval assigned to the owner + when: The owner observes the local Console snapshot and opens Flow watch + then: The snapshot shows the manager, direct-report state, named approver, approval evidence, and cadence while explicitly not claiming timer installation or performing a decision + observable_outcomes: + - Read-only SQLite observation returns scoped manager reports and pending approvals without altering Git or project state + - Owner approvals appear in the attention queue and flow watch; summaries are escaped and private model reasoning is not exposed + - Timer state is reported as unverified, not inferred from a configured schedule + - Snapshot endpoint remains GET-only and Flow watch has no approval mutation controls + determinism_controls: + - Temporary repository and SQLite fixture, fixed clock, local HTTP server, no provider, systemd query, or runtime launch + anti_cheat_rationale: + prevents: + - Treating a configured schedule as proof its timer is active + - Inferring approval from vague presence text + - Letting dashboard observation mutate approval or manager state + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Versioned organization graph foundation + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-organization-graph + given: A versioned organization graph with an owner, owner-facing role, coordinator, and specialist + when: TORCH validates the graph, a specialist promotion, invalid ownership, authority leakage, or invalid reporting links + then: Valid structures are retained and duplicate implementation owners, owner-only authority leakage, cycles, and unknown role references are rejected + observable_outcomes: + - Organization graph validator returns the unchanged valid graph or a typed validation error with field evidence + - The installed project configuration schema accepts the versioned organization graph + - A specialist identity may hold both specialist and coordination roles without moving implementation ownership + determinism_controls: + - In-memory fixed graph fixtures; no clock, random IDs, model calls, or network access + anti_cheat_rationale: + prevents: + - Accepting multiple implementation owners for one surface + - Granting release or organization approval authority to a coordinator + - Treating reporting cycles or unknown role links as valid + - Forcing promotion to transfer implementation ownership + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Adapter-specific launch-policy profiles + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-runtime-launch-policy + given: Installed identities with runtime-default and identity launch policies for Codex, Claude, and a custom adapter + when: TORCH resolves launch plans or the owner changes project-local policy + then: Supported values inherit/override and map to adapter flags, unsupported or unsafe/conflicting values block before execution, legacy Codex defaults migrate without changing their invocation, and edits require --yes without starting sessions + observable_outcomes: + - Codex sandbox/approval flags and Claude permission-mode flags match validated policy values + - Pi and undeclared custom adapters reject policy overrides; fixed Pi safety flags are not weakened + - Profile inspection exposes effective launch policy and profile mutations only alter future launch plans + determinism_controls: + - Temporary local Git repository and deterministic adapter plans; no provider process, network, clock, or live runtime + anti_cheat_rationale: + prevents: + - Silently dropping or substituting unsupported policy values + - Combining Codex approve-for-me with an explicit sandbox + - Exposing Claude bypassPermissions as an ordinary profile setting + - Letting policy edits start sessions or change running identities + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Project-scoped saved backlog views + +```yaml +test_integrity_note: + change_type: [new_tests, interface_redesign] + scenarios: + - id: SCN-console-saved-views + given: An authoritative backlog snapshot and a browser-local project preference store + when: An owner filters tasks, saves or updates a named view, reloads it, or deletes it + then: The board shows matching active tasks while task records and server state remain unchanged + - id: SCN-console-saved-views-storage + given: Malformed browser preferences or unavailable/quota-limited browser storage + when: The Console loads or saves a view + then: Invalid views are ignored, the authoritative unfiltered backlog remains usable, and save failures explain that no task data changed + observable_outcomes: + - Saved filters are scoped by stable project identity and contain only bounded names and filter values + - Owner, state, priority, domain, and text filters combine against current backlog fields + - Static serving exposes only the fixed work-view asset; the snapshot API remains GET-only + determinism_controls: + - Fixed in-memory task objects, injected local-storage doubles, and temporary local repository; no provider, server-side write, or task mutation + anti_cheat_rationale: + prevents: + - Creating another backlog or copying task state into browser preferences + - Applying one project's filters to another project + - Hiding storage failures while implying a view was persisted + - Letting malformed preferences affect the authoritative work board + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Evidence-scoped shared path proposals + +```yaml +test_integrity_note: + change_type: [spec_clarification] + scenarios: + - id: SCN-domain-collision + given: Two domains import a shared source contract while repository-wide package and configuration files are also present + when: The deterministic domain baseline is generated + then: Each importing domain lists the actual shared contract, dependency evidence creates coordination, and unrelated repository-wide candidates are not blanket-assigned as shared writable paths + observable_outcomes: + - Shared ownership in the baseline is derived from component dependency edges and exact target paths + - Remaining shared candidates stay visible in architecture review evidence for Session Architect and owner review + determinism_controls: + - Fixed source imports and local Git fixture; no provider call or project mutation + anti_cheat_rationale: + prevents: + - Treating all configuration and untracked repository files as writable by every specialist + - Hiding real cross-domain contract ownership behind an empty shared-path list + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Adaptive owner cockpit observation and scoped feedback + +```yaml +test_integrity_note: + change_type: [new_tests, interface_redesign] + scenarios: + - id: SCN-console-readonly + given: An installed Fleet with durable backlog metadata and local runtime configuration + when: The owner opens the local Console and its observation endpoint + then: The responsive dashboard projects task dependencies, identity profiles, conversations, provenance, gates, and attention state without mutating the repository; its snapshot endpoint remains GET-only + observable_outcomes: + - Snapshot contains the authoritative backlog fields needed for a kanban projection and runtime/model/reasoning profile fields + - Missing screenshot/artifact provenance is identified as unavailable rather than represented as an empty verified gallery + - Console assets render a pulse, work board, attention queue, and accessible responsive navigation + - POST requests to the observation endpoint are refused + determinism_controls: + - Fixed local repository and state fixtures; no provider call, hosted service, or write action through the observation endpoint + anti_cheat_rationale: + prevents: + - Rendering a dashboard board from a second, divergent task source + - Hiding missing artifact provenance behind a fabricated gallery + - Treating read-only dashboard rendering as owner authorization + - Exposing private model reasoning in the communication feed + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Adaptive organization proposals and per-identity runtime profiles + +```yaml +test_integrity_note: + change_type: [new_tests, spec_clarification] + scenarios: + - id: SCN-hierarchy-proposal + given: A versioned active organization and repeated coordination evidence + when: The Session Manager submits or deduplicates a hierarchy proposal, or an owner reviews it + then: Sustained evidence and alternatives are required, only the configured owner may decide, implementation ownership remains unchanged, and activation requires a separate fresh plan and owner confirmation + - id: SCN-hierarchy-reconsideration-after-new-evidence + given: A hierarchy proposal that was rejected by the owner + when: The Session Manager submits the same proposed graph with a later evidence window and new references + then: The new evidence creates a fresh owner-review proposal while exact retries of that evidence remain idempotent + - id: SCN-hierarchy-pressure-assessment + given: A project with a configured observation window and durable coordination records + when: The Session Manager assesses recurring organization pressure + then: The tool reports only dated in-window evidence, requires recurrence or multiple signals, refuses specialist authority, and never creates or activates a proposal + - id: SCN-hierarchy-proposal-mcp + given: An MCP server bound to the Session Manager identity + when: It proposes, lists, reads, or plans an organization change + then: The manager can prepare owner-review material but no owner-decision tool is exposed + - id: SCN-hierarchy-cli + given: A durable hierarchy proposal created by the Session Manager + when: A caller attempts owner review through the CLI + then: A coordination identity is refused, the configured project owner may approve a bounded pilot, and the pilot plan remains non-mutating + - id: SCN-hierarchy-activation + given: A fresh, owner-approved hierarchy pilot that promotes an existing identity and leaves the project clean + when: The owner previews and separately confirms pilot activation + then: The committed graph and required manager schedules change together while identities, implementation ownership, active tasks, systemd units, and runtime sessions remain untouched + - id: SCN-hierarchy-activation-rollback + given: A fresh owner-approved hierarchy pilot and a local Git hook that rejects its activation commit + when: The owner confirms pilot activation but Git refuses the scoped commit + then: Canonical config, install manifest, pilot record, and index return to their exact prior state and the proposal remains approved for retry + - id: SCN-console-hierarchy-activation + given: An owner-approved hierarchy pilot is shown in the local Console + when: The owner previews activation, repository state changes, then reviews and confirms a fresh plan + then: Cross-origin and stale requests are refused, a fresh one-use confirmation creates exactly one scoped activation commit, and no session or timer is started + - id: SCN-runtime-profiles + given: Multiple identities assigned distinct registered runtime adapters and model/reasoning profiles + when: TORCH plans startup, diagnoses identities, and the owner edits installed project-level profile defaults + then: Each effective profile is routed to its selected adapter, supported config changes affect only a future launch plan, unsupported capabilities block startup with explicit findings, and TORCH does not silently substitute a provider + - id: SCN-runtime-profile-cli + given: An installed project with configured runtime defaults and persistent identities + when: The owner inspects profiles, changes an identity/default, resets identity overrides, or omits explicit confirmation + then: Writes require --yes, validate the resulting config, preserve identity overrides above runtime defaults, and never start or change a running session + - id: SCN-pi-adapter + given: A fleet identity assigned the built-in Pi adapter with a user-selected provider/model profile + when: TORCH plans a fresh or resumed session + then: It uses Pi's resumable-turn CLI, scoped prompt and transcript directory, explicit model/thinking selection, and a required model without starting Pi during planning + - id: SCN-pi-mcp-bridge + given: A TORCH-bound Pi extension and a local project with an approved roster identity + when: Pi invokes a registered TORCH tool through the bundled MCP bridge + then: The extension exposes the complete MCP tool set, binds calls to the configured identity, rejects identity spoofing, and shuts down the per-call MCP process without invoking an AI provider + observable_outcomes: + - Hierarchy evidence, decisions, and candidate graph revision persist in the local control-plane database without editing active config or roster + - A hierarchy pilot plan records graph delta, resulting manager schedules, timer reconciliation requirement, affected active backlog, worktrees, preservation promises, and blockers + - Explicit pilot activation updates canonical organization and schedule config, install-manifest hashes, and a durable pilot record in one scoped Git commit; no identity, worktree, timer, or provider is started + - The Console activation preview exposes the exact fresh pilot plan, owner identity, active work, manager cadence delta, blockers, and systemd reconciliation requirement; stale preview cannot activate + - Replaying a consumed Console activation token returns the same result without creating a second commit + - When an exact-config timer exists, activation leaves its units unchanged and reports the required separate reconciliation; successful reconciliation applies the new manager cadence, while a failed activation commit leaves the proposal and timer untouched + - Runtime startup plans report the selected adapter and per-identity profile + - Identity/agent queries and dashboard snapshots expose the effective runtime defaults plus any per-identity overrides + - Editing installed runtime defaults updates future launch plans and dashboard snapshots without starting or stopping a session + - Pi launch plans retain the same Pi session identifier and isolate transcript data under local TORCH project state + - Pi extension tools match the MCP server manifest and cannot claim another roster identity + determinism_controls: + - Fixed project fixtures, explicit timestamps and identities, registered test adapters, no provider calls, and temporary local Git repositories + anti_cheat_rationale: + prevents: + - Promoting a role from a single uncorroborated observation + - Treating a manager proposal as owner approval or as live organization state + - Transferring implementation ownership through coordination hierarchy changes + - Claiming a runtime supports model or reasoning controls it does not implement + - Silently replacing an unavailable or unsupported runtime profile + - Loading arbitrary project Pi extensions into TORCH Pi sessions + - Allowing a Pi model to escape its identity-bound TORCH MCP context + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Provenance-backed visual evidence and routed owner feedback + +```yaml +test_integrity_note: + change_type: [new_tests, interface_redesign] + scenarios: + - id: SCN-artifact-review + given: An installed Fleet, one assigned task, a publishing identity, a full Git commit, and a bounded image in that identity worktree + when: The identity publishes the image, then the owner previews and confirms feedback through the CLI or the local Console + then: TORCH preserves task, identity, runtime-session, commit, and digest provenance; routes the comment as a durable message to the task owner; and serves only digest-verified bytes + - id: SCN-artifact-boundaries + given: Artifact publication or feedback input with an unknown task, abbreviated commit, symlink, path traversal, wrong actor, or tampered content + when: TORCH validates or serves the item + then: The operation fails with a typed error and does not claim the image is verified + observable_outcomes: + - Images are copied into private project-local external state, never checked into the project repository by the artifact service + - Feedback preview is mutation-free; only the configured owner can create the durable message, linked to artifact, task, and exact commit + - The observation endpoint remains GET-only; Console feedback requires same-origin loopback JSON and a single-use five-minute preview token bound to the exact message effect + - Stale, expired, cross-origin, and replayed confirmations are refused without adding a message + - The Console displays comments without exposing private model reasoning or changing backlog state + - Content tampering is rejected by the serving route and marked unavailable in the snapshot + determinism_controls: + - Temporary local Git repository, one-pixel fixed PNG, deterministic runtime identity, local SQLite, and loopback fixture HTTP; no provider or hosted network calls + anti_cheat_rationale: + prevents: + - Attaching an image to a task not owned by its publishing identity + - Accepting an abbreviated or missing commit as exact provenance + - Traversing symlinks or copying external files into the gallery + - Treating a feedback preview as an owner decision + - Letting a cross-origin caller forge owner feedback or replay a prior confirmation + - Serving tampered image bytes as trusted visual evidence + - Adding a second comment ledger beside durable inter-session messages + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — User-trusted local runtime adapters + +```yaml +test_integrity_note: + change_type: [new_tests, security_boundary] + scenarios: + - id: SCN-runtime-plugin-trust + given: A local CommonJS adapter module that is not in TORCH's user-local trust store + when: The owner previews trust, edits the module after preview, approves the stale digest, lists adapters, loads the reviewed plugin, and later revokes trust + then: Preview/list never execute the module; approval requires the exact reviewed SHA-256; changed code fails closed; the adapter runs only after trust; revocation does not claim to stop running sessions + - id: SCN-runtime-plugin-lifecycle + given: An installed Fleet whose project configuration initially uses only a built-in adapter and a separately user-trusted local plugin + when: The owner assigns that plugin to one identity and TORCH plans the mixed Fleet + then: Project-local profile configuration may select the already-trusted adapter, the launch plan uses its declared model/reasoning/capabilities, and the plan starts no runtime + observable_outcomes: + - Trust records are user-local, outside project-controlled configuration, and pin the canonical entrypoint SHA-256 + - Trust and revocation require --yes plus the exact hash shown by the review plan + - Project configuration cannot register a module path or authorize plugin code + - Runtime plugin load failures are surfaced in the registry/doctor; no provider substitution occurs + - Profile selection may add a locally trusted adapter's declared configuration to the project without starting or stopping sessions + determinism_controls: + - Synthetic local adapter with a load sentinel, temporary XDG config/data roots, and temporary Git repository; no network or AI provider calls + anti_cheat_rationale: + prevents: + - Executing a plugin during trust preview or runtime catalog listing + - Reusing owner approval after the entrypoint changes + - Letting project-edited JSON invent a trusted adapter or executable path + - Treating an adapter launch plan as evidence that a provider was started + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Backlog-derived feature and milestone progress + +```yaml +test_integrity_note: + change_type: [new_tests, dashboard] + scenarios: + - id: SCN-backlog-classification + given: A backlog task with an explicit revision and a Session Manager identity + when: The manager classifies or clears its feature and milestone labels through the service, CLI, or MCP tool + then: TORCH revision-checks and audits only the labels; task ownership, state, evidence, dependencies, and queue order remain unchanged + - id: SCN-console-initiative-rollups + given: Backlog tasks with explicit feature and milestone labels, including completed, cancelled, blocked, active, and unlinked tasks + when: The Console derives progress summaries + then: It groups only explicitly labeled tasks, excludes cancelled tasks from the completion denominator, reports risk and task-state counts, and does not alter source records + observable_outcomes: + - Feature and milestone labels remain optional fields on the authoritative backlog task, not a parallel initiative ledger + - Classification is manager-only, reasoned, revision-checked, and retained in task-local history plus the audit stream + - Unlabeled tasks remain ungrouped; names and completion are never inferred from paths, domains, or prose + - Progress percentages describe task states only and cannot imply passing tests, integration, release, deployment, or live verification + determinism_controls: + - Fixed task fixtures and browser helper execution; no provider calls, network calls, or live project mutation + anti_cheat_rationale: + prevents: + - Treating dashboard percentages as release or quality qualification + - Introducing a competing feature/milestone queue + - Letting specialists or stale revisions change project classification + - Silently fabricating labels for unclassified work + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Preview-confirmed owner backlog reprioritization + +```yaml +test_integrity_note: + change_type: [new_tests, security_boundary, dashboard] + scenarios: + - id: SCN-owner-backlog-priority + given: A backlog task with a current revision and a local project owner + when: The owner previews and confirms a reasoned priority change, or a specialist attempts the same operation + then: Only owner authority can apply a revision-checked change; task state, owner, dependencies, and evidence stay unchanged; history and audit record the change + - id: SCN-console-priority-confirmation + given: An installed Fleet task and the loopback Console + when: The owner previews a priority change, the task changes concurrently, a request comes cross-origin, or a token is replayed + then: Only a fresh same-origin preview confirmation applies the exact reviewed effect; stale, cross-origin, expired, and replayed requests fail without an additional mutation + observable_outcomes: + - Preview is mutation-free and identifies task, revision, previous/new priority, reason, and bounded effect + - Confirmation consumes a five-minute single-use token and recomputes the revision-bound plan before writing + - Owner priority changes have durable task history and a control-plane audit event + - Snapshot remains GET-only and the Console gains no task-state, assignment, evidence, integration, release, or lifecycle write route + determinism_controls: + - Temporary installed Git fixture, local SQLite state, and loopback HTTP requests; no provider or hosted network calls + anti_cheat_rationale: + prevents: + - Applying a priority selected after preview without renewed confirmation + - Reusing a stale revision or confirmation token + - Crossing origin boundaries to invoke owner actions + - Treating reprioritization as authority to reassign work or claim task completion + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Dark TORCH product surface and v1 mark + +```yaml +test_integrity_note: + change_type: [interface_redesign, asset_reuse] + scenarios: + - id: SCN-torch-brand-surface + given: The public TORCH site and loopback Fleet Console + when: Both surfaces load their shared design tokens and brand assets + then: Dark color-scheme is the default, accessible text/status contrast remains intentional, and the actual v1 torch mark is used as the wordmark icon and browser favicon + observable_outcomes: + - The source v1 SVG is confirmed from the legacy/nostr-torch branch and served through a fixed local asset route + - Both public and Console HTML reference the same torch mark and declare a dark browser theme color + - Responsive layout, focus indicators, and reduced-motion behavior remain in the shared stylesheet + determinism_controls: + - Static markup/style assertions and loopback fixed-asset route test; no browser extension or network resource required + anti_cheat_rationale: + prevents: + - Replacing the historical logo with a lookalike glyph + - Applying dark mode only to the Console while leaving the public product page inconsistent + - Referencing an external or unpinned logo asset + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Preview-confirmed owner task proposals + +```yaml +test_integrity_note: + change_type: [new_tests, authority_boundary, dashboard] + scenarios: + - id: SCN-owner-backlog-create + given: An installed Fleet with a configured owner and Session Manager + when: The owner plans and creates a new task proposal, or a specialist attempts either operation + then: Only owner authority can propose the task; it is audited, proposed, unassigned, and leaves dispatch authority with the Session Manager + - id: SCN-console-task-create-confirmation + given: An installed Fleet and the local loopback Console + when: A task proposal is previewed, altered, confirmed, replayed, or submitted cross-origin + then: Only a fresh same-origin confirmation creates the exact reviewed proposal; changed inputs and cross-origin calls do not mutate the backlog, and an exact retry returns the same task without creating another + observable_outcomes: + - Preview records exact normalized task fields, observed commit, routing suggestions, dependencies, and the proposed/unassigned effect without writing a task + - Confirmation uses a five-minute token bound to the reviewed plan; each token creates at most one task + - Domain selection records affected domains only; it does not bind implementation ownership or dispatch work + - Owner action is durably audited and does not alter runtime/session state + - Existing Session Manager task creation and assignment authority remain unchanged + determinism_controls: + - Temporary installed Git fixture, local SQLite state, and same-origin loopback HTTP requests; no provider turn or live project write + anti_cheat_rationale: + prevents: + - Treating routing suggestions as specialist assignment + - Creating work on stale or changed preview content + - Replaying a browser retry into duplicate task records + - Escalating the owner interface into task-state or runtime lifecycle authority + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Owner review of Fleet and hierarchy proposals + +```yaml +test_integrity_note: + change_type: [owner_authority, dashboard, state_transition] + scenarios: + - id: SCN-console-organization-review + given: An installed TORCH Fleet with proposed Fleet and hierarchy changes + when: The owner previews, tampers with, confirms, or replays a proposal decision + then: Only a fresh same-origin preview can record the reviewed owner decision, and no decision directly activates organization state or starts an agent + observable_outcomes: + - Preview is mutation-free and displays the current proposal, scope, evidence, effect, owner identity, and repository head + - Hierarchy preview shows manager check-in schedule additions/obsolete candidates and reports timer installation as unverified; any config-bound timer reconciliation remains a separate step + - Schedule-launcher reconciliation refreshes the digest-bearing service for newly added manager schedules, refuses changed or missing owned units, updates the manifest only after validation, and restores exact prior unit and manifest state if systemd reload fails + - Confirmation rechecks the proposal state, active graph, current head, and exact plan hash; stale and cross-origin requests are refused + - Fleet approval only authorizes a later activation review; hierarchy approve-pilot does not itself activate, and a separate activation rejects stale, dirty, unapproved, or non-owner plans + - A successful owner decision is audited by the existing proposal services and exact retries do not create duplicate effects + - Organization review controls use the existing proposal ledgers; they do not write the active graph, provision worktrees, or launch providers + determinism_controls: + - Temporary installed Git fixture, local SQLite state, and loopback HTTP requests; no provider or hosted network calls + anti_cheat_rationale: + prevents: + - Treating a staged approval as activation, runtime launch, or hierarchy adoption + - Applying a decision after the proposal, active graph, or current repository head changed + - Replaying or forging a consequential owner action from another origin + - Creating a second dashboard-only organization proposal ledger + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Installed Pi extension startup without a model turn + +```yaml +test_integrity_note: + change_type: [new_tests, runtime_adapter, provider_boundary] + scenarios: + - id: SCN-pi-installed-extension-startup + given: An installed Pi CLI and TORCH's bundled identity-bound extension + when: Pi starts in offline, no-session RPC mode with no prompt and requests only its state + then: The extension loads, its TORCH-specific flags are accepted, RPC responds, and no extension error or provider turn occurs + observable_outcomes: + - Pi's extension API resolves its declared TypeBox runtime dependency from TORCH's installed package + - Local adapter qualification checks the actual installed Pi loader rather than only a mocked registration API + - Missing Pi is reported as a skipped host-specific smoke test; a present but incompatible Pi fails the scenario + determinism_controls: + - PI_OFFLINE=1, PI_TELEMETRY=0, isolated temporary Pi config/session paths, --no-session, and no prompt + anti_cheat_rationale: + prevents: + - Treating mocked extension registration as proof that Pi can load the distributed extension + - Calling a model or depending on provider credentials to qualify local extension loading + - Allowing project extension discovery or project trust during the smoke test + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Preview-confirmed owner agent requests + +```yaml +test_integrity_note: + change_type: [owner_authority, durable_messaging, dashboard] + scenarios: + - id: SCN-console-owner-agent-request + given: An installed Fleet, an approved roster identity, and an optional active backlog task + when: The owner previews, changes, confirms, replays, or submits a request cross-origin + then: Only a fresh exact preview can write one audited durable message to the selected inbox; backlog, ownership, identity state, and runtime lifecycle remain unchanged + observable_outcomes: + - Preview identifies exact recipient, title, message body, optional task reference, active graph, repository head, and no-start/no-reassignment effect + - Confirmation recomputes that plan; altered input, changed repository, unknown task, cross-origin, expired, and replayed requests fail closed + - Owner identity is checked against the active organization graph and the durable owner message is visible through existing conversation reads + - Idempotent retry returns the original message without adding a second inbox item or audit action + determinism_controls: + - Temporary installed Git fixture, local SQLite state, and same-origin loopback HTTP requests; no provider or runtime launch + anti_cheat_rationale: + prevents: + - Turning an owner request into a new task queue or silent assignment + - Starting or waking an agent as a side effect of sending a message + - Reusing a preview after its request text or repository evidence changed + - Allowing non-owner identities or cross-origin pages to create owner requests + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — COMBATRIG backlog migration assessment + +```yaml +test_integrity_note: + change_type: [new_tests, importer_compatibility] + scenarios: + - id: SCN-combatrig-backlog-assessment + given: A legacy fleet repository has roster-backed areas, a manager-area queue, an unknown historical area, free-form assignee names, and legacy terminal statuses + when: Torch builds a read-only compatibility proposal + then: It reports counts and unmapped references, does not infer identity ownership, does not create backlog tasks, and distinguishes legacy done from verified Torch completion + observable_outcomes: + - Backlog inventory reports counts by source area and status plus unmapped source areas + - The ai-sessions alias is visible as Session Manager queue history requiring review, not specialist implementation ownership + - Free-form assignedTo values are not treated as runtime identity IDs + - Legacy done is not marked as completed without target commit, evidence, and landed integration provenance + - Repository Git status is unchanged by compatibility analysis + determinism_controls: + - Temporary Git fixture with four fixed backlog items; no actual COMBATRIG writes, task creation, provider, or runtime + anti_cheat_rationale: + prevents: + - Silently discarding queue items from retired or unmapped areas + - Converting display names to executable runtime identities + - Bypassing Torch's verified-completion evidence gate + - Claiming migration when only a status count was inventoried + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Candidate failure diagnostics + +```yaml +test_integrity_note: + change_type: [new_tests, candidate_acceptance] + scenarios: + - id: SCN-candidate-failure-diagnostics + given: Candidate acceptance checks return both stdout and stderr and one check fails + when: TORCH reports the candidate acceptance result + then: The failed check includes bounded output excerpts while successful checks do not duplicate their logs + observable_outcomes: + - Test failures and subprocess errors include actionable captured output in candidate build diagnostics + - Excerpts are bounded to 12,000 characters per stream and retain the final output where the likely failure cause appears + - Successful validation receipts preserve check status and output byte counts without embedding full logs + determinism_controls: + - Injected child-process results; no filesystem, provider, runtime, or network use + anti_cheat_rationale: + prevents: + - Reporting a failed candidate without the reason needed to fix it + - Dumping unbounded process output into CLI errors and validation receipts + - Duplicating successful test logs into persistent metadata + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-28 — Candidate acceptance evidence completeness + +```yaml +test_integrity_note: + change_type: [new_tests, reliability_fix] + scenarios: + - id: SCN-candidate-acceptance-evidence + given: The self-host candidate manifest lists required acceptance scenarios + when: TORCH maps test output markers into the candidate validation receipt + then: Every required scenario has one or more explicit markers, and missing evidence prevents candidate acceptance + observable_outcomes: + - The evidence mapping keys exactly match the required acceptance scenario list + - Hierarchy activation, manager check-in schedules, and approval routing must be present in test output before their scenarios are accepted + - An unmapped or marker-free required scenario can never pass through an empty evidence condition + determinism_controls: + - Synthetic check output with all mapped markers, followed by a run missing one hierarchy activation marker + - No repository mutation, runtime, timer, or provider access + anti_cheat_rationale: + prevents: + - Treating test and lint exit codes as proof that every declared product scenario was exercised + - Quietly accepting newly added required scenarios without corresponding test evidence + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Candidate acceptance state isolation + +```yaml +test_integrity_note: + change_type: [reliability_fix, new_tests, spec_completion] + scenarios: + - id: SCN-candidate-acceptance-isolation + given: Candidate validation is launched while the installer has a configured XDG data home + when: TORCH runs candidate test, lint, and syntax gates + then: Every child uses a disposable temporary HOME, TMPDIR, and XDG roots distinct from the candidate version store, and those roots are removed after validation + observable_outcomes: + - The inherited installation XDG data root is not passed to acceptance children + - All three gates share only the isolated temporary user-state roots + - Acceptance cleanup removes the temporary roots after success or failure + - Candidate activation and the active version pointer remain untouched + determinism_controls: + - Injected child-process runner captures exact cwd and environment without running a provider or timer + - Candidate version storage and acceptance HOME use distinct temporary roots + anti_cheat_rationale: + prevents: + - Tests mutating or colliding with the candidate installer state they are meant to qualify + - Passing source-tree checks while the copied candidate fails under inherited user XDG configuration + - Candidate validation leaving user-state artifacts behind + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Owner-authorized scheduled integration drain + +```yaml +test_integrity_note: + change_type: [spec_correction, new_tests] + scenarios: + - id: SCN-scheduled-integration-drain + given: A project has a reviewed periodic integration-drain schedule and a source commit with passing exact-SHA checks + when: The system schedule dispatches first before and then after per-request landing authorization + then: The unauthorized request stays unlanded, the authorized request lands through the FIFO integration service, and no timer is installed merely by defining the schedule + observable_outcomes: + - Integration drain is accepted only as a periodic, owner-authorized system mutation using configured landing authority + - Sub-minute intervals and retries are rejected before scheduled execution + - A request without per-item landing authorization is not drained even when the schedule itself is owner-approved + - A request with fresh exact-commit check receipts lands through the same serialized integration path + - Schedule configuration alone does not install or start the user-systemd timer + determinism_controls: + - Disposable local Git repository/worktree, fixture check receipt, manual clock, no remote, provider, real systemd executor, or runtime + anti_cheat_rationale: + prevents: + - Turning a background schedule into blanket approval for queued code + - Draining work without exact-SHA evidence or configured landing authority + - Implying that review or configuration automatically activates persistent host state + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Evidence-backed organization assessment in Session Architect proposals + +```yaml +test_integrity_note: + change_type: [new_tests, spec_completion] + scenarios: + - id: SCN-architect-organization-assessment + given: A project has two evidenced implementation domains and a Session Architect proposal is pending + when: The proposal recommends either a flat fleet or a coordination lead + then: The choice is evidence-validated, proposed leads state an integrated outcome and bounded decisions, implementation and owner authority remain separate, and direct peer communication stays available + observable_outcomes: + - The bootstrap brief presents hierarchy evidence, all six authority distinctions, and a flat-by-default recommendation without selecting a fixed role catalog + - A coordination role must cite known evidence, coordinate only existing domains, and name an integrated outcome, decision scope, and owner escalations + - Duplicate or unknown domain references and fields outside the authority contract are rejected + - A role that claims implementation paths or owner-approval authority is rejected + - An unresolved shape, fabricated evidence, or manager-only peer routing is rejected + - The proposal remains pending and no identity, organization graph, runtime, or provider is activated + determinism_controls: + - Temporary Git project with two local source domains and a synthetic provider response; no paid provider, runtime, or installation + anti_cheat_rationale: + prevents: + - Copying a fixed management hierarchy into unrelated projects + - Promoting relays without evidence or an integrated outcome + - Smuggling code ownership or owner authority into coordination roles + - Turning hierarchy into a communication bottleneck + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +## 2026-09-29 — Read-only organization lifecycle order in the owner Console + +```yaml +test_integrity_note: + change_type: [new_tests, interface_redesign] + scenarios: + - id: SCN-console-readonly + given: An installed Fleet with an approved reporting graph and managed identities + when: The owner views the organization section in the local Console + then: Startup and wind-down orders match the runtime planner, direct manager dependencies and hierarchy blockers remain visible, and observation performs no lifecycle action + - id: SCN-console-lifecycle-blockers + given: An installed Fleet whose valid role graph collapses into an identity-level reporting cycle + when: The owner opens a fresh Console snapshot + then: The lifecycle projection exposes the cycle blocker and performs no start or stop action + observable_outcomes: + - Startup lists managers before their reports using the same ordering function as fleet startup + - Wind-down lists the exact reverse order used by fleet shutdown + - The projected order includes direct manager identities and any graph blockers + - An identity-level reporting cycle remains an explicit lifecycle blocker in the fresh snapshot + - The lifecycle projection states mutationPerformed false and offers no start/stop controls + - Narrow layouts stack the two ordered sequences without hiding identities + determinism_controls: + - Temporary installed Git project, deterministic organization graph, no runtime adapter invocation, timer, provider, or host service + anti_cheat_rationale: + prevents: + - A dashboard-only order that disagrees with executable lifecycle policy + - Hiding a missing manager or identity cycle behind a plausible sequence + - Representing an observation preview as a session launch or shutdown + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` +# Frozen queued-check input scenarios (2026-09-30) + +```yaml +test_integrity_note: + change_type: [new_tests, refactor_tests] + scenarios: + - id: SCN-frozen-check-inputs + given: Approved build inputs and matching project commit marker + when: Source is rebuilt in place after capture + then: Frozen bytes stay unchanged and only the owned copy can be removed + - id: SCN-frozen-check-rejection + given: Stale markers, unsafe input paths or modified copies + when: Capture or verification is requested + then: No qualifying snapshot is accepted + - id: SCN-frozen-resource-check + given: A browser slot is occupied and a specialist prepares its inputs + when: The specialist commits and rebuilds before its slot becomes free + then: CLI and identity-bound MCP run the captured build and receipt its original SHA + - id: SCN-frozen-check-mutating-executor + given: An executor changes input bytes or fails to launch + when: The prepared check finishes + then: Nonpassing evidence is persisted and the owned copy is cleaned up + observable_outcomes: + - Actual filesystem bytes, independent inodes, exact-SHA receipts and persisted prepared states + - CLI approval refusal and identity-bound MCP results + - Candidate acceptance rejects missing frozen-input evidence even with green command exits + determinism_controls: + - Disposable Git repositories and source/build fixtures + - Explicit FIFO acquisition and release without waits or retries + anti_cheat_rationale: + prevents: + - Reusing a live build directory under a frozen label + - Unsafe hardlinks to mutable outputs + - Crediting an old test to a newer specialist commit + - Green executor exit hiding changed inputs + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` +# Project-defined test-condition receipts (2026-09-30) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-check-conditions-contract + given: A real project probe reports matching subject and required conditions + when: Measurements execute + then: Pre/post reports validate and the diagnostic precedes measurement output + - id: SCN-check-conditions-fail-closed + given: False, missing, malformed or wrong-subject reports + when: A check is requested + then: Measurements do not execute + - id: SCN-check-condition-drift + given: Initially valid conditions + when: A green measurement changes visibility + then: The invalid postflight makes the evidence incomplete + - id: SCN-condition-receipts + given: Live and frozen checks with condition policy + when: Check results are persisted and queried + then: Valid reports persist and missing evidence or changed policy cannot qualify + observable_outcomes: + - Real subprocess output and absence or presence of measurement files + - Durable exact-commit receipt fields, condition artifacts and pass qualification + - Candidate acceptance rejects omitted condition scenario markers + determinism_controls: + - Fixed clock and disposable local Git/filesystem fixtures + - Probe reports use independent copies of expected subject values + - Policy-change checks edit canonical project configuration, not an in-memory service map + anti_cheat_rationale: + prevents: + - Treating exit zero as proof of correct test conditions + - Running measurements despite invalid preflight + - Passing results despite postflight drift + - Applying a report to a different build or condition policy + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` +# Policy-controlled specialist self-claim (2026-09-30) + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-console-live-refresh + given: Concurrent snapshot reads, edited model/task fields and an active approval preview + when: Controlled 15-second refreshes and explicit owner actions run + then: Reads remain single-flight, stale responses are discarded, unrelated evidence updates, draft/preview DOM survives, completing one action preserves other drafts and failures retain evidence + observable_outcomes: + - Deferred promise request ordering without sleeps + - Real browser input values, original preview token and unprotected panel node replacement + - Successive external profile changes update a clean editor without falsely marking it dirty + - Pause/resume, hidden visibility and page lifecycle timer behavior + - Missing scenario marker blocks candidate acceptance + determinism_controls: + - Explicit promise resolution for read races + - Playwright clock with controlled visibility/page lifecycle events + - Isolated demo transport and zero live API calls + anti_cheat_rationale: + prevents: + - Re-rendering over owner drafts or issuing a different preview token + - Allowing slow background reads to starve updates + - Completing an action by clearing unrelated forms + - Using real sleeps or broad retry loops to hide races + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-console-operation-outcomes + given: Unknown delivery effects, interrupted operations, malformed receipts and credential-bearing arbitrary metadata + when: Read-only project-scoped evidence is rendered through the shared dashboard + then: Unknown and unconfirmed states stay explicit, success does not imply live deployment, arbitrary metadata is omitted, safe text is escaped and no retry buttons or state mutations appear + observable_outcomes: + - In-memory SQLite records unchanged after observation + - Cross-project records excluded and raw secret references/error codes omitted + - Real isolated browser demo renders outcome/effect uncertainty on desktop and mobile + - Missing scenario marker blocks candidate acceptance + determinism_controls: + - Explicit SQLite fixtures and isolated demo transport + - Live API calls aborted during browser tests + anti_cheat_rationale: + prevents: + - Interpreting command success as a live release claim + - Leaking raw adapter references or arbitrary error text + - Retrying uncertain effects from a read-only dashboard + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Browser review exposed a genuine mobile section-heading overflow. Production +CSS now allows the title/status row to wrap; the strict document-width assertion +remains unchanged. No retry, sleep, hidden overflow or tolerance was added. + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-canonical-fetch + given: An installed project with a local canonical remote containing a new commit + when: Owner-confirmed object import encounters one transient failure then succeeds + then: Attempts are durable and bounded, exact objects arrive, refs and working tree remain unchanged, no remote publication or secret error persistence occurs + - id: SCN-canonical-fetch-boundaries + given: Authentication or unknown errors, invalid limits, non-owner callers and a changing canonical destination + when: CLI and service fetch paths are exercised + then: Missing approval or invalid callers fail, permanent errors do not retry, transient exhaustion stops at the limit, changed policy stops another attempt and read-only status reports results + observable_outcomes: + - Real local bare Git remote, exact object presence, unchanged refs and worktree + - SQLite attempt records and actual CLI JSON/status + - Missing scenario markers reject candidate acceptance + determinism_controls: + - Disposable local Git repositories; transport failures injected at the executor boundary + - No real remote or deployment exercised + anti_cheat_rationale: + prevents: + - Sharing fetch retry semantics with push or deploy authority + - Treating authentication failure as a transient network hiccup + - Updating branches through implicit fetch refmaps + - Persisting credential-bearing transport errors + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Fixture correction: local canonical creation correctly rejected an uncommitted +installation, so the fixture now commits installation metadata before that gate. +Production uses the existing narrow owner-audit API and adds only the named +`canonical.fetch` permission; no identity or audit assertions were relaxed. + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-console-task-activity + given: Old owner requests, blocked work, managed Git branches and missing history + when: Activity is observed through the Console snapshot and rendered on the work board + then: Owner requests sort first, metadata updates do not hide age, incomplete history remains unknown, blocked waiting is explicit, no task or Git mutation occurs + observable_outcomes: + - Real disposable Git history and read-only HTTP snapshot + - Shared renderer escapes text, bounds lists and preserves closure as intent + - Real browser demo shows owner-first activity review without live API calls + - Missing scenario marker rejects candidate acceptance + determinism_controls: + - Fixed Git dates, explicit observation clock and disposable repositories + - Isolated demo transport with all live API requests aborted + anti_cheat_rationale: + prevents: + - Equating missing history with neglected work + - Resetting inactivity through metadata edits + - Mutating backlog or treating Closes text as completion + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-console-check-evidence + given: Legacy and conditioned receipts, malformed frozen-input metadata and an unconfirmed running preparation + when: Read-only project evidence is projected and rendered in the actual Console + then: Invalid or absent evidence remains explicit, project isolation and text escaping hold, no database mutation occurs, private capture paths stay hidden + observable_outcomes: + - Project-scoped SQLite projection and shared escaped dashboard HTML + - Real browser demo displays captured input and before/after observations + - Missing scenario marker blocks candidate acceptance + determinism_controls: + - In-memory SQLite with explicit records and isolated demo transport + - All live API calls blocked during the browser scenario + anti_cheat_rationale: + prevents: + - Treating a running record as proof of a live executor + - Treating missing conditions as independently verified runtime truth + - Leaking capture paths or reading untrusted artifact paths + - Escaping legacy schema support through observation-side migrations + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Owner-briefing UI additions (2026-09-30): `SCN-console-owner-digest` tests +owner-first rendering, HTML escaping, ignored raw Markdown, evidence labels, +stale/future-clock/unpublished/unavailable/truncated states and no source mutation. +`SCN-demo-owner-digest` uses the existing isolated demo transport to prove that +an approval changes live sample state without rewriting the historical briefing +or another demo workspace. Both exact markers are candidate acceptance requirements +with missing-marker rejection. The real Playwright demo scenario additionally +checks actual briefing DOM, immutable publication after approval, desktop/mobile +screenshots, mobile anchor clearance above sticky navigation and expanded-evidence +width, while all live API requests are intercepted/aborted. Reduced-motion media +makes anchor-position assertions deterministic without sleeps. No previous +overflow assertion was weakened: two animation frames after viewport resize +allow responsive layout to settle before the existing strict width measurement. +No previous +assertion was relaxed; new fixtures use the actual TorchDemo factory and token +confirmation fields. Screenshots prompted the mobile anchor-spacing fix. + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-owner-digest + given: Owner and peer approval waits, an old owner request and implementation-only work + when: A read-only report is generated and explicitly published locally + then: Owner waits lead, implementation is not shipping, escaped Markdown and bounded Git coverage preserve truthful reporting, and latest publication survives reopening and CLI/API reads + - id: SCN-owner-digest-schedule + given: Owner-approved typed system coordination schedule and enabled digest policy + when: The action runs or publication policy is revoked + then: A local report is stored with no external command or AI wake and the existing scheduler respects revocation + - id: SCN-owner-digest-receipts + given: Seeded recorded receipts across old, current and future windows plus uncertain later deployment + when: Bounded reporting and publication encounter an audit failure + then: Window/count/truncation and recorded decisions remain explicit, uncertainty is not live success and publication rolls back + observable_outcomes: + - Immutable local report and atomic publication audit, unchanged authoritative backlog + - No outbound notification, provider wake or host timer installation + - Markdown escaping, read-only no-store API and method rejection + - Exact missing-scenario markers block candidate acceptance + determinism_controls: + - Disposable Git/worktree/database fixtures with fixed clock and recorded-history seeds + - Receipt seeds test reporting, not independent deployment or landing qualification + - No sleep, network provider or retry used to hide failures + anti_cheat_rationale: + prevents: + - Calling landed or implemented work shipped + - Inventing management decisions from undated documents + - Reporting partial history as proven neglected work + - Treating a local artifact as public publication or external delivery + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +New fixture corrections use the existing scheduler's canRun result and the +approval service's decidedBy argument. They do not weaken authority, revision, +policy-revocation or receipt assertions. The schema refinement now explicitly +permits only the owner-authorized typed owner-digest coordination action; it +does not permit arbitrary command actions to claim coordination behavior. + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-delivery-attempts + given: Approved delivery policy and an explicitly retry-safe adapter + when: Transient failures precede success or an executor throws with uncertain effects + then: Attempts persist before execution, approval remains mandatory, success advances once, unknown effects block repeats and owner recovery preserves uncertainty + - id: SCN-delivery-retry-boundaries + given: Bounded retry policy and independent database callers + when: Failures are permanent, safety is undeclared, the limit is reached or authority changes mid-operation + then: No extra external invocation occurs and changed policy is observed by a long-lived service + - id: SCN-delivery-succeeded-recovery + given: A durable successful adapter receipt and interrupted local application + when: The owner reviews and confirms a stopped executor after reopening the control plane + then: Exact saved success is applied once without an adapter, destination/authority drift blocks it and recovery-audit failure rolls back while preserving receipts + observable_outcomes: + - Persisted operation and attempt records, shared idempotency key, unchanged failed lifecycle state + - Independent control-plane connections refuse overlapping execution + - Reopened state, CLI attempts and owner-attested recovery, bounded console observation + - Existing lifecycle scenario still proves real Git/check/integration gates separately + - Legacy schema migration preserves receipts; parent-running/latest-success crash window is recoverable + - CLI preview/approval and replay produce no extra attempts or lifecycle events + determinism_controls: + - Disposable local repositories and deterministic in-process project adapter outcomes + - Direct fixture placement at release-ready only for retry scenarios, explicitly not release qualification + - No network calls, delays, host timers or live deployment + anti_cheat_rationale: + prevents: + - Retrying uncertain side effects or hiding failed attempts + - Advancing state after a failed adapter or bypassing owner approval + - Counting provider declarations as independently verified behavior + - Selecting latest fixture by ambiguous timestamps + - Redeploying already-successful work after a local failure + - Treating unknown outcomes as saved success or asserting provider receipt truth + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Landed-closure additions (2026-09-30): `SCN-landed-task-closure` uses real +disposable Git worktrees/check receipts and proves opt-in closure after the +exact canonical landing, no closure before landing, manager-only authority, +wrong-owner/wrong-commit/unknown-item refusal, idempotent replay and fresh +check-policy invalidation. `SCN-landed-closure-recovery` holds an actual task +lock while landing, verifies durable landed state survives closure failure, +revokes automatic policy, tests read-only CLI preview and missing-approval +refusal, then explicitly retries closure without another Git landing. +Identity-bound MCP refuses specialist closure and unapproved manager mutation. +The existing bootstrap scenario now also requires generated specialist prompts +to name task IDs, treat trailers as intent and confirm landed/completed state; +these are additive assertions, not proof that a live model follows the prompt. +Both exact scenario markers are now required by candidate acceptance, including +negative missing-marker tests. No assertions were relaxed. Initial fixture +filename corrected from nonexistent project.json to actual tracked torch.yaml; +production source-area lookup uses the actual config.domains schema. These +tests do not qualify a live fleet or independently verify model behavior. + +Commit-activity additions (2026-09-30): new tests only; no existing assertion +was removed or relaxed. Real disposable Git histories with fixed commit dates +exercise `SCN-backlog-commit-activity` (linked progress, owner-first neglect, +unchanged task state and clean working tree), `SCN-backlog-activity-coverage` +(bounded/missing history produces unknown, future commits cannot fake current +progress), and `SCN-commit-closure-intent` (only standalone trailers describe +intent, without executing text or completing tasks). Current-source candidate +acceptance requires all three exact scenario tokens; missing evidence fails +even when test commands exit green. Commit metadata is not implementation +proof. Automatic closure, host scheduling and live dashboard activity are not +claimed by these scenarios. + +```yaml +test_integrity_note: + change_type: new_tests + scenarios: + - id: SCN-backlog-self-claim + given: Default-disabled policy and explicitly routed ready work + when: The owner permits one specialist and it resolves work + then: Priority-ordered claims are audited to the specialist and existing work resumes even after revocation + - id: SCN-backlog-self-claim-readiness + given: Dirty work, guards, prepared checks, resource waits, approvals or integration + when: The specialist requests its next assignment + then: Work remains ready and concrete blockers are returned + - id: SCN-backlog-self-claim-race + given: Two local specialist processes and two ready tasks + when: Both request work concurrently + then: Exactly one task is assigned and the other call resumes or fails closed on contention + - id: SCN-cli-backlog-self-claim + given: Approved policy and a bound specialist + when: CLI previews or confirms and MCP attempts another identity + then: Preview preserves task revision, confirmation claims once, identity spoofing is rejected + - id: SCN-backlog-blocked-resume-serialization + given: One blocked task and a different active task owned by the same specialist + when: The blocked task is resumed + then: It remains blocked until the other active assignment ends + observable_outcomes: + - Tracked task JSON, revisions, state and specialist-attributed history/audit + - Actual concurrent child processes, CLI JSON and identity-bound MCP responses + - No assignment-only message generated for a self-claim + - Exact marker verification rejects missing scenarios even when longer IDs share their prefix + determinism_controls: + - Disposable Git repositories and explicit persisted policy changes + - Real project-scoped locks; lock contention is a fail-closed outcome, not a retry + anti_cheat_rationale: + prevents: + - Bypassing assignment authority via a public selfClaim field + - Granting work outside explicit routing or with incomplete dependencies + - Claiming a second task after restart or blocked-task reactivation + - Reading policy only once before the owner revokes it + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` diff --git a/docs/TODO.md b/docs/TODO.md index 7ac11b7..a16e3a3 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,9 +1,499 @@ # TORCH remaining work +## Review corrections (2026-09-29) + +## Fresh readiness audit (2026-09-30) + +Self-host ownership follow-up: see +`reports/audit/self-host-roster-review-2026-09-30.md`. A proposed six-specialist +grouping covers 124 current implementation/assets/harness/test files with zero +missing/duplicate primary mappings. A pending six-domain JSON proposal now passes +the real Architect CLI validator with zero unassigned analyzer components: +`/tmp/torch-selfhost-six-domain-proposal-20260930-ownership.json` (validated at +that source checkpoint; subsequent bootstrap changes make its binding stale). +This is not approved or commit-bound; root metadata/schema ownership review and +a fresh post-checkpoint intake remain required. The +local source checkpoint decision has been requested; no install or launch occurred. + +See `reports/audit/readiness-audit-2026-09-30.md` for current specification +exit gates and evidence boundaries. Latest intake contains 15 candidate domains, +13 unassigned components and 41 coordination boundaries; it remains pending, +not approved for installation. The older self-host report's 10/18/24 counts +are historical, as is the earlier 14/14/39 intake. Latest preview is +`/tmp/torch-selfhost-intake-20260930-ownership.json`; regenerate after the source +checkpoint. `main` still points to legacy TORCH; the rewrite is uncommitted, +candidate planning correctly refuses it, and no stable version/launcher exists. +Prioritize frozen queued-browser/same-runtime qualification and stale-work +manager review, then a scoped source checkpoint and reviewed TORCH roster +before a separately authorized bounded live trial. + +## COMBATRIG operational lessons (2026-09-30) + +- [x] Generate collision-free baseline rosters for test-only, release-only and + combined repositories. Catch-all Core fallback is used only if no evidenced + domain exists, preserving empty-project startup without inventing a conflicting + owner. `SCN-bootstrap-specialist-only` is required by candidate acceptance. + +- [x] Reject provable duplicate primary ownership at both pending Architect and + approved-install boundaries. Exact normalized paths, universal ownership and + literal recursive-parent claims fail closed, including planned files; shared + consultation is allowed. Arbitrary glob intersections remain outside this check. + `SCN-bootstrap-primary-ownership` is required by candidate acceptance. + +- [x] Add optional frozen check inputs with project-declared build provenance, + reflink/independent-copy isolation, durable preparation and exact-SHA + receipts. CLI/MCP prepare/list/run/cancel paths integrate with explicit FIFO + resource acquisition. Specialist rebuilds do not mutate captured inputs; + tampering/policy changes/executor failures cannot count as passes. +- [ ] Qualify a real queued browser test serving its frozen build, including + project harness paths/dependencies, disk use and interrupted-run recovery. + Real Chromium capture/rebuild/queued-run qualification now PASS: the browser + serves captured HTML after an in-place live rebuild, reopens prepared state, + credits only the captured SHA and rejects the newer build with the unchanged + harness. Explicit external harness/module resolution is exercised. Disk use, + other-project interruption and project-specific qualification remain unfinished; see + `reports/audit/frozen-browser-qualification-2026-09-30.md`. +- [x] Add explicit owner-only abandoned-check recovery with persisted runner + identity, conservative live-runner rejection, stopped-executor/descendant + attestation and audit. Terminal recovery creates no receipt and releases no + lease. Owned-snapshot cleanup is replayable; ownership mismatch preserves it. + Real Linux runner/measurement SIGKILL qualification and cleanup replay PASS. +- [x] Qualify one real Linux Chromium harness surviving TORCH runner death, + then explicit owned browser/harness shutdown before owner recovery. Pre-opened + pidfds confirm the captured isolated browser group and harness terminate; live + browser status after runner death proves those are different events. +- [ ] Extend interrupted-check qualification to other platforms and arbitrary + project harnesses, including descendants escaping captured process groups. + Linux process metadata observes only the runner, not descendant termination. +- [x] Persist prepared-check cleanup outcomes and surface abandoned checks in the + actual Console/shared demo. Completed, pending and unconfirmed states remain + distinct; private paths, owner evidence and process IDs are omitted. No recovery + buttons or implied descendant liveness proof. Crash-before-outcome persistence + remains conservatively pending; replay updates cleanup without duplicate audit. +- [x] Add project-configurable test-condition receipts and prerequisites + (build identity, switches, acceleration, simulation clock, visibility for + COMBATRIG; other projects define their own conditions). +- [ ] Qualify condition probes with a real browser harness observing the same + runtime that it measures, including drift detection through the measurement + interval. Current receipts enforce project-reported pre/post contracts, not + independent sensor truth or continuous monitoring. + Real persistent Chromium qualification now PASS for healthy, preflight-frozen, + mid-measurement-frozen and replaced-runtime cases. Preflight, measurement and + postflight contact the same page; invalid conditions/replacement cannot count + as exact passes. Optional reported runtime identity is now continuity-checked. + Transient drift that recovers between probes, actual GPU/game conditions and + project-specific harness qualification remain unfinished. +- [x] Pass captured commit/input digest to the measurement command itself, not + just condition probes. Real browser qualification exposed the missing wiring; + conditioned and unconditional child-command regressions now enforce it. +- [x] Require frozen receipt linkage to a finished prepared operation before + exact-pass qualification; orphaned pass output after interruption is insufficient. +- [x] Surface condition/snapshot receipt details and interrupted prepared checks + in the dashboard rather than requiring CLI/MCP inspection. + Read-only, bounded, project-scoped projection supports legacy databases; + running records are explicitly unconfirmed, not assumed live or auto-restarted. + +Condition-receipt verification: thirteen focused condition/resource/self-host +tests PASS; final current-source acceptance PASS (38 groups, test/lint/syntax). +Probe observations are project-reported; malformed/missing/mismatched preflight +prevents measurements, invalid postflight makes green output incomplete. +Long-running check services reload canonical policy before planning/execution +and qualification. No live browser/GPU/provider or host timer was started. +- [x] Add bounded commit-linked task activity and neglected owner-request + detection, with owner requests first and incomplete coverage explicitly unknown. +- [x] Wire landed `Closes:` intent into idempotent task completion only after + matching current-policy verification, ownership, task state and exact landing + evidence. Automatic closure is opt-in; read-only activity still only reports + intent. Manager preview/confirmed reconciliation supports interruption/lock + recovery without a second landing. Invalid items stay open. +- [ ] Qualify real specialist trailer conventions and live landing/closure + recovery before enabling automatic closure in an installed fleet. + +Landed-closure verification: seventeen focused integration/MCP/self-host tests +PASS; final source acceptance PASS (38 groups, test/lint/syntax), including +generated specialist instructions. Receipt: +`/tmp/torch-landed-closure-final-20260930-acceptance.json`. Actual disposable Git +landings prove closure/replay and locked-task failure recovery; default remains +disabled. Source is uncommitted and not installed/live-qualified. +- [x] Expose commit activity/stale coverage in the actual dashboard and isolated + demo, with owner-request attention links, blocked-wait context and unknown + coverage kept distinct from proven inactivity. +- [x] Add configurable daily manager review of stale work via optional + `manager-check-in.action.stale_work` bounds and existing approved scheduling. + Owner requests first; direct-report tasks plus Fleet Operations' unassigned + intake only; incomplete history stays unknown; blocked work is not restarted. + Durable review deduplicates separately from routine check-ins and requires a + fresh read-only MCP query before decisions. No task mutation or host timer is + implied by observation or inbox delivery. +- [ ] Qualify an owner-approved installed daily stale-review cadence and manager + follow-through with live specialists; local scheduler and durable delivery are + tested, not real AI judgment or timer installation. +- [x] Add non-disruptive 15-second live dashboard refresh with pause/resume, + visible-tab-only polling and bounded reads. Edited/focused forms and active + previews retain their existing DOM while unrelated evidence updates; the view + explicitly labels retained panels as potentially older. Successful actions + release only their own editor; background reads are single-flight and explicit + newer requests supersede older responses. Failed reads retain displayed evidence. +- [ ] Qualify live installed-fleet refresh against real provider activity and + native browser background/back-cache behavior. Controlled browser clock and + visibility/page lifecycle events are verified locally without starting a fleet. + +Activity verification: eighteen focused activity/backlog/MCP tests PASS; +current-source acceptance PASS (38 required groups, test/lint/syntax). Commit +messages remain untrusted intent, not completion authority. Missing/truncated +managed history produces unknown absence of activity. Receipt: +`/tmp/torch-task-activity-20260930-acceptance.json`. + +- [x] Persist delivery failures/attempt receipts and add bounded classified + retries for explicitly safe delivery adapters without broadening approval. + Per-delivery unresolved reservations prevent overlapping effects; unknown + outcomes stop. Owner-attested not-applied recovery retains original evidence. +- [x] Complete recovery for externally succeeded operations whose local + lifecycle update was interrupted; preserve the receipt and do not re-execute. + Owner-approved executor-stopped preview/reconciliation checks exact state, + destination hash and current authority; repeat application is harmless. +- [ ] Qualify real adapter receipt identity and stopped-executor confirmations + for success/not-applied recovery; project-reported success is not live proof. + +Success-recovery verification: nine focused delivery/self-host scenarios PASS; +all 38 current-source acceptance groups PASS (test/lint/syntax). Receipt: +`/tmp/torch-delivery-success-recovery-20260930-acceptance.json`. Injected local +failures and reopening prove zero re-execution, rollback, legacy preservation, +destination/authority checks and idempotent replay. Not installed/live-qualified. +- [ ] Qualify real release/deploy adapters, provider-side idempotency and network + classifications; adapters currently supplied through the service interface + are not automatically installed or invoked by CLI configuration alone. +- [x] Add bounded retries for safe canonical object fetch separately from + release/deploy authority. Owner-confirmed CLI defaults to three attempts + (one to five), retries only recognized transport failures, imports a resolved + canonical SHA without moving refs/FETCH_HEAD and persists attempt receipts. + Destination/policy changes stop subsequent attempts; raw transport errors + and remote URLs are not persisted. Read-only CLI status/snapshot expose results. +- [x] Surface latest canonical fetch and delivery attempt outcome details in + the actual dashboard and shared demo. Project-scoped bounded receipts show + classifications/effect uncertainty, with unconfirmed-running and adapter-success + caveats, no retry/recovery buttons and no raw references or transport errors. +- [ ] Qualify real remote transport classification before relying on it + operationally. Initial canonical head discovery fails closed, not retried. + +Delivery-attempt verification: three focused scenarios PASS (original real +lifecycle, durable retry/uncertainty/recovery, safety/limit/policy boundaries); +current-source acceptance PASS (38 groups, test/lint/syntax). Receipt: +`/tmp/torch-delivery-attempts-20260930-acceptance.json`. Actual provider behavior +and receipt correctness remain project-adapter claims, not live qualification. +- [x] Add configurable owner-first local digest of recorded shipping, structured + approval decisions, blockers, owner approvals and neglected requests without + another task queue. Read-only Markdown/JSON generation, approved durable + publication, latest CLI/API view and typed owner-authorized scheduling are + implemented; default automatic publication is disabled. +- [x] Add the readable digest dashboard view/navigation to the actual Console + and the shared isolated demo, with owner-first sections, recorded-versus-live + labels, stale/unpublished/unknown/truncated states and safe text rendering. +- [ ] Add explicit opt-in external notification adapters; no public publication or message delivery is + implied by local publication. Qualify an installed daily cadence only after + owner approval. Unstructured manager decision documents need time-indexing + before the digest can honestly claim complete coverage of management calls. + +Digest verification: eight focused digest/self-host scenarios PASS; final +current-source acceptance PASS (38 groups, test/lint/syntax). Receipt: +`/tmp/torch-owner-digest-final-20260930-acceptance.json`. Local loopback verifies +stable GET-only no-store retrieval; no real timer, AI wake, external send, +public publication, stable install or fleet activation occurred. + +Dashboard briefing follow-up: renderer/demo regression tests PASS and all 38 +current-source acceptance groups PASS; receipt +`/tmp/torch-owner-briefing-final-20260930-acceptance.json`. Real Playwright desktop +and mobile demo checks PASS with zero live API calls, including sticky-navigation +anchor clearance and expanded evidence width. Screenshots are saved under +`reports/design-system/torch-dashboard-owner-briefing-{desktop,mobile}.png`. +- [x] Add atomic self-claim under approved fleet policy and exception-only + specialist escalation. Preserve resume-first/single-active assignment. +- [ ] Qualify the autonomous settle/claim loop with live specialists under + an approved policy. Claim readiness is mechanically checked; model compliance + with exception-only communication and fresh per-item instructions still + requires a live trial. + +Self-claim verification: twenty focused backlog/MCP/self-host tests PASS; +final current-source acceptance PASS (38 groups, tests/lint/syntax). Real +competing local processes preserve one active assignment. Blocked-to-active +reactivation uses the same assignment lock/busy rule; priority selection now +honors the existing owner-controlled priority field. Acceptance scenario IDs +are matched as exact tokens, not substrings. No live fleet, timer or provider +was launched; source remains uncommitted and stable installation is still gated. +- [x] Add bounded dashboard auto-refresh with visible freshness/error state. + Completed by the protected 15-second refresh checkpoint above; do not reopen + this duplicate implementation task. Live installed-fleet/native browser + qualification remains a separate unfinished gate. + +Verification: frozen-input/resource/CLI/MCP scenarios PASS (11 focused tests); +full current-source candidate acceptance PASS (38 groups, test/lint/syntax). +The strengthened missing-evidence gate PASS (five focused self-host tests). +No real browser/GPU workload, provider or timer was launched. Owned disposable +snapshot copies were removed by the scenarios; source build files were retained. + +## Review follow-up + +First-release trial correction: specification section 32 requires at least two +domain managers to coordinate one real task, not merely one manager supervising +two specialists. Review/activate the proposed lead graph separately from the +pending roster, then record actual coordination evidence. A smaller rehearsal +does not complete the release gate. Source checkpoint approval remains pending; +generation-zero/no-launcher status and dirty-source rejection were revalidated. + +2026-09-30 self-host readiness review: current candidate planning correctly +rejects the dirty checkout; the real version store and stable launcher remain +empty. Fresh no-provider intake finds ten candidate domains, 24 coordination +boundaries, four project checks, and 18 unassigned components requiring roster +review. These are not approved agent assignments. The ordered clean-checkpoint, +commit-bound candidate, roster, stopped install, and bounded live-trial path is +in `reports/audit/self-host-readiness-2026-09-30.md`. No agent or timer was started. + +- The public demo now runs the actual Console at `/console.html?demo=1`, + with tab-local sample state and working preview/confirm flows. Its link is + `View dashboard demo`. Chromium verifies opening a separate tab, queued work, + priority changes, messages, feedback, runtime profiles, approval and + organization review, reset, mobile layout, and zero live API calls. +- Generated domain roles inherit the install runtime. Codex-only and mixed + installations are tested; `--default-runtime` controls inheritance without + replacing explicit per-role assignments. Architect validation accepts Pi + and custom adapter identifiers while installation checks adapter trust and + availability. +- Domain creation and merge/split proposals now inherit the installed default + when runtime is omitted or `default`; a Codex-only Fleet no longer tries to + introduce Claude for a newly proposed specialist. +- The work board now groups actual `ready` tasks in Queue. The prior `open` + grouping did not match the backlog state model. +- [x] Resolve manager wake policy for subscription/local runtimes. Explicit + `invocation-count` mode now permits configured built-in adapters without + claiming dollar enforcement; USD-hard-cap mode retains its exact receipt + requirements. Shared UTC invocation budgets, pending-message recovery, + duplicate suppression, and overlapping-wake reservations are tested. +- [ ] Qualify manager wake behavior with real configured providers after the + fleet is ready and launch is authorized. Current evidence uses real adapter + planning with simulated invocation, not live provider sessions. +- [x] Add CLI inspection and owner-attested recovery of interrupted wake + reservations. Recovery requires an offline manager, explicit stopped-runtime + confirmation and evidence note; it is atomic/audited and retains budget and + message replay guards. No provider is launched or message acknowledged. + The read-only Console now exposes unknown launches in owner attention and + Operations with reservation/message provenance. Following the warning opens + the collapsed panel; observation never releases a launch lock. +- [ ] Qualify interrupted-wake recovery against live runtime lifecycle evidence; + owner attestation is explicit but is not independent proof of termination. +- [ ] Qualify the updated source as a clean commit before self-host activation; + older artifact receipts predate these changes. No fleet or timer has started. + +Review verification (2026-09-30): full regression suite through isolated +candidate acceptance PASS; `npm run test:dashboard` PASS; +lint, syntax and diff hygiene PASS. The existing preview at +`http://127.0.0.1:4173/console.html?demo=1` serves the shared Console demo. +Details: `reports/audit/product-alignment-review-2026-09-29.md`. + +2026-09-30 self-host gate follow-up: candidate acceptance now explicitly +requires provider-inheritance, count-mode wake, overlap reservation, wake-policy +validation and interrupted-wake recovery evidence. Missing any one marker +rejects acceptance even with green command exits. The updated current source +passes all 38 acceptance groups using disposable HOME/TMPDIR/XDG state, +including the full regression suite, lint and syntax checks. This is a source +gate run, not a new installed candidate receipt or clean-commit qualification. +Read-only live candidate status still reports no installed version, active +version or stable launcher. Legacy branches remain present. User-owned dirty +changes and the protected untracked fixture remain untouched. + This is the restart checklist for `rewrite/portable-agent-fleet`. The detailed state and evidence are in `reports/audit/completion-audit-report-2026-09-27.md`. +## Current snapshot (2026-09-29) + +This snapshot supersedes older counts and artifact paths further below. The +latest isolated artifact-mode self-host candidate passed all 38 required +acceptance scenarios plus `npm test`, lint, and syntax checks. Candidate +acceptance now creates and removes a disposable HOME/TMPDIR/XDG environment so +validation cannot share the active version store; the npm tarball bundles its +pinned production dependencies so its offline-consumer gate does not depend on +a warmed global cache. This build is non-commit-bound and isolated under +`/tmp/torch-selfhost-final.rD27ls`; it is installed there only, with no active +version or launcher. The exact artifact digest is +`521a2d6af0931bc9461ad706f9809bda2c9f84cee92f6d62dc83dc306207a5f8`. The latest read-only self-host design +intake is +`/tmp/torch-self-bootstrap-current.tmVj5s/architect-brief.json`, bound to +HEAD `d31437a4fb255c1e307bbb70ee9513cc1e0bdf4c` and working-tree +fingerprint `5b97b6850526da2f6ca01f6eefd9ea7ec7e57e2afe9247086cb670c5d4b0f7dc`. It +proposes nine code/QA domains, records 19 collision candidates for review, and +identifies three repository-wide checks (`test`, `check`, `lint`). This +proposal is pending and unapproved. Specialists +now receive these project integration gates separately from domain-specific +checks, without inferred gate ownership. The existing untracked +`test-torch-config-host-uQKdqL/` fixture appears among shared-surface candidates; +it remains untouched and the proposed roster is not approved or installed. +The Console Flow watch now allows preview-confirmed owner decisions only for +requests named to the owner; requests assigned to a manager/peer remain +read-only. Confirmation is same-origin, revision-bound, audited, and replay +idempotent (`SCN-console-owner-approval`). Re-run intake after further code or +documentation changes before owner review. No TORCH runtime, timer, or provider +was started. The pre-existing untracked fixture remains untouched and was +excluded from the candidate source copy. + +2026-09-29 follow-up: manager scheduled wakes now require both the shared daily +invocation count and an adapter-enforced positive per-invocation USD cap for +the exact create/resume mode. The cap receipt is validated before count +reservation; built-in Claude, Codex, and Pi modes all fail closed and leave the +check-in queued. The landing page now links to a separate, noindex interactive +dashboard demo at `/demo.html` and opens it in a new browser tab; it has no +project/provider connection. +Current working-tree verification: `npm test` 128/128 PASS, all 38 candidate +acceptance scenarios PASS, lint/check and `git diff --check` PASS. This still +does not qualify a committed candidate: live `candidate status` shows no +installed/active version or launcher, and `candidate plan --source` is blocked +by modified tracked files. No fleet, provider, or system timer was started. + +2026-09-30 UTC: an exact mirror of the latest working tree, treated explicitly +as a prepared non-Git artifact and excluding `.git`, local Torch state, reports, +and the protected fixture, passed isolated candidate staging and acceptance. +Digest `f62082b3ea5424607dd7ff40790d7c9a9c67ab7141507338c6788bf55f0d00fc`; +receipt `/tmp/torch-selfhost-tree-PuFzM2/state/torch/runtime/versions/0.1.0-alpha.0/.torch-validation.json`. +The isolated store reports the version installed but `activeVersion: null` and +no launcher. This strengthens artifact-mode evidence only; source commit is +null, the checkout still needs a reviewed clean commit for commit-bound +qualification, and no project Fleet/provider/timer was started. + +The Fleet dashboard now lets the owner review and change one identity's +next-launch runtime/model/reasoning profile. Its same-origin preview calls the +existing CLI validation path, clears old adapter-specific policy on provider +switch, rechecks the exact config digest at confirmation, and records a +`profile.change` audit event. It does not reconfigure an active session or +start a runtime (`SCN-console-runtime-profile`). + +The Console now also offers preview-confirmed setup or refresh of the persistent +schedule timer. Review shows the configured system schedules (including +mutating actions and provider-wake policy), exact owned unit paths/content +digests, and the schedule configuration digest. Same-origin, five-minute, +single-use confirmation rechecks the plan, audits the owner action, then calls +the unit manager; observation reports file ownership/digest without claiming +the timer is live. The tests use only a fake systemd executor; no host timer was +installed (`SCN-console-schedule-launcher`). + +Manager check-in wake messages now carry a timestamped, explicitly staleable +snapshot and require a fresh read before the manager acts. The +identity-bound, read-only `torch_plan_manager_check_in` MCP tool recomputes +direct-report presence and structured approval waits from current durable state, +including approvals created after a deduplicated wake was queued; specialists +cannot use it to inspect manager reports. `SCN-manager-check-in-refresh` is +required candidate-acceptance evidence. No manager is automatically granted +another identity's approval authority. + +Session Architect bootstrap now requires an evidence-backed flat-versus- +coordination assessment. Any proposed lead must have a concrete integrated +outcome, bounded decision scope and owner escalations; the contract separately +states implementation, coordination, fleet-operations, priority, review, and +owner-only authority while preserving direct peer communication. Validation +rejects unresolved shapes, unknown evidence, fabricated domains, code-ownership +or owner-approval grants, and manager-only routing in the architect assessment +scenario. No provider or organization activation was involved. + +Integration now has a durable FIFO drain over authorized requests via +`torch integrate drain --area --yes`. Each fast-forward remains +serialized and revalidated; after one lands, stale sibling tips are recorded as +`needs_convergence` until their owner merges the latest main and reruns exact +checks. This prevents concurrent landing races without relaxing check +freshness. The Console exposes pending queue order, authorization, and wait +reason read-only. A typed `integration-drain` action can now be added as an +owner-authorized, at-least-minute system schedule. It only drains requests +already authorized by the configured landing authority and reuses the same +exact-check/FIFO/lock path; a test proves an unauthorized request stays put and +a fresh authorized request lands. Defining the schedule does not install or +start its exact-config systemd timer. Remote publishing is now a separate, +owner-confirmed `torch forge sync` operation: it fast-forwards the configured +remote to an already-landed local canonical commit, never force-pushes, blocks +remote-ahead/diverged history, and writes an owner audit event. The +`SCN-forge-sync` test passes against temporary local bare remotes. No TORCH +runtime, timer, provider, or hosted remote was started. Remaining COMBATRIG +parity differences are deliberate: TORCH does not automatically merge latest +main into each candidate or run a batch of quick merged-tree checks in the +lander. Source owners converge and record exact-commit check receipts before +integration; remote publication remains separate. + +Self-host candidate planning now lists non-ignored untracked Git paths that +will be excluded from the exact-commit artifact; ignored local data remains +unlisted and excluded. `SCN-candidate-untracked-visibility` verifies the plan +and staged artifact agree while tracked modifications still block candidate +staging. The isolated-candidate tests do not install the current dirty source +tree into user-local state. + +Latest isolated artifact qualification (2026-09-29): candidate version +`0.1.0-alpha.0`, digest +`861bc58f94ecd7f8396a0cf1f5822a53ac3cd0eb2f2d576a3344fb20b7ea3aec`, passed +all 36 required scenarios plus `npm test`, lint, and syntax checks. Its receipt +is `/tmp/torch-hierarchy-candidate-de5hgZ/data/torch/runtime/versions/0.1.0-alpha.0/.torch-validation.json`. +The staged artifact omitted `.git`, `.torch`, `.claude`, `.scheduler-memory`, +`reports`, coverage, and the protected untracked fixture; the local dependency +tree was copied separately for offline gates. It is a +working-tree artifact (`source.commit: null`), not a clean Git-commit candidate. +Version state and launcher paths were redirected to `/tmp`; the active pointer +and stable launcher are absent. No persistent install, activation, runtime, +timer, or provider was started. Commit-bound candidate qualification remains +open until a clean, reviewed source commit exists. + +An earlier isolated artifact-mode snapshot passed the actual candidate acceptance +runner: version `0.1.0-alpha.0`, digest +`eee55b9a5af9ce83e70df6e5e718fc202e2f7e26e2a0a1cd5c7e38b20b10c11a`, all 32 +required scenarios, `npm test`, lint, and syntax checks. Its validation receipt +is `/tmp/torch-candidate-qualification-mGmYqa/state/torch/runtime/versions/0.1.0-alpha.0/.torch-validation.json`. +This was an explicitly filtered snapshot (`.git`, `.torch`, `.claude`, +`.scheduler-memory`, `reports`, and the protected untracked fixture excluded), +installed only in `/tmp`; verification found no active pointer or stable +launcher. It is not a clean Git-commit candidate or persistent installation. +That older receipt predates the candidate failure-diagnostics change and does +not qualify the latest tree; the fresh isolated artifact qualification above +supersedes it. `SCN-candidate-failure-diagnostics` verifies bounded +stdout/stderr excerpts on failed acceptance while successful logs remain +summarized. Commit-bound qualification against a clean Git candidate is still +required before stable self-host installation. + +2026-09-29 follow-up: the candidate acceptance runner was also invoked directly +against the current working tree. It passed all 36 named scenario markers plus +`npm test`, lint, and syntax checks. This proves the current checkout's gates +and evidence mapping, but it did not stage a versioned candidate artifact; the +isolated artifact qualification above provides that separate evidence. +The first attempt inside the sandbox could not run the fixture-spawning tests; +the successful run was repeated outside the sandbox without installing or +activating TORCH. + +2026-09-29 follow-up: after adding the organization lifecycle order preview, +the current dirty working tree again passed all 38 named candidate acceptance +scenarios, `npm test`, lint, and syntax checks through the isolated acceptance +runner. This is current-tree evidence only; it did not stage a versioned +candidate or authorize installation, activation, runtime launch, provider use, +or timer installation. + +Read-only COMBATRIG compatibility audit (2026-09-28) confirms the importer +remains correctly blocked at source HEAD `7a543fe2a658a474f7598efec7a516172895f5fd` +and working-tree fingerprint `fd399be1b70e8ec30ca0f85faf14c7c0218c544dfe1e0dd59a1a7f89926b4c1d`. +The 33 roster entries have 33 missing `owned_paths` declarations and 23 +missing explicit `not_scope` declarations (56 blocking gaps total). Of 33 +roster worktree references, 1 currently matches, 1 points at a mismatched +branch, and 31 have no matching live worktree; do not reconcile these +automatically because active/external work may be intentionally absent. The +legacy backlog contains 570 items (148 assigned, 95 open, 5 blocked, 322 +done), and the package exposes 461 test/build/QA scripts; the import only +selects `test:sessions`, `build`, and `test`, so check mapping still needs +human qualification. The report is +`/tmp/torch-combatrig-readonly-escalated-20260928.json`. COMBATRIG remained +unchanged (`main` behind `origin/main` by 4, pre-existing untracked `.codex/`); +no worktrees, tests, gates, or migration actions were run there. + +The importer now exposes an assessment-only backlog migration view, refreshed +at `/tmp/torch-combatrig-backlog-assessment-20260928.json`. Current evidence: +12 items reference four area IDs absent from the roster (`ai-contracts` 3, +`contracts` 1, `music` 7, `roadway` 1); 22 items are in the legacy +`ai-sessions` queue; all 148 assigned items carry free-form `assignedTo` labels; +322 legacy `done` items cannot be called Torch `completed` without commit, +evidence, and a landed integration request; and `observedAt` is absent on 125 +items or malformed on 4. The assessment creates no Torch tasks and does not +infer identities. Existing COMBATRIG work remains the source of truth until a +separately reviewed queue migration. + ## Next safe implementation task - [x] Repair the Codex adapter for `codex-cli 0.157.1`: @@ -26,20 +516,375 @@ state and evidence are in `npm run check`, and `git diff --check`. - [x] Refresh the completion audit with the new exact commit and evidence. -Completed in `d8ab841`; focused scenarios passed 8/8 and the complete suite -passed 61/61 with lint, syntax checks, and diff hygiene also passing. +Historical adapter repair completed in `d8ab841`; its focused scenarios passed +8/8 and that tree's suite passed 61/61 with lint, syntax checks, and diff +hygiene also passing. + +## New coordination reliability work + +- [x] Project the approved hierarchy's executable start and wind-down order in + the owner Console. Snapshot data now shares the same topological ordering + helper as runtime lifecycle plans, includes direct manager dependencies and + graph blockers, and is explicitly read-only. The responsive view lists the + exact startup order and its reverse shutdown order without exposing launch + controls (`SCN-console-readonly`, `SCN-hierarchy-aware-start-stop`). +- [x] Qualify serialized canonical landing. The landing path + now holds a project SQLite `BEGIN IMMEDIATE` transaction across exact-plan + revalidation, canonical fast-forward, and durable result recording; competing + processes fail closed with `INTEGRATION_LANDING_BUSY`. `SCN-integration- + landing-serialization` uses independent database handles to prove the lock + boundary. Revised focused/full gates pass and prompt tests verify the + single integration path. TORCH + cannot stop users or agents with independent forge credentials from bypassing + its local queue; hosted branch protection or credential isolation is optional + defense in depth. +- [x] Qualify current instructions on resume. Start and resume + plans now rebuild digest-addressed prompts from canonical project files; + Claude/Pi get current prompt-file flags and Codex gets a current instruction + envelope in the resume turn. `torch brief` uses the same composer. The new + regression scenario mutates canonical prompts and leaves stale worktree copies + behind. Revised runtime scenarios pass; live provider resume remains a + separate owner-authorized qualification gate. +- [x] Include active organization roles, authority, reporting lines, direct + reports, coordination links, and implementation ownership in each identity's + digest-addressed start/resume bundle and `torch brief`. The current graph is + rebuilt from canonical project config; resumed manager prompts cannot rely on + stale worktree copies. `SCN-current-instructions-on-resume` now changes the + graph and verifies updated context reaches the prompt without starting a + runtime during planning. +- [ ] Complete hierarchy-aware manager check-in timers. Implemented now: + durable structured approval requests name one owner/Fleet approver; requests + notify AI approvers; only the named approver can decide; decisions are + revision-checked, audited, and durably returned; owners decide through an + explicit CLI confirmation; direct-report check-ins classify waits as manager, + peer AI, or owner without taking over another approver's decision. Also + implemented: read-only organization-aware check-in plans, durable deduplicated + queueing, typed coordination schedule dispatch, a default 15-minute Session + Manager schedule in proposals, and scenario coverage. Manager schedules are + now derived for every reporting identity; hierarchy pilot plans show missing + additions and obsolete candidates while preserving existing cadence and + leaving activation to a distinct owner-confirmed CLI step. Owner hierarchy previews surface this timer + delta and explicitly mark timer installation unverified. The owner can now + inspect and explicitly reconcile an installed exact-config launcher with + `torch schedules launcher plan-reconcile` / `reconcile --yes`; it verifies + unit ownership, refreshes the digest-bearing service, updates the manifest, + and rolls back on daemon-reload failure (`SCN-system-schedule-launcher`). + Hand-edited interval schedules enforce the same 60-second minimum as generated + schedules (`SCN-manager-check-in-cadence-floor`). An opt-in adapter-mediated + wake is wired behind a shared project-wide daily invocation ceiling and skips + active managers. Owner-confirmed `hierarchy-activate` now commits the + approved organization graph, derived manager schedules, manifest hashes, and + pilot record; it never changes systemd state or starts identities. When a + launcher is installed, its digest becomes stale and the owner must separately + reconcile it. Check-in wakes now carry a timestamped, potentially stale + snapshot; before acting, a manager refreshes direct-report state and named + approval waits through the identity-bound, read-only + `torch_plan_manager_check_in` MCP tool. Specialists without direct reports + cannot use it to inspect manager state, and it grants no approval authority + (`SCN-manager-check-in-refresh`). Start/resume instruction bundles now + derive manager duties from the active reporting graph, direct managers to + refresh check-ins and route named approvals, and omit those duties from + specialist-only bundles. `SCN-hierarchy-aware-start-stop` covers promoted + middle-manager prompt behavior. Adapter contracts now declare hard-cap + support separately for create/resume modes; scheduled wakes require a + positive per-invocation USD cap, preflight an exact enforced-cap receipt, + fail closed before invocation-count reservation when unsupported or + mismatched, and reuse the prepared adapter plan. Tests prove the built-in + Claude, Codex, and Pi adapters cannot currently wake managers because none + declares an enforceable persistent-session cap (`SCN-runtime-cost-ceiling`, + `SCN-manager-check-in-cost-ceiling`). The project-wide daily invocation + ceiling remains an independent safeguard, not a dollar ceiling. Still needed: + verify a genuine runtime/provider hard-cap mode and end-to-end timer + qualification; telemetry or estimated price alone does not qualify. The Console + Flow watch shows manager/report state, named approval waits, and configured + cadence without implying that a timer is installed. Owner-addressed approval + controls use the existing audited, named-approver decision path; + manager-addressed requests cannot be decided there. Persistent timer + installation and provider wake remain separate owner-authorized gates. +Latest current-tree reliability validation (2026-09-29): `npm test` **125/125 +PASS**, `npm run lint` **PASS**, `npm run check` **PASS**, and `git diff --check` +**PASS**. The manager refresh scenario verifies that a request created after a +deduplicated wake remains visible through current-state refresh, and that a +specialist cannot inspect manager reports. The hierarchy reconsideration scenario +verifies revisioned evidence for one open proposal, refusal of same-window edits, +fresh proposals after terminal decisions, and exact-retry idempotence. Direct +`runCandidateAcceptance` on the current checkout passed **38/38** required +scenario markers plus test, lint, and syntax gates. This is a working-tree check, +not a staged or commit-bound +candidate artifact. Tests use temporary Git fixtures and fake runtime/systemd +executors. Lifecycle tests now prove hierarchy-derived parent-first startup, +bottom-up shutdown/status routing, and fail-closed identity-cycle handling. No +provider, Fleet session, host timer/service, or COMBATRIG mutation occurred. +Pilot measurement/adoption/reversal, enforceable provider/runtime cost ceilings, +live timer qualification, and commit-bound self-host qualification remain open. + +Latest local verification before the profile-CLI addition (2026-09-28): +`npm test` **75/75 PASS**, +`npm run lint` **PASS**, `npm run check` **PASS**, and `git diff --check` +**PASS**. TORCH's read-only analyzer and no-provider bootstrap completed against +this TORCH checkout and portable-fleet specification. This is a repo-intake +smoke test only—not a TORCH install, self-host upgrade, or live provider +qualification. The current self-host bootstrap artifact is +`/tmp/torch-self-bootstrap-20260928-profile-edits.json`, bound to working-tree +fingerprint `c2e96b6cb8f62b703de144c93c445cde231c392fec03f02e357c4e7b6aa6bebc`. +The baseline remains pending with eight provisional domains; the brief surfaces +19 unassigned components, 15 specification signals, six repository-level +shared candidates, and 14 collision candidates for Session Architect/owner +review. A new guard now grants each deterministic domain only imported shared +contracts evidenced by its dependency edges; unrelated root config and +untracked fixtures are not blindly added to every domain's `shared_paths`. +The existing untracked `test-torch-config-host-uQKdqL/` remains untouched and +is still visible as an analyzer review candidate. No provider was called, +nothing was installed, and no agent sessions were started. The profile-CLI +addition subsequently passed the full suite at **76/76**, lint, syntax checks, +and diff hygiene. That code change makes the bootstrap fingerprint and proposal +above stale; regenerate the repo-intake artifact after the current code/docs +pass is complete, then review the proposed roster without approving or +installing it. The hierarchy view remains empty for this not-yet-installed +self-host checkout. The completion audit below remains historical and has not +been refreshed for this tree. The Pi adapter/MCP bridge and per-identity profile +coverage subsequently passed `npm test` **79/79**, `npm run lint`, `npm run check`, +and `git diff --check`; Pi's bundled extension path now also resolves correctly +when TORCH is installed under a directory containing spaces. This invalidates +the earlier profile-edits bootstrap preview; regenerate it after the current +code/docs pass, and leave the new proposal pending review. No Pi CLI, AI +provider, or fleet session was started. The owner-console stale-evidence +surface subsequently passed the full suite at **79/79**, lint, syntax checks, +and diff hygiene; it consumes existing read-only backlog-health findings and +adds no parallel ledger. The Pi-adapter intake preview is stale after this +dashboard change. Regenerate a final pending intake after the current code/docs +pass, then review it without approving, installing, or starting the Fleet. + +Latest local validation (2026-09-28): Codex identities default to +`gpt-6-luna` / `high`, Claude identities to `sonnet`, with per-identity and +per-runtime CLI overrides; Pi requires an explicit model. Locally installed +adapters use a user-local, exact-hash, explicit-approval trust flow. The owner +Console now also supports bounded project-scoped saved backlog views in browser +storage; these filter the authoritative board and never write project/task +state. Review, catalog listing, and doctor do not execute plugin code. The full +suite now passes **93/93**, with lint, syntax checks, and `git diff --check` +passing. Runtime profiles now also expose adapter-specific launch policies as +runtime defaults with per-identity overrides; doctor/start planning reject +unsupported, unsafe, and conflicting values, while owner CLI edits require +`--yes`, migrate legacy Codex policy without changing the effective sandbox, +and only affect future plans. This is covered by `SCN-runtime-launch-policy`. +Backlog tasks support optional feature/milestone classification by +the Session Manager with revision checks and audit history; dashboard rollups +derive only task-state progress from those same tasks and keep quality/release +gates separate. Saved backlog views can filter both labels. The owner Console +supports preview-confirmed priority changes and task proposals with durable +audit history. New owner tasks remain proposed and unassigned; selected domains +are routing hints, not agent assignments. Fleet and hierarchy proposal decisions +now use preview-confirmed owner controls; approval stays staged and cannot +activate the organization or launch a runtime. The public site and Console default +to a dark theme and use +the verified Torch v1 flame mark. The fresh read-only TORCH repo/spec intake +is `/tmp/torch-self-bootstrap-20260928-launch-policy.json`, bound to HEAD +`d31437a4fb255c1e307bbb70ee9513cc1e0bdf4c` and working-tree fingerprint +`9fc23334b2a863176ca29c700f2f1859eb497cc118210a759605197f36eed1a0`; its +analysis reports mutation-free reconnaissance across 84 tracked and 22 +untracked files. The baseline remains pending with eight provisional domains, +26 architecture components, and 15 collision candidates for owner/architect +review. The checkout still has uncommitted changes, so it is +not eligible for clean-commit candidate staging. The local dashboard preview +was not listening at `127.0.0.1:4317` on 2026-09-29; it can be started +separately for UI review and does not start the TORCH Fleet. Installed Pi +0.80.3 now passes an offline, no-session RPC startup +smoke test with the bundled bridge and no model turn; TypeBox 1.1.38 is a +declared exact runtime dependency so this also passes offline package +distribution. No TORCH self-install or agent session occurred, and no provider +was called. +Next self-host gates: review/finalize the roster, land a clean Git candidate, +and pass candidate acceptance against that committed source. A filtered +artifact-mode snapshot passed all 36 candidate scenarios, but it does +not substitute for commit-bound qualification or authorize stable self-host +installation. COMBATRIG migration and live mixed-provider qualification remain +separate owner-authorized gates. + +## Next safe implementation task + +- [x] Harden repository-analysis freshness before trusting a self-host proposal. + Analysis now includes unignored, non-symlink regular files from the working + tree and records a deterministic content fingerprint. Proposal approval and + installation reject any changed fingerprint; the regression scenario proves + that adding an untracked source file changes both the inferred design and its + approval validity (`SCN-analysis-working-tree`, included in the 76/76 suite). + The older `/tmp/torch-self-design-20260928.json` preview is stale. The current + pending proposal is `/tmp/torch-self-design-20260928-artifacts.json`, bound to + the fingerprint recorded above; inspect it before any approval and keep it + outside the repository until owner review. This intake test is not a self-host + installation or provider test. +- [x] Keep specification prose from silently expanding repository ownership. + The deterministic baseline now merges a responsibility only when its + specification signal cites a concrete path present inside the analyzed + repository; otherwise the signal remains visible for Session Architect/owner + review. Prose-only projects still receive provisional groups with missing-path + blockers. `SCN-spec-evidence-grounding` covers false attribution by keyword + overlap. ## Planned architecture follow-up -- [ ] Implement ADR-011: versioned, project-specific organization graphs and - owner-approved hierarchy changes. Extend Fleet assessment to detect - sustained coordination pressure, distinguish role needs from process or - specialist changes, and create deduplicated proposals for the owner. Then add - a gated role schema, impact plan, promotion path, bounded pilot, success/stop - measures, and reversal before enabling Program Directors, Fleet Operations - roles, or domain leads. Preserve direct peer communication, single backlog, - implementation ownership, active work, and explicit runtime launch. Do not - copy COMBATRIG's proposed hierarchy as the default. +- [x] Add the initial versioned organization-graph schema: it distinguishes + owner-facing, program, fleet, domain-coordination, specialist, and + independent-review roles; validates reporting links and prevents + coordination roles from receiving owner-only approval or implementation + owner authority. +- [ ] Complete ADR-011 adaptive organization. Implemented so far: initial graph + schema; sustained-evidence, deduplicated durable proposals; explicit no-change + and process-change alternatives; impact and pilot baseline/success/stop/ + reversal fields; owner-only decision; CLI/MCP read/propose/plan surfaces; a + project-configured read-only assessment of in-window cross-domain tasks, + manager coordination requests, approval waits, and handoffs + (`SCN-hierarchy-pressure-assessment`); a revisioned evidence history for + open proposals, exact-retry deduplication, + and reconsideration after terminal decisions (`SCN-hierarchy-reconsideration-after-new-evidence`); + separate owner-confirmed CLI pilot activation that applies the graph and + manager schedules in one scoped commit without launching sessions or timers; + active-graph identity checks in doctor; and a dashboard map of + approved roles, identity bindings, reporting, coordination, and implementation + surfaces. Proposed roles and evidence must reference the active roster; new + persistent identities still use the separate Fleet-evolution approval path. + The Session Architect brief now requires an evidence-backed flat-versus- + coordination assessment, outcome-based lead proposals, and explicit boundaries + for implementation, coordination, fleet operations, priority, review, and + owner-only authority. Semantic validation refuses unresolved shapes, unknown + evidence, duplicate ownership/owner-approval grants, and manager-only peer + routing (`SCN-architect-organization-assessment`). This recommends future + organization only; it does not create or activate roles. + Pilot evidence can now be recorded with `hierarchy-review-pilot` and read + with `hierarchy-pilot-reviews`: exact baseline/criterion coverage, matching + units, measured/qualitative/unavailable labels, linked reviewer-reported + evidence, full-window adoption-recommendation gates, durable deduplicated + history and atomic audit. Reviews do not mutate the graph or start anything. + Separate CLI conclusion-plan/confirm paths now implement owner-only adoption + and safe reversal. Fresh plan hashes bind tracked and active-work state; + adoption needs the latest supporting review. Reversal restores prior + relationships at a new revision while preserving owners, identities, + worktrees, tasks and messages. Schedules remain for explicit review. Failed + commits restore scoped files/index; interrupted local recording reconciles + the exact committed receipt without a second commit. Confirmed retries are + idempotent. No runtime or timer lifecycle change occurs. + The Console now shows the latest five pilot reports per hierarchy proposal, + with baseline/observed comparisons, units, evidence quality, interim/full + review status, criterion outcomes and references. It explicitly labels these + as reviewer-reported recommendations, not owner decisions or verified facts. + Snapshot observation leaves history unchanged; the demo includes a + qualitative inconclusive pilot for browser qualification. + Still needed: collect/assess real fleet pressure, qualify pilot measurements + and live adoption/reversal. Console conclusion preview/confirm now reuses + the owner-only service path, binds target/operation/action/reason and fresh + state, and provides no confirmation token for blocked plans. Same-origin + API scenarios verify adoption, reversal, stale/cross-site/altered requests + and idempotent replay; Chromium exercises blocked adoption and sample + reversal with zero live API traffic. Adoption rechecks current calendar + maturity rather than relying solely on a stored report flag. + The Console now exposes owner-confirmed activation for an + approved pilot through an exact-plan preview, loopback same-origin check, + one-use token, and fresh-state revalidation before its scoped commit. It shows + active work, manager schedule changes, blockers, and timer reconciliation; + it never starts identities or changes systemd state. + Current working-tree validation (2026-09-29): 38/38 candidate-acceptance + scenarios, 126/126 tests, lint, and syntax checks pass. This is not a + clean-commit release receipt and does not qualify a live hierarchy pilot. +Invariant: preserve direct peer communication, the single backlog, +implementation ownership, active work, and explicit runtime launch. Do not copy +COMBATRIG's proposed hierarchy as the default. + +- [ ] Complete ADR-012 per-identity runtime profiles and portable local adapters. + Implemented so far: profiles carry an independently selected Claude, Codex, + Pi, or explicitly registered adapter plus model/reasoning values per identity; + startup and doctor report unsupported adapter capabilities without provider + substitution; custom adapter contracts are testable; mixed-adapter planning + has a regression scenario. Profile UX now includes `torch profile show`, + `torch profile set`, and `torch profile defaults`; all writes require `--yes`, + validate project config, and affect future launch plans only. Adapter-specific + launch-policy validation and argument mapping are now implemented with + fail-closed unsupported/unsafe handling. Still needed: supported-version Pi + runtime coverage and live mixed-provider resume evidence. + Pi now has a built-in resumable-turn adapter and a bundled extension that + exposes the TORCH MCP tools through an identity-bound local bridge; local MCP + bridge and launch-plan tests are supplemented by an installed Pi 0.80.3 + offline/no-session startup test that loads the extension without a provider + turn. The `typebox` dependency is declared and exact-version pinned so offline + package installation remains verifiable. Third-party local runtimes can now + be explicitly trusted by exact user-reviewed entrypoint hash and selected per identity; synthetic + mixed-runtime planning passes without launching a provider. Pi extensions execute + with Pi's full OS permissions; + TORCH disables project extension discovery/trust and loads only its bundled + bridge. Live mixed-provider coordination/resume remains an owner-authorized + qualification gate. + Built-in defaults are Codex `gpt-6-luna` / high reasoning and Claude + `sonnet`; installed project config and individual identity entries can + override these without changing global provider settings or starting a + process. Codex passes reasoning via invocation-local config. Profile + inheritance is resolved consistently by startup, doctor, live identity + queries, and dashboard snapshots; the active identity surface exposes the + effective runtime/model/reasoning profile. + +## Product experience follow-up + +- [ ] Complete the adaptive owner dashboard. Implemented so far: a responsive, + local read-only project cockpit with attention-first pulse, one authoritative + backlog board, per-identity runtime/model/reasoning visibility, durable + inter-session communication, exact-commit checks, integration/delivery gates, + resource/worktree state, hierarchy proposals, a provenance-backed visual + evidence catalog, and read-only backlog-health signals for stale or + unreachable `observedAt` commits. These appear in the project pulse and on + affected task cards. Agents may attach bounded images to owned backlog tasks with + identity/session/full-commit provenance; bytes are privately stored outside + the repository and digest-checked when served. Owners can preview feedback + from the CLI and explicitly confirm a durable message to the responsible + identity. The Console now offers feedback controls that first display the + recipient, task, commit, durable-message effect, and exact message; only a + fresh single-use preview token can confirm. Same-origin loopback checks, + expiry, replay refusal, and stale-preview checks prevent cross-site or + duplicate writes. The snapshot endpoint remains GET-only, and the Console + gains no general task/message ledger or lifecycle authority. The view adapts + to the observed roster. Saved, customizable project-aware backlog views now + use bounded per-project browser preferences and do not create a second + ledger. Backlog tasks now support optional feature and milestone labels, the + manager can update them through revision-checked classification, and the + Console derives progress from those same task records while keeping quality + and release gates separate. The owner can now propose backlog tasks through + same-origin preview/confirm; each is audited and remains unassigned for + Session Manager triage. Fleet and hierarchy proposals now have owner-only, + same-origin preview/confirm decisions bound to the exact proposal, reason, + active graph, and current repository head. Fleet approval is explicitly only + for later activation review; hierarchy approval only approves a pilot plan. + Separate explicit `hierarchy-activate` commits the approved graph, schedules, + and pilot record but does not provision a worktree or launch a runtime. Owners can now also send + a preview-confirmed request to one approved roster identity, optionally linked + to an existing active task; this becomes one audited durable inbox message and + does not create/modify work, change ownership, or wake/start a session. Still + needed: expose appropriately gated deeper workflow controls. Continue to + avoid hard-coded COMBATRIG domains or hierarchy. + Target experience: + - a concise project pulse showing what is moving, what needs owner attention, + current blockers, stale evidence, and the next consequential decisions; + - project-aware kanban/backlog views with dependency, owner, status, resume, + and verification evidence, backed by TORCH's single authoritative backlog; + - an inter-session activity and communication view for durable messages, + coordination requests, handoffs, acknowledgements, and decisions—never + private model chain-of-thought; + - a visual review gallery for screenshots and other artifacts, with exact + task/session/commit provenance, user comments or annotations, and a clear + feedback route into the responsible agent or backlog item; + - feature and milestone progress with test receipts, integration state, + release gates, deployment authority, and live-verification evidence kept as + distinct states; + - an organization/fleet view showing session health, runtime/model profile, + work ownership, relationships, resource use, and approved hierarchy-change + proposals; + - safe owner controls to comment, prioritize, create or route work, accept or + reject proposals, and request permitted agent actions. Every consequential + operation must show its target, effects, evidence, and authority gate before + execution; provider launches, spending, worktree removal, integration, + release, and deployment remain separately authorized operations. + Keep the interface responsive and accessible on desktop and mobile, local- + first by default, evidence-linked, and explicit when data is unavailable or + stale. Preserve the read-only observation API; expose only individually + scoped, previewed owner actions and do not create parallel task/message + ledgers. ## Owner-authorized qualification still required diff --git a/package-lock.json b/package-lock.json index 521bde9..2547d15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,15 @@ "": { "name": "torch-agent-fleet", "version": "0.1.0-alpha.0", + "bundleDependencies": [ + "@modelcontextprotocol/server", + "typebox", + "zod" + ], "license": "MIT", "dependencies": { "@modelcontextprotocol/server": "^2.1.0", + "typebox": "1.1.38", "zod": "^4.6.5" }, "bin": { @@ -273,6 +279,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.1.0.tgz", "integrity": "sha512-YdFj5gMRHr0gYNAhTbQDgjLHiEJdIAUYxomhKseUNye7ZGlISQmUoLh2sveFFCiQ7j2YMffT1kgSND0FSfIFjQ==", + "inBundle": true, "license": "MIT", "dependencies": { "zod": "^4.2.0" @@ -285,6 +292,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.1.0.tgz", "integrity": "sha512-FdvwZU5N03O77Suh35JqjAqq5R1ZTEPFtwu6N4BgBq28qrQtrCnpbLL5U2pwYCuE194jAw0xnlOVmIqhfoYuUQ==", + "inBundle": true, "license": "MIT", "dependencies": { "@modelcontextprotocol/core": "2.1.0", @@ -1411,6 +1419,13 @@ "node": ">= 0.8.0" } }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "inBundle": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -1563,6 +1578,7 @@ "version": "4.6.5", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "inBundle": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 04c5e0c..f59ded6 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,11 @@ "./backlog": "./src/backlog/service.mjs", "./adapters/claude": "./src/adapters/claude.mjs", "./adapters/codex": "./src/adapters/codex.mjs", + "./adapters/pi": "./src/adapters/pi.mjs", + "./adapters/plugins": "./src/adapters/plugins.mjs", + "./adapters/registry": "./src/adapters/registry.mjs", + "./adapters/runtime": "./src/adapters/runtime.mjs", + "./artifacts": "./src/artifacts/service.mjs", "./canonical/local": "./src/canonical/local.mjs", "./checks": "./src/checks/service.mjs", "./convergence": "./src/convergence/service.mjs", @@ -17,12 +22,14 @@ "./design/architect": "./src/design/architect.mjs", "./delivery": "./src/delivery/service.mjs", "./evolution": "./src/evolution/service.mjs", + "./evolution/hierarchy": "./src/evolution/hierarchy.mjs", "./forge": "./src/forge/service.mjs", "./control-plane": "./src/control-plane/service.mjs", "./kernel/analyze": "./src/kernel/analyze.mjs", "./kernel/config": "./src/kernel/config.mjs", "./kernel/git": "./src/kernel/git.mjs", "./kernel/install": "./src/kernel/install.mjs", + "./kernel/organization": "./src/kernel/organization.mjs", "./kernel/specifications": "./src/kernel/specifications.mjs", "./kernel/worktrees": "./src/kernel/worktrees.mjs", "./integration": "./src/integration/service.mjs", @@ -60,6 +67,7 @@ "bin/torch.mjs", "src/cli.mjs", "src/adapters/", + "src/artifacts/", "src/backlog/", "src/canonical/", "src/checks/", @@ -88,17 +96,24 @@ "schemas/" ], "scripts": { - "test": "node --test --test-timeout=30000 test/fleet/*.test.mjs", + "test": "node --test --test-timeout=60000 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 && node --check src/console/server.mjs && node --check site/app.js && node --check site/console.js", + "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 src/artifacts/service.mjs && node --check src/adapters/plugins.mjs && node --check src/adapters/pi.mjs && node --check src/adapters/pi/extension.mjs && node --check site/app.js && node --check site/demo.js && node --check site/console.js && node --check site/work-views.js && node --check site/work-progress.js", "site:serve": "node bin/torch.mjs console serve", - "format": "eslint src/adapters src/backlog src/canonical src/checks src/console src/convergence src/control-plane src/delivery src/design src/evolution src/forge src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet --fix", - "lint": "eslint src/adapters src/backlog src/canonical src/checks src/console src/convergence src/control-plane src/delivery src/design src/evolution src/forge src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet" + "test:dashboard": "node scripts/test-dashboard-demo.mjs", + "format": "eslint src/adapters src/artifacts src/backlog src/canonical src/checks src/console src/convergence src/control-plane src/delivery src/design src/evolution src/forge src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet --fix", + "lint": "eslint src/adapters src/artifacts src/backlog src/canonical src/checks src/console src/convergence src/control-plane src/delivery src/design src/evolution src/forge src/importers src/integration src/kernel src/mcp src/observability src/resources src/runtime src/schedules src/self-host src/telemetry src/cli.mjs bin/torch.mjs site/*.js test/fleet" }, "dependencies": { "@modelcontextprotocol/server": "^2.1.0", + "typebox": "1.1.38", "zod": "^4.6.5" }, + "bundleDependencies": [ + "@modelcontextprotocol/server", + "typebox", + "zod" + ], "devDependencies": { "@eslint/js": "^10.0.1", "@playwright/test": "^1.58.2", diff --git a/reports/audit/abandoned-check-console-2026-09-30.md b/reports/audit/abandoned-check-console-2026-09-30.md new file mode 100644 index 0000000..cc639fb --- /dev/null +++ b/reports/audit/abandoned-check-console-2026-09-30.md @@ -0,0 +1,30 @@ +# Abandoned-check cleanup visibility — 2026-09-30 + +Check recovery now records cleanup pending before attempting owned disposal, then +persists its outcome. A process interruption before outcome persistence remains +conservatively pending. Reopening shows recorded cleanup state; explicit replay +can update it without another authority audit. A no-op replay preserves metadata. + +The project-scoped read-only snapshot omits input paths, process IDs, owner evidence +and private actor metadata. It supports databases without recovery columns and +malformed/oversized records without migrating or inspecting their artifact paths. +Abandoned checks remain visible in the actual shared Console/demo evidence rows. +Completed, pending and unconfirmed cleanup are distinct, and executor termination +is explicitly owner-attested rather than automatically verified. No recovery or +retry controls were added. + +Verification: + +- 12 focused projection/demo/resource/recovery scenarios PASS (two browser + qualification scenarios deliberately skipped in this focused Node run). +- Actual Chromium dashboard demo desktop/mobile/control-refresh checks PASS, + zero live API calls. Expanded cleanup evidence was visually inspected at + `reports/design-system/torch-dashboard-check-evidence.png`. +- Full isolated source acceptance PASS: 38 groups, test/lint/syntax; receipt + `/tmp/torch-abandoned-check-console-20260930-acceptance.json`. +- Focused lint and diff hygiene PASS. + +Frontend-design guidance kept the existing dark palette, typography and expandable +evidence-row layout rather than adding competing dashboard chrome. No agents, +host timers, stable installation, deployments or COMBATRIG changes occurred. +Browser-descendant interruption and other-platform recovery remain unfinished. diff --git a/reports/audit/browser-descendant-interruption-2026-09-30.md b/reports/audit/browser-descendant-interruption-2026-09-30.md new file mode 100644 index 0000000..56bde14 --- /dev/null +++ b/reports/audit/browser-descendant-interruption-2026-09-30.md @@ -0,0 +1,41 @@ +# Browser descendant interruption — 2026-09-30 + +`SCN-browser-descendant-interruption` launches real Chromium through an actual +prepared measurement subprocess, serving frozen captured HTML and exact source +identity. Only the disposable TORCH runner is killed first. The remaining harness +can still query real page DOM/visibility, proving that stopped runner metadata is +not evidence of stopped browser execution. Recovery without all-executor stopped +confirmation fails and preserves captured inputs. + +Before explicit harness shutdown, the qualification opens Linux pidfds for the +owned harness and currently observed isolated browser group. Browser/harness PID +birth identity and isolated group ownership are checked. Multiple actual Chromium +processes must be observed. After explicit shutdown, kernel terminal events are +awaited for those handles and no live member of the isolated group may remain. +No sleeps, guessed elapsed-time death, or retries are used. + +Only then does owner-confirmed recovery record abandonment and clean the owned +snapshot. It creates no receipt/pass and leaves the browser lease held until its +owner explicitly releases it. Original live build files remain intact. + +Qualification command (16 focused scenarios PASS, zero skips): + +```sh +TORCH_QUALIFY_BROWSER=1 TORCH_QUALIFY_INTERRUPTION=1 node --test test/fleet/verification-resources.test.mjs test/fleet/check-snapshot.test.mjs test/fleet/check-conditions.test.mjs +``` + +Full isolated source acceptance PASS (38 groups, tests/lint/syntax): +`/tmp/torch-browser-descendant-interruption-20260930-acceptance.json`. +Focused ESLint and diff hygiene PASS. + +## Boundaries + +This proves one explicitly owned Linux Chromium harness and its captured isolated +process group. It does not discover every arbitrary project descendant, escaped +group, foreign host process or other-platform executor. TORCH runtime recovery +still requires owner evidence; the Python pidfd helper is explicit qualification +instrumentation, not a product/runtime dependency or an automatic kill service. + +No live agents, host timers, stable installation, deploys or COMBATRIG changes +occurred. Test-owned browser/harness processes stopped; only owned disposable +snapshots were removed and source/live build inputs were preserved. diff --git a/reports/audit/frozen-browser-qualification-2026-09-30.md b/reports/audit/frozen-browser-qualification-2026-09-30.md new file mode 100644 index 0000000..3bfc80a --- /dev/null +++ b/reports/audit/frozen-browser-qualification-2026-09-30.md @@ -0,0 +1,50 @@ +# Frozen queued-browser qualification — 2026-09-30 + +## Evidence + +`SCN-real-frozen-browser` uses real Chromium, a loopback HTTP server, +disposable installed Git project/worktrees and the actual resource/check services. +A peer holds the browser slot; the specialist captures its ignored build and +queues. Execution without the lease fails. The specialist then commits newer +source and overwrites its live output in place. After promotion, a reopened +CheckService executes the frozen build through an explicitly configured absolute +harness path (Playwright dependencies resolve from the TORCH checkout). + +The browser observes the old build and captured commit. Retained output matches +the input digest, exact-pass lookup rejects the newer SHA, and completed snapshot +cleanup leaves the live build unchanged. A second capture of the new build fails +the unchanged browser expectation, proving the harness does not simply return +success. Both browser processes and loopback servers close. + +The first run failed: measurement commands lacked the captured-subject environment +variables already supplied to condition probes. `executeConditionedCheck` now +passes `TORCH_CHECK_COMMIT` and `TORCH_CHECK_INPUT_DIGEST` to measurements too. +`SCN-check-measurement-subject` uses actual child commands to enforce this for +checks with and without condition policies. Assertions were not relaxed. + +Focused command (five scenarios PASS, zero skips): + +```sh +TORCH_QUALIFY_BROWSER=1 node --test --test-name-pattern='SCN-real-frozen-browser|SCN-check-' test/fleet/verification-resources.test.mjs test/fleet/check-conditions.test.mjs +``` + +Browser qualification is explicit opt-in because Chromium installation and +loopback permissions are not requirements for a headless kernel installation. +Ordinary acceptance runs do not count the skipped browser scenario as proof. + +Full isolated current-source candidate acceptance PASS; receipt: +`/tmp/torch-frozen-browser-20260930-acceptance.json`. Focused ESLint and +`git diff --check` PASS. No existing assertions were weakened or deleted. + +## Not proven + +- GPU acceleration, a game simulation clock, or COMBATRIG compatibility of this harness. +- Independent condition sensors or same-runtime pre/post probe coordination. + The browser checks visibility and advancing browser performance time only. +- Actual filesystem reflink allocation or zero additional disk use. +- Hard-killed executor recovery; reopening an unstarted prepared check is not that proof. +- Portability of arbitrary project harness dependencies; this exercises one explicit path. +- Installed fleet/provider lifecycle. No agents, host timers, deploys or stable + TORCH installation were started. + +The full queued-browser roadmap item remains open for these qualification gates. diff --git a/reports/audit/interrupted-check-recovery-2026-09-30.md b/reports/audit/interrupted-check-recovery-2026-09-30.md new file mode 100644 index 0000000..e097e29 --- /dev/null +++ b/reports/audit/interrupted-check-recovery-2026-09-30.md @@ -0,0 +1,46 @@ +# Interrupted prepared-check recovery — 2026-09-30 + +The Linux qualification starts an actual CheckService runner and real Node +measurement subprocess. The subprocess reports its PID and blocks until explicitly +terminated. Recovery while the recorded runner lives is refused. The owned runner +is killed and its exit observed; the owned executor is then terminated through a +kernel pidfd and its terminal notification awaited without sleep/poll retries. + +Reopening CheckService observes persisted running state and intact inputs; it +cannot rerun the prepared operation. Recovery requires the configured owner, +explicit approval, stopped-executor/descendant attestation and evidence. +`abandoned` state and the original audit commit atomically before cleanup. + +An altered ownership marker causes cleanup to remain pending with inputs intact. +Restoring the exact original marker permits explicit replay cleanup, with one +original authority audit. Recovery never creates a receipt, invokes an executor, +releases a lease or changes source files. Owner CLI previews, refusal without +confirmation and replay are exercised. Frozen-pass qualification also rejects a +receipt lacking linkage to a finished prepared operation (simulated crash window). + +Qualification command: + +```sh +TORCH_QUALIFY_BROWSER=1 TORCH_QUALIFY_INTERRUPTION=1 node --test test/fleet/verification-resources.test.mjs test/fleet/check-snapshot.test.mjs test/fleet/check-conditions.test.mjs +``` + +The interruption test is explicit opt-in on Linux and uses Python pidfd support +only as a qualification helper. TORCH itself does not require Python. Ordinary +skipped scenarios are not evidence of hard-interruption qualification. + +All 15 focused scenarios PASS with zero skips, including real Chromium and hard +interruption. Full isolated current-source acceptance PASS (38 groups, +test/lint/syntax); receipt `/tmp/torch-interrupted-check-20260930-acceptance.json`. +Focused lint and `git diff --check` PASS. + +## Boundaries + +- Process observation tracks the runner, not all descendants. The owner evidence + must cover descendants separately; stale/unknown/foreign runner identity is + not automatic proof of their termination. +- No automatic kill, retry, restart, lease theft or stale-time abandonment exists. +- Browser descendant interruption, other-platform recovery and Console visibility + for abandoned checks with pending cleanup remain open qualification/work items. +- No live agents, host timers, deployments, COMBATRIG changes or stable installation. +- Captured disposable test inputs were removed only through owned cleanup; + original source/build inputs remained intact. diff --git a/reports/audit/product-alignment-review-2026-09-29.md b/reports/audit/product-alignment-review-2026-09-29.md new file mode 100644 index 0000000..b8d9721 --- /dev/null +++ b/reports/audit/product-alignment-review-2026-09-29.md @@ -0,0 +1,507 @@ +# TORCH product alignment review + +Reviewed the conversation requirements, current specification and TODO, +dirty working tree, runtime installation/design boundaries, provider adapters, +backlog loop, integration serialization, resume instruction injection, and +public/dashboard surfaces. This is a bounded implementation review, not a +claim of release qualification or a line-by-line security audit. + +## Product intent + +TORCH is a portable project development fleet. A repository and selected +specifications shape specialist responsibilities. Each identity can use a +different local runtime/model. Specialists keep focused context, communicate +directly, and work from one durable backlog. Coordination roles can be proposed +as integration work grows. The owner uses the actual project dashboard to +inspect progress, evidence, agent communication, approvals, and profiles. +TORCH should eventually develop TORCH through its installed stable engine. + +## Corrected findings + +1. **Dashboard demo was a separate miniature implementation.** + `site/demo.html` and the prior demo script implemented only four static + panels with their own controls. The owner's correction requires the actual + tool. The landing link now opens `console.html?demo=1`; the real Console + markup and renderer use an in-memory sample transport. Tasks, priority, + requests, image feedback, profiles, approvals and organization review work + through the same UI. The link label is `View dashboard demo` and the demo + title is `TORCH Dashboard`. The former demo URL redirects to that view. + +2. **Generated runtime assignments prevented Codex-only installation.** + `src/kernel/domains.mjs` assigned Claude to every generated domain, and + installation required all those assignments even when the owner selected + only Codex. Generated roles now use `default`; installation resolves them + to its chosen adapter, reports it in the dry-run, and supports + `--default-runtime`. Explicit per-role assignments remain intact. The + existing no-selection Claude default remains for compatibility. + The same hard-coded fallback also existed in dynamic domain creation and + merge/split proposals. Those now resolve omitted or inherited runtime + choices from the installed project's default, covered by a Codex-only + domain-proposal scenario. + +3. **Session Architect validation excluded Pi/custom adapters.** + A two-provider whitelist conflicted with the registered adapter model. + Pending designs now validate adapter identifier syntax. Installation still + checks support and plugin trust before creating project state. This does + not add a new AI provider invocation pathway to the architect itself. + +4. **Work board used a state absent from the backlog model.** + The Queue lane matched `open`, but `src/backlog/service.mjs` defines + `ready`. Real ready work fell into Other. The shared Console now puts + `ready` work into Queue, exercised in the browser scenario. + +## Remaining issues and limits + +2026-09-30 conclusion follow-up: separate owner-confirmed CLI adoption and +reversal now use fresh plan hashes binding current commit, graph, owned pilot, +manifest, latest review and active work. Adoption requires the latest supporting +review; reversal restores prior relationships at a new revision and retains +implementation owners, identities, actual worktrees, assigned task history, +messages and schedules. Obsolete schedules remain for explicit review. Failed +Git commits restore scoped files/index, while a committed receipt whose local +audit transaction failed reconciles on the same owner confirmation without a +second commit. Confirmed replays are idempotent. The full isolated acceptance +gate passes all 38 groups with test/lint/syntax exit 0 and no signal/error; +pilot reversal evidence is explicitly required. Live qualification and Console +conclusion controls remain open. No real provider, fleet or timer was started. + +2026-09-30 hierarchy lifecycle follow-up: the readiness audit found the +end-of-pilot comparison path missing. `hierarchy-review-pilot` now records +durable, audited, deduplicated baseline comparisons and criterion assessments; +`hierarchy-pilot-reviews` reads the history. It requires the active owned pilot +record, exact metric/criterion coverage and matching units. Observations are +explicitly reviewer-reported measured/qualitative/unavailable evidence. Adoption +recommendations require the configured window to finish and all success/stop +criteria to support that recommendation. No graph, tracked files, runtime or +proposal lifecycle state changes. Owner-confirmed adoption, safe reversal and +live pilot qualification remain open. The focused hierarchy suite passes +12/12, and the strengthened real isolated acceptance gate passes all 38 groups +with test/lint/syntax exit 0 and no signal/error. Pilot review evidence is now +required by that gate; missing-marker regressions reject its absence. + +2026-09-30 follow-up: the manager wake policy finding below has been corrected +in implementation. The owner can explicitly select count-limited wakes for +subscription/local runtimes while retaining enforced USD-hard-cap mode when +supported. Real built-in adapter planning, simulated invocation, UTC budgets, +pending delivery recovery, and overlap protection pass the new scenarios. +Live provider/timer qualification remains open. CLI reservation inspection and +owner-attested recovery are now implemented and tested. Recovery requires an +offline manager and explicit stopped-runtime evidence, preserves budget and +message replay guards, and does not launch anything. This is not independent +process-termination proof or evidence of an operational fleet. + +- **Manager wakes still need operational qualification.** The original + mandatory USD ceiling prevented built-in adapters from waking managers. + Explicit invocation-count mode now fixes that policy mismatch without + claiming dollar enforcement. Live provider/timer behavior and operational + qualification of owner-attested reservation recovery remain open. Hard-cap mode continues to + fail closed for adapters that cannot enforce it; estimates are not caps. +- **Landing differs from COMBATRIG's batch lander.** TORCH serializes authorized + FIFO integration and requires exact checks after each source owner merges + current main. Stale siblings return for convergence. Remote publishing is a + separate owner-confirmed sync. This protects main, but it is not evidence of + an automatically rebased/merged batch or unattended remote publication. +- **Operational acceptance remains open.** Local fixtures and browser tests do + not qualify live mixed-provider sessions, persistent timers, COMBATRIG + cutover, or stable TORCH managing this checkout. The protected untracked + fixture remains untouched. No fleet, provider session, timer, or remote + publication was started in this review. +- **Tests need their proper interpretation.** Existing product tests rely + heavily on source text. The new browser scenario exercises the rendered + product and rejects live API traffic. Pure demo-state tests prove isolation, + preview/confirmation and replay behavior; they do not prove backend actions. + +## Verification entrypoints + +2026-09-30 Console conclusion follow-up: piloting proposals now offer a +separate owner conclusion preview and confirmation through the actual existing +organization UI. Blocked plans receive no token; same-origin loopback checks, +target/operation/action/reason binding, short-lived one-use previews, fresh +service plan hashes and idempotent confirmed replay guard the scoped commit. +Adoption rechecks current calendar maturity, including future-dated report +rejection. API scenarios verify supported adoption and reversal, missing or +altered confirmation, cross-operation token use, cross-site requests, stale +main, and one owner audit. The isolated demo shows blocked qualitative adoption +and confirmed sample reversal without changing agents/backlog or calling live +APIs. Its status explicitly says no real Git commit occurs. The dark preview +interaction was reused and the focused screenshot +`reports/design-system/torch-dashboard-pilot-conclusion.png` was visually +reviewed. Full isolated candidate acceptance passes all 38 groups with +test/lint/syntax exit 0 and no signal/error; both conclusion API markers are +required. Browser and diff checks pass. Live candidate status remains empty, +with no active version or stable launcher. Live qualification and self-host +activation remain open; no real Fleet, provider or timer was started. + +2026-09-30 pilot-review Console follow-up: snapshots attach the latest five +review reports to each hierarchy proposal without modifying review history. +The existing dark organization view renders baseline/observed comparison +columns, units, evidence quality, interim/full-window status, criterion +assessments and references. Reviewer-reported evidence and recommendations +are explicitly distinguished from verified facts and owner conclusions. The +actual Console demo includes a qualitative inconclusive sample; Chromium +checks the rendered values, references, lack of recommendation-execution +buttons, responsive layout and zero live API calls. The focused screenshot +`reports/design-system/torch-dashboard-pilot-review.png` was visually reviewed. +Full current-source suite 139/139, browser, lint, syntax and diff checks pass. +Console conclusion confirmations and live qualification remain open; no real +provider, Fleet or timer was started. + +2026-09-30 observability follow-up: read-only snapshots now expose manager wake +reservation history and blocking counts without modifying the SQLite records. +The actual Console shows unknown launches in owner attention and schedule +operations, with exact reservation/message references and presence explicitly +separate from process proof. Attention links open collapsed ancestor panels. +The isolated demo includes a sample unknown launch. Chromium verifies rendered +details, navigation, absence of unlocking/restart buttons, mobile layout and +zero live API calls; the focused image is +`reports/design-system/torch-dashboard-manager-wakes.png`. Full regression +137/137, browser, lint, syntax and diff checks pass. No live Fleet or timer +was started. The existing dark interface was preserved rather than introducing +a separate alert/dashboard design. + +2026-09-30 acceptance follow-up: the real `runCandidateAcceptance` gate on the +updated source passes all 38 required acceptance groups under disposable +HOME/TMPDIR/XDG isolation. The required evidence map now includes all four new +wake-policy/reservation/recovery scenarios plus unique provider-inheritance +and CLI provider-default scenarios. Regression checks reject each omitted +marker despite green process exits. Gate results are test/lint/syntax exit 0 +with no signal or error. This is current-source evidence, not an artifact +receipt or clean-commit qualification. Live read-only candidate status shows +an empty user-local version store, no active version and no stable launcher; +HEAD remains `d31437a4fb255c1e307bbb70ee9513cc1e0bdf4c` with preserved dirty work. + +Final current-tree evidence: **full regression suite PASS**, browser scenario +PASS, lint/syntax/diff checks PASS. The existing loopback preview serves the +demo with HTTP 200. These checks do not refresh earlier self-host candidate +acceptance receipts or qualify a stable installation. + +- `npm run test:dashboard`: Chromium browser scenario and desktop/mobile images + in `reports/design-system/`. +- `node --test test/fleet/dashboard-demo.test.mjs`: isolated demo state scenarios. +- Runtime selection scenarios in `test/fleet/kernel-lifecycle.test.mjs` and + `test/fleet/cli-scenarios.test.mjs` exercise real disposable Git installations. +- Full `npm test`, lint, syntax checks and diff hygiene provide regression + evidence for the current tree. See the current task handoff for final counts. + +Existing legacy branches `legacy/nostr-torch` and +`legacy/development-network-v2` are present. Installed CLI help also recognizes +the Codex create/resume options and Claude's background/resume system-prompt +file options used here; this is parser evidence, not provider execution. +# COMBATRIG frozen-check follow-up (2026-09-30) + +Implemented optional project-configured frozen check inputs in +`src/checks/snapshot.mjs` and the existing CheckService. `checks prepare` +captures explicit input paths before resource acquisition, validates a captured +project build-commit marker, and persists the subject. Copies are reflinks or +independent copies, never mutable hardlinks. Queued specialists may commit and +rebuild while waiting; `run-prepared` still verifies and tests the captured +subject and produces evidence only for its original SHA. CLI and identity-bound +MCP expose prepare/list/run/cancel. Resource ownership remains explicit. + +Stale markers, unsafe paths/symlinks, policy changes, tampered input copies, +execution exceptions, changed bytes and lost leases fail closed. Normal runs +remove only their exact owned snapshot while preserving source outputs and +receipt manifests. Interrupted running state is retained, not retried or +cancelled automatically. The build marker is project-declared provenance, not +independent build attestation. Executing from a snapshot is not a process +sandbox; project harnesses must avoid absolute live build paths and unrelated +existing servers. + +PASS: eleven focused frozen-input/resource/MCP tests; full current-source +candidate acceptance (38 groups with test/lint/syntax); five self-host tests +including missing-frozen-evidence rejection; diff hygiene. Source acceptance +receipt: `/tmp/torch-frozen-checks-20260930-acceptance.json`. No real browser/GPU +qualification, installed candidate, provider session, persistent timer, remote +action or COMBATRIG mutation occurred. The remaining operational lessons are +tracked under `docs/TODO.md` as explicit incomplete work. +# Test-condition receipt follow-up (2026-09-30) + +Added optional project-defined condition probes to both live and frozen checks. +Approved shell-free probes return a bounded schema/subject/conditions report. +Preflight rejects wrong build/digest, malformed or missing reports, incorrect +literal conditions and failed probes before measurements. Postflight drift +invalidates green measurement output. Pre/post diagnostic lines and structured +project-reported observations persist in artifacts and durable receipts; an +exit-zero receipt without required condition evidence cannot qualify. + +Check services now reload canonical definitions before listing/planning, +prepared execution and exact-pass qualification. Policy-change scenarios edit +the real project configuration rather than an in-memory map, demonstrating that +a previously constructed service refuses changed prepared policy and old +receipt qualification. An initial wrong-subject fixture accidentally aliased +its expected subject; that fixture now uses an independent copy. All rejection +assertions are unchanged. + +PASS: thirteen focused condition/resource/self-host scenarios; final isolated +current-source acceptance (38 groups, full tests/lint/syntax); diff hygiene. +Receipt: `/tmp/torch-test-conditions-final-20260930-acceptance.json`. +These are local fixture/contract results, not real GPU/browser harness or +installed-release qualification. The project probe must observe the same +subject as measurements and detect drift through the interval; endpoint reports +are not continuous sensor verification. Dashboard condition/prepared-check +details and live harness qualification remain in TODO. No provider, fleet, +persistent timer, stable installation or COMBATRIG mutation occurred. +# Commit-linked activity follow-up (2026-09-30) + +Subsequent owner-digest checkpoint: deterministic read-only Markdown/JSON +generation now uses current tasks, structured approvals, landed requests, +delivery lifecycle/operation receipts and bounded managed Git activity. Owner +attention leads; readable delivery names/versions, unresolved latest outcomes, +recorded shipping versus landing, completed work, blockers, approval decisions +and neglected owner requests follow. UTC windows, section counts/truncation, +missing evidence and unindexed decisions are explicit. Project strings are +escaped. Database reads share a transaction; worktree state is not claimed as +an atomic snapshot or independently verified deployment. + +Owner-approved local publication stores report/audit atomically. Enabled-policy +Fleet Operations publication and typed owner-authorized system scheduling use +no external command or AI wake, honor revocation and install no timer. Latest +reports survive reopen and are exposed by CLI, snapshot and GET-only no-store +`/api/digest`. Readable web presentation, external notifications and actual +installed daily cadence remain pending; no second backlog or public publication +was introduced. + +Verification: eight focused digest/self-host scenarios PASS; existing +digest/schedule run PASS (14 scenarios before the added receipt-window test). +Final current-source acceptance PASS (38 required groups, test/lint/syntax), +receipt `/tmp/torch-owner-digest-final-20260930-acceptance.json`; diff hygiene +PASS. No stable install, host timer, provider, fleet activation or real deploy. + +Subsequent saved-success recovery checkpoint: owner-only preview and explicit +stopped-executor reconciliation now apply a durably saved successful receipt +without executing an adapter. Exact commit/from-state/next-state, latest +successful receipt, current owner authority and unchanged destination settings +are required. New operations store a canonical destination-config hash; additive +migration preserves older receipts and falls back to their full original policy +hash. The parent-running/latest-success crash window is supported. Local +application/event/audit/operation changes are atomic and replay is unchanged; +interrupted recovery rolls back without discarding the external receipt. + +Verification: nine focused delivery/self-host scenarios PASS; all 38 source +acceptance groups PASS (test/lint/syntax), receipt +`/tmp/torch-delivery-success-recovery-20260930-acceptance.json`; diff hygiene +PASS. This supersedes the previous confirmed-success-recovery-pending notes. +Actual provider truth, idempotency and executor-stop qualification remain open. +No real deployment, provider, timer, stable install or COMBATRIG change occurred. + +Subsequent delivery-attempt checkpoint: each external call now has a durable +operation reservation and pre-execution attempt record. Independent callers +cannot overlap a delivery's effects. Approval remains mandatory; failed receipts +do not advance lifecycle state. Explicit safe-adapter declarations and structured +transient/not-applied failures permit 1–5 bounded attempts, default one, sharing +an idempotency key. Policy drift stops further calls. Thrown/malformed/uncertain +outcomes remain unresolved. Owner-approved executor-stopped not-applied +attestation clears uncertainty for later deliberate action without rewriting +the original attempt or advancing state. CLI and bounded console snapshots expose +records. Successful lifecycle/event/audit/operation application is transactional. + +Verification: three focused delivery scenarios PASS; all 38 current-source +acceptance groups PASS (test/lint/syntax), receipt +`/tmp/torch-delivery-attempts-20260930-acceptance.json`; subsequent indentation +cleanup is mechanical. Real provider execution, provider-side idempotency, +confirmed-success local-application recovery, canonical fetch retry and dashboard +detail presentation remain open. No real deploy, timer, provider, stable install +or COMBATRIG mutation was performed. + +Subsequent landed-closure checkpoint: opt-in post-landing reconciliation now +requires matching project/target, canonical ancestry, current integration and +specialist checks, task ownership, exact commit, evidence and ready-to-integrate +state. It processes standalone trailers from the submitted tip only. Per-task +locks and normal revisioned completion preserve idempotency. CLI preview/yes +and bound manager MCP provide explicit recovery. A post-landing bookkeeping +failure returns needs-review and audits it without reversing successful Git +landing. Unknown/mismatched tasks remain open. Specialist prompts describe +intent versus authority and require confirmation. Automatic policy defaults +off; no real fleet was launched. This supersedes the earlier closure-pending +statement below; live qualification remains open. + +Verification: seventeen focused integration/MCP/self-host tests PASS; final +source acceptance PASS (38 required groups, tests/lint/syntax), receipt +`/tmp/torch-landed-closure-final-20260930-acceptance.json`. Diff hygiene PASS. +Source remains uncommitted and no stable install/provider/timer/deploy was run. + +Added a bounded read-only scan of canonical and managed local worktree branch +history. CLI `backlog activity` and MCP `torch_backlog_activity` expose exact +task-ID links, owner-request-first stale findings, coverage, future timestamps, +unknown task references and standalone closure intent. Backlog health includes +activity anomalies without auto-repair. Approved configuration controls the +age/history bounds; task metadata edits do not reset the activity clock. + +Incomplete history cannot prove neglect. Commit dates/messages are reported +metadata, not independent proof of substantive work. `Closes:` remains intent: +no message alone bypasses landed-evidence completion. Automatic post-landing +closure, daily review/digest scheduling, durable deployment retry receipts and +dashboard activity remain open. No live provider, fleet, timer, stable install, +COMBATRIG mutation or deployment was authorized or performed. + +Focused activity/backlog/MCP tests PASS (18 tests). Full current-source +acceptance PASS (38 required groups, test/lint/syntax); receipt: +`/tmp/torch-task-activity-20260930-acceptance.json`. Diff hygiene PASS. This does +not qualify an installed candidate or real provider/browser operation. + +# Specialist self-claim follow-up (2026-09-30) + +Added optional canonical `backlog.self_claim` policy with explicit identity +allowlist; default/omitted policy is disabled. CLI preview/confirmation and +bound MCP plan/claim resolve active work before selecting explicitly routed +ready tasks with complete dependencies. Selection honors existing priority, +then creation time/ID. Claim histories/audits name the specialist; no fake +manager identity or acknowledgement-only assignment message is generated. + +Claim readiness blocks dirty/wrong-branch worktrees, Git operations, active +guards, prepared/running checks, held/waiting resources, unresolved named +approvals and unfinished integration. Existing assignments remain observable +after new-claim permission is revoked. Ordinary transitions cannot spoof the +private self-claim authorization. All transitions into active assignment states +now use the same global assignment lock and busy check, closing a blocked-task +reactivation loophole that could otherwise produce a second active item. + +PASS: twenty focused backlog/MCP/self-host tests and final current-source +acceptance (38 groups, full tests/lint/syntax); diff hygiene. Actual concurrent +local processes compete for ready work in the race scenario. The initial QA +fixture needed a second real test surface to produce its analyzed QA identity; +the original rejection assertions were retained. Strict MCP catalog ordering +was fixed in production, not loosened in tests. + +The new missing-evidence regression also exposed substring matching in the +candidate verifier: a longer self-claim scenario ID could satisfy a missing +shorter ID. Verification now tokenizes exact scenario IDs. No acceptance +requirement was removed. Final receipt: +`/tmp/torch-selfclaim-final-20260930-acceptance.json`. + +These results qualify local mechanics, not model compliance with the new +exception-only communication/per-item refresh guidance or live autonomous +settle/claim operation. No provider, fleet, timer, installation, remote action +or COMBATRIG mutation occurred. Commit-linked progress/closure and other +operational lesson gaps remain tracked in TODO. + +# Owner-briefing dashboard checkpoint (2026-09-30) + +The actual Console and isolated demo now share an escaped, read-only owner-digest +renderer. Owner decisions come first, then deployment state and expandable +shipping, integration, decisions, blockers and coverage evidence. Missing, +stale, future-clock and partial reports are labeled; raw Markdown is not executed. +Live demo approvals do not rewrite a previously published briefing. + +PASS: all 38 source acceptance groups (tests/lint/syntax), receipt +`/tmp/torch-owner-briefing-final-20260930-acceptance.json`; real Playwright demo +desktop/mobile interactions with zero live API calls; diff hygiene. Screenshot +review led to a mobile sticky-navigation anchor-spacing fix. Resize measurements +wait two animation frames for responsive layout; strict width assertions remain. +Screenshots: `reports/design-system/torch-dashboard-owner-briefing-desktop.png` +and `reports/design-system/torch-dashboard-owner-briefing-mobile.png`. + +No agent, timer, installation, external notification or deployment was started. +Live browser/GPU frozen-input qualification, real delivery adapters, safe fetch +retries, richer queue/activity views and opt-in external notification remain open. + +# Read-only check evidence dashboard checkpoint (2026-09-30) + +The actual Console and shared isolated demo now expose frozen-input commit and +digest, recorded copy strategy, invalidation reasons and before/after condition +observations through expandable check rows. Unconfirmed running preparations +remain visible without implying executor liveness or offering automatic reruns. +Project-scoped observation supports older databases without migrating them and +does not follow artifact paths. Private capture paths/file inventories are omitted. +Linked capture evidence resolves independently of the bounded preparation list. + +PASS: all 38 final source acceptance groups (tests/lint/syntax), receipt +`/tmp/torch-check-evidence-final-20260930-acceptance.json`; focused projection and +renderer scenarios; real Playwright demo desktop/mobile checks, zero live API +calls; diff hygiene. Screenshot reviewed: +`reports/design-system/torch-dashboard-check-evidence.png`. +Frontend-design guidance kept the existing dark interface and progressive +disclosure rather than adding another control surface. No existing assertion +was weakened. No provider, timer, stable install, fleet activation, deployment or +COMBATRIG mutation occurred. Actual queued browser/GPU runtime qualification +and richer activity/deployment outcome views remain separate unfinished gates. + +# Commit activity dashboard checkpoint (2026-09-30) + +Live read-only snapshots now expose bounded managed Git activity using canonical +project policy. Task creation time and originating owner identity survive the +backlog projection. The shared actual Console/demo view shows owner requests +first, separates stale work from unknown history, retains blocked waiting context +and labels closure mentions as intent. Neglected owner requests receive attention +links; no duplicate queue or state mutation was added. + +PASS: all 38 final source acceptance groups (tests/lint/syntax), receipt +`/tmp/torch-dashboard-activity-final-20260930-acceptance.json`; real Git and HTTP +snapshot regression; actual Playwright desktop/mobile dashboard checks with +zero live API calls; diff hygiene. Screenshot reviewed: +`reports/design-system/torch-dashboard-activity-review.png`. +Design guidance kept the work board authoritative and used compact progressive +disclosure with bounded text lines. No assertions were weakened. No provider, +timer, installation, fleet activation, deployment or COMBATRIG mutation occurred. +Daily automatic review and non-disruptive live refresh remain unfinished. + +# Safe canonical fetch checkpoint (2026-09-30) + +Added owner-confirmed `forge fetch plan`, `forge fetch --yes --attempts 3` and +read-only receipt status. The resolved canonical SHA is imported as objects only: +no implicit refmap, tags, FETCH_HEAD, submodule recursion or publication. +Recognized transient transport errors retry up to the explicit one-to-five bound; +authentication/unclassified failures stop. Canonical destination hashes and policy +are refreshed per attempt; execution uses the pinned URL. Raw URLs/error text +are not persisted. Attempt reservations precede execution; terminal state and +owner audit are atomic. Snapshot projection includes the last 20 operations. + +PASS: all 38 source acceptance groups (tests/lint/syntax), receipt +`/tmp/torch-canonical-fetch-20260930-acceptance.json`; real disposable bare Git +object import, unchanged refs/worktree, actual CLI confirmation/status, +authentication/exhaustion/policy-change boundaries; diff hygiene. No assertions +were weakened. No real project fetch, remote push, deploy, provider, timer, +fleet start or stable install occurred. Initial head discovery fails closed +without retry. Real network classification and dashboard attempt presentation +remain open; fetch receipts are not release/deploy evidence. + +# Recorded operation outcome dashboard checkpoint (2026-09-30) + +The shared actual Console/demo now displays bounded canonical fetch and delivery +operation/attempt outcomes, with uncertainty and unconfirmed-running labels. +Owner attention links unresolved delivery actions to the view; no retry/recovery +control was added. Read-only project-scoped receipt projection exposes only +recognized status/classification/effects and TORCH error categories, not arbitrary +references/raw error text. Malformed receipts remain unavailable, adapter success +is not live release proof and object import success is not deployment. + +PASS: all 38 final source acceptance groups (tests/lint/syntax), receipt +`/tmp/torch-operation-outcomes-final-20260930-acceptance.json`; nine focused +Console/receipt/fetch tests; real desktop/mobile Playwright demo, zero live API +calls; diff hygiene. Screenshot reviewed: +`reports/design-system/torch-dashboard-operation-outcomes.png`. +Browser testing caught a real mobile heading/status overflow; production CSS +wraps that row without weakening document-width assertions or hiding overflow. +Design guidance kept read-only evidence compact and expandable in the existing +dark interface. No provider, timer, fleet activation, installation, real fetch, +deployment or COMBATRIG mutation occurred. Live adapters and draft-preserving +dashboard auto-refresh remain unfinished. + +# Protected live dashboard refresh checkpoint (2026-09-30) + +The actual Console and shared demo refresh visible snapshots every 15 seconds, +with pause/resume, a ten-second read deadline and single-flight/coalesced reads. +Explicit newer requests discard stale responses. Hidden views skip polling and +page lifecycle handlers tear down/restart the timer without duplication. Failed +reads retain displayed evidence; unsupported live snapshot schemas are rejected. + +Edited/focused controls and active preview DOM remain intact while unrelated +read-only panels update. Retained panels are explicitly labeled potentially old; +existing server-side token/revision checks remain authoritative. Successful +actions release only their own editor. Clean-editor baselines follow fresh +snapshot values, including successive external model changes; matching expanded +details remain open. No preview is reissued or confirmed by polling. + +PASS: all 38 final source acceptance groups (tests/lint/syntax), receipt +`/tmp/torch-live-refresh-verified-20260930-acceptance.json`; controlled deferred +read races; actual desktop/mobile browser demo with zero live API calls and a +controlled clock proving draft/preview preservation, independent panel updates, +pause/resume, hidden-state/page-event handling and unrelated-draft survival after +approval. Screenshot reviewed: `reports/design-system/torch-dashboard-live-refresh.png`. +No assertions were weakened or sleeps added. Design guidance kept the existing +dark interface with one small status indicator and pause control. No AI provider, +host timer, installed fleet, deployment or COMBATRIG operation was started. +Native back-cache/background behavior and installed-fleet activity remain +separate qualification work. Next prioritize a fresh specification/readiness +audit before enabling TORCH self-hosting, rather than inferring release readiness +from these source/dashboard gates alone. diff --git a/reports/audit/readiness-audit-2026-09-30.md b/reports/audit/readiness-audit-2026-09-30.md new file mode 100644 index 0000000..936e475 --- /dev/null +++ b/reports/audit/readiness-audit-2026-09-30.md @@ -0,0 +1,102 @@ +# TORCH readiness audit — refreshed 2026-09-30 + +Later checkpoints qualified frozen/same-runtime/browser-interruption scenarios, +implemented scheduled stale-work review and surfaced cleanup outcomes. See the +individual current reports and `self-host-roster-review-2026-09-30.md`. The latest +pending intake has 15 baseline domains, 13 unassigned components and 41 boundaries; +the 08:34 14/14/39 snapshot below is retained as historical evidence. No source +commit, stable install or live fleet lifecycle has occurred since this snapshot. + +This is a source/readiness audit, not proof that the full goal is complete. +The authoritative release definition is specification section 32: a real, +local-only multi-domain fleet must assign, coordinate, verify, integrate, +stop/resume and detach without losing identity or damaging user code. +Source tests and an isolated dashboard demo do not prove that exit gate. +The final trial must specifically include at least two coordinating domain +managers; a one-manager/two-specialist rehearsal is insufficient. Current +read-only status revalidated after bootstrap fixes still has generation zero, +no installed versions or stable launcher and `CANDIDATE_SOURCE_DIRTY`. + +## Current authoritative state + +- Rewrite branch: `rewrite/portable-agent-fleet`; committed HEAD + `d31437a4fb255c1e307bbb70ee9513cc1e0bdf4c`; extensive tracked/untracked + implementation changes remain uncommitted. No changes were discarded. +- `main` and `legacy/nostr-torch` both resolve to + `a46832314f6a32c6636ae8ff672df8c3e67e2672`. The preserved package explicitly + describes the former Nostr task-locking product. Legacy development-network + branch resolves to `b026a6e6fcd855b9fd158c388629d93f297e486c`. +- Current `candidate status`: generation zero, no installed/active/previous + version and no stable launcher. Current candidate planning rejects the + checkout with `CANDIDATE_SOURCE_DIRTY`; it copied no candidate. +- Fresh bootstrap intake: `/tmp/torch-selfhost-intake-20260930-0838.json`, + generated `2026-09-30T08:34:22.289Z`, fingerprint + `bcd69eb6048d5cca2e89f2eaa32440f21a4bfdc9876f345bb244a02645817467`. + It contains **14 candidate domains**, **14 unassigned components** and + **39 coordination boundaries**. This supersedes the older ten-domain, + eighteen-unassigned, twenty-four-boundary assessment. It is pending review, + not an approved organization or a request to launch fourteen agents. +- Unassigned components: `bin`, `core`, `scripts`, `src`, `src-artifacts`, + `src-console`, `src-control-plane`, `src-convergence`, `src-forge`, + `src-importers`, `src-integration`, `src-resources`, `src-self-host`, + `src-telemetry`. These include essential implementation surfaces. +- Report/doc edits after intake can invalidate its fingerprint; regenerate + after the final source checkpoint. Do not install from this temporary brief. +- The protected pre-existing `test-torch-config-host-uQKdqL/` remains outside + managed trial scope. Candidate source selection excludes untracked files; + a scoped checkpoint must include required new source/assets/tests without + accidentally adopting that fixture. + +## Requirement-to-evidence map + +| Requirement | Current evidence | Remaining exit gate | +| --- | --- | --- | +| Legacy preservation and replacement product | Actual branch hashes above; rewrite source and product pages | Scoped checkpoint and eventual replacement promotion to `main`; no remote push authorized by this audit | +| Portable install/reversal on existing repositories | Real Git fixtures in `kernel-lifecycle.test.mjs`: `SCN-install-doctor-purge`, `SCN-existing-repository-preservation`, `SCN-install-trackability` | Committed release artifact and actual installed tool qualification | +| Repository/spec-aware project-specific bootstrap | Architecture graph, fingerprinted specifications, pending six-area consolidation, duplicate-primary ownership rejection and specialist-only repository scenarios | Regenerate after checkpoint; owner review of exact roster and root metadata/schema boundaries; a paid Architect call is optional, not an approval prerequisite | +| Durable provider-independent control plane | SQLite identities/messages/approvals, identity-bound stdio MCP handshake and CLI scenarios | Real manager plus specialists complete a coordinated change | +| Current instructions and resume-first backlog | `SCN-current-instructions-on-resume`, single-active assignment/self-claim/race and blocked-resume tests | Live stop/resume and autonomous settle/claim behavior | +| Claude/Codex/Pi/custom runtime choice | Mixed adapter planning and local MCP/extension boundaries; per-identity profiles/defaults | Live mixed-provider coordination/resume with explicit usage limits; offline extension loading is not a provider turn | +| Local checks, scarce resources and frozen inputs | Exact-SHA checks, FIFO leases, real queued Chromium capture/rebuild, same-runtime condition probes and Linux browser-descendant interruption qualification | Actual project GPU/game probes, disk allocation, transient drift and arbitrary/other-platform harness interruption qualification | +| One serialized integration path and local-only canonical Git | Real disposable Git integration/closure and conflict/failure recovery scenarios | Real fleet submits independently tested branches; no direct pushes to main; actual interruption trial | +| Forge attach/detach/outage and safe fetch | Real local remotes in `forge-migration.test.mjs` and `canonical-fetch.test.mjs`, owner-confirmed non-publishing object import | Real network classification; local remote fixtures do not prove hosted authentication or outage behavior | +| Adaptive hierarchy and manager check-ins | Graph authority tests, pressure/pilot/adoption/reversal scenarios; bounded wake reservations and typed owner-digest scheduling | Actual fleet-pressure evidence, live adoption/reversal, owner-authorized systemd install/dispatch/reversal and stopped-runtime proof | +| Delivery lifecycle, uncertainty and retries | Durable attempt reservations, classifications, saved-success/not-applied recovery and independent authority boundaries | Real project adapters, provider receipt identity/idempotency and stopped-executor qualification; no public release inferred | +| Stable/candidate isolation, upgrade and rollback | Disposable HOME/XDG gates, version-store fixtures, atomic pointer/journal/rollback tests | Install a commit-bound accepted candidate; stable TORCH actually manages its successor on this repo | +| COMBATRIG portability | Read-only importer plus legacy fixture compatibility and preserved prompt/check/authority provenance | Old 56-blocker report is historical, not a current COMBATRIG audit; separately authorize fresh intake, resolve ownership, compare and cut over | +| Website, actual dashboard demo and owner controls | Shared Console/demo, actual Playwright desktop/mobile interactions, protected live refresh, source HTTP tests | Public/package release approval; native background/back-cache and real installed-fleet qualification | + +Configurations A–I in specification section 29 remain only partially qualified: +local Git/kernel/forge fixtures cover mechanics, but A/G/H/I explicitly require +real operational fleet evidence. None is upgraded to complete merely because +the matching source scenarios pass. Section 32 remains **not achieved**. + +Fresh verification: all 38 required source acceptance groups PASS, including +full tests/lint/syntax, receipt +`/tmp/torch-readiness-audit-20260930-acceptance.json`. Actual Playwright +desktop/mobile demo and controlled protected-refresh scenarios PASS with zero +live API calls. Diff hygiene PASS. These checks use local/disposable or demo +state and deliberately do not satisfy the live-fleet exit gates above. + +## Ordered next work + +1. Preserve completed local qualification: real frozen queued-browser execution, + same-runtime probes, Linux interruption and scheduled stale-work mechanics now + have source evidence. Remaining real project/provider/installed cadence gates + must not be substituted with another repetition of those source scenarios. +2. Review the accumulated source scope and required new files, preserving the + protected fixture and parallel work. Produce a clean commit-bound checkpoint; + do not weaken dirty-source acceptance or substitute an artifact-mode build. +3. Generate a fresh TORCH brief and review ownership for every omitted component, + plus shared CLI/schema/package/test interfaces. Fewer coherent specialists + are preferable to blindly approving the deterministic fourteen-domain roster. +4. Build/test the committed candidate and review tool activation/project-install + plans separately. Installation must not implicitly start agents or timers. +5. Once ready, obtain explicit provider/budget authority for the section-32 + bounded live trial, then separately qualify timers, adapters and COMBATRIG. +6. Promote the replacement/default branch and publish/deploy only under the + corresponding release authority. Retain rollback/legacy recovery boundaries. + +No provider, host timer, stable installation, project fleet, remote publication, +deployment or COMBATRIG mutation was started during this audit. Remaining safe +implementation work exists; this is not a blocked-goal or completion claim. diff --git a/reports/audit/same-runtime-browser-qualification-2026-09-30.md b/reports/audit/same-runtime-browser-qualification-2026-09-30.md new file mode 100644 index 0000000..313b563 --- /dev/null +++ b/reports/audit/same-runtime-browser-qualification-2026-09-30.md @@ -0,0 +1,46 @@ +# Same-runtime browser qualification — 2026-09-30 + +The explicit qualification service holds one real Chromium page across three +actual subprocess commands: preflight probe, measurement and postflight probe. +It serves captured build files from a disposable project's snapshot. The page +generates its own random runtime identity and advances a simulation counter on +animation frames. Probes observe DOM build/commit, actual clock advancement and +visibility; they do not simply return expected policy values. + +Seven focused scenarios PASS, zero skips: + +```sh +TORCH_QUALIFY_BROWSER=1 node --test --test-name-pattern='SCN-real-browser-conditions|SCN-real-frozen-browser|SCN-check-' test/fleet/verification-resources.test.mjs test/fleet/check-conditions.test.mjs +``` + +The actual CheckService produces: + +- Healthy: pass with identical browser identity before/during/after measurement. +- Frozen before preflight: incomplete, zero measurement requests. +- Clock freezes during measurement: command exits zero but postflight invalidates + its result; exact-pass lookup remains false. +- Runtime replaced during measurement: replacement clock/build conditions are + healthy, but changed browser identity invalidates the result. + +Runtime continuity was previously only a harness obligation. TORCH now rejects +changed or missing postflight `runtimeId` when preflight supplied one, and rejects +malformed supplied IDs before measurement. IDs remain project-reported metadata; +absence in both probes remains backwards-compatible, not proof of continuity. +Exact-pass qualification revalidates stored reports under the current contract; +historical green flags with mismatched runtime IDs cannot bypass it. The real +scenario verifies that rejection through stored receipt lookup as well. + +Browsers and loopback servers terminate through explicit IPC shutdown; completed +snapshots are removed. No AI providers, fleet sessions, host timers, deploys or +stable installs were started. Focused lint and diff hygiene PASS. + +Full isolated current-source acceptance PASS (38 groups, tests/lint/syntax): +`/tmp/torch-same-runtime-browser-final-20260930-acceptance.json`. + +## Remaining gates + +This proves one persistent harness, not arbitrary project adapters. It does not +qualify GPU acceleration, COMBATRIG game time, transient drift that recovers +between probes, independent sensor truth, hard-killed executor recovery or disk +allocation. Projects must instrument drift through the measurement interval; +endpoint probes alone cannot prove uninterrupted validity. diff --git a/reports/audit/self-host-readiness-2026-09-30.md b/reports/audit/self-host-readiness-2026-09-30.md new file mode 100644 index 0000000..3a8d2b7 --- /dev/null +++ b/reports/audit/self-host-readiness-2026-09-30.md @@ -0,0 +1,116 @@ +# TORCH self-host readiness + +Later refresh: `readiness-audit-2026-09-30.md` and +`self-host-roster-review-2026-09-30.md` supersede the intake counts below. +The latest baseline snapshot has 15 candidate domains, 13 unassigned components +and 41 coordination boundaries. A six-specialist consolidation passed semantic +validation with zero unassigned analyzer components, but subsequent source fixes +make that temporary proposal stale. The original 05:04 snapshot remains historical; +no proposal has owner approval. Fresh read-only candidate status still shows +generation zero, no installed/active version and no stable launcher; candidate +planning still refuses dirty source. Regenerate intake after the approved checkpoint. + +## Scope and current evidence + +This is a pre-install review, not installation or live fleet qualification. +Read-only checks on 2026-09-30 observed: + +- Branch: `rewrite/portable-agent-fleet`. +- HEAD: `d31437a4fb255c1e307bbb70ee9513cc1e0bdf4c`. +- `node bin/torch.mjs candidate status --json`: no installed version, + active version, previous version, or stable launcher; generation zero. +- `node bin/torch.mjs candidate plan --source . --json`: correctly rejected + the checkout with `CANDIDATE_SOURCE_DIRTY`. No candidate was copied. +- Fresh repository/spec intake: + `/tmp/torch-selfhost-review-20260930-0503.json`, generated + `2026-09-30T05:04:22.421Z`, working-tree fingerprint + `86f7bf26fd2be48789fd16bfa2b30bc014b886dfbb5f2842e8f4712a440b12e5`. + It was generated before this report; subsequent edits invalidate that + fingerprint for installation. Regenerate after the source checkpoint. +- Deterministic baseline: ten candidate domains, 24 coordination boundaries, + and four discovered project checks (`test`, `check`, `test-dashboard`, + `lint`). Baseline review remains pending. + +The intake command writes only a temporary brief; it does not call the AI +Session Architect, approve ownership, install a project, or start providers. +Temporary artifacts are convenient review evidence, not durable release +receipts. Preserve final acceptance receipts in the version store when a +specific committed candidate is qualified. + +## Roster is not ready for approval + +The baseline is a starting point for the Session Architect, not an automatic +instruction to launch ten specialists. Its review focus explicitly identifies +18 unassigned components, including the Console backend, control plane, +backlog, integration, self-host lifecycle, and CLI entrypoints. An empty +`missingOwnership` list means each *selected* domain has paths; it does not +mean every repository component has an implementation owner. + +Before approving the real proposal: + +1. Assign the omitted components to coherent owners or explicitly retain them + as owner-managed, out-of-trial surfaces. Do not hide unresolved ownership. +2. Define ownership/protocols for CLI, package metadata, schemas, documentation, + shared test infrastructure, and cross-domain contracts. +3. Keep repository-wide checks separate from specialist-local checks. +4. Exclude the pre-existing `test-torch-config-host-uQKdqL/` fixture from the + managed trial and candidate. Do not edit, remove, or adopt it. +5. Review collision protocols rather than accepting every manager-coordination + suggestion as a resolved implementation boundary. + +## Ordered path to the first trial + +1. Review the combined local source changes and create a scoped, clean source + checkpoint. This requires inspecting both modified tracked files and new + runtime/test files; a blanket add-all risks including the protected fixture. + Do not reset or discard existing work to obtain cleanliness. +2. Build and validate a commit-bound candidate. Current-source acceptance and + older artifact-mode receipts do not qualify that new version. +3. Refresh intake against the final checkpoint; validate and review a complete + project-specific proposal. A paid Architect call requires provider/budget + approval; deterministic intake does not. +4. Review candidate activation and project installation plans separately. + Installing the tool must not implicitly launch the fleet or host timers. +5. Install the accepted version and stable launcher, then the approved project + configuration. Verify `doctor`, roster, worktree and runtime plans while all + provider sessions remain stopped. +6. Obtain explicit approval for the complete section-32 bounded live trial: the + owner-facing coordinator, at least two domain managers and the selected + specialists needed for one real cross-domain task. The proposed Platform and + Workflow/Experience leads must be reviewed and activated through the hierarchy + pathway separately; a proposal's assessment alone is not an active graph. + A one-manager/two-specialist run may be useful rehearsal, but cannot satisfy + section 32. Keep other sessions dormant + and persistent timers disabled initially. Select providers/models per + session and set an invocation or genuinely enforced spending boundary. +7. Run the scenario below; only then qualify scheduler installation and + interrupted-wake recovery as separate operational trials. + +## Live trial acceptance scenario + +Record exact task IDs, session IDs, messages, revisions, tested commit SHAs, +integration results, and lifecycle receipts. Require observable evidence for: + +- Each specialist receives current on-disk instructions and one assignment. +- Specialists coordinate directly across one real interface boundary. +- At least two domain managers exchange durable coordination on that task's + integrated outcome, preserving specialist ownership and direct peer communication. + Record lead identities, graph revision, decision scope and coordination evidence; + specialist messages or a roster merely listing managers do not prove this gate. +- The manager discovers a waiting approval/blocker and resolves or escalates + it without overriding an owner-only decision. +- Both branches submit exact tested tips through the serialized integration + path; nobody independently pushes main. A conflict or failed check returns + to its author without silently dropping or landing unverified work. +- A stopped/resumed specialist retrieves its existing assigned task before + another ready task and reloads current instructions. +- Shutdown records current state and leaves no unidentified live provider + process. Resume preserves identity, task and durable message history. +- Capture total usage and coordination overhead. Cache savings remain an + unproven hypothesis unless the runtime supplies comparable measurements. +- Verify detach/uninstall plans preserve user code and clearly distinguish + removing project infrastructure from removing the installed tool version. + +Failures are qualification findings, not reasons to weaken checks or silently +restart sessions with unknown process state. COMBATRIG cutover, publishing, +deployment, and larger-fleet scaling remain separate gates. diff --git a/reports/audit/self-host-roster-review-2026-09-30.md b/reports/audit/self-host-roster-review-2026-09-30.md new file mode 100644 index 0000000..cf049b6 --- /dev/null +++ b/reports/audit/self-host-roster-review-2026-09-30.md @@ -0,0 +1,151 @@ +# TORCH first self-host roster review — 2026-09-30 + +Status: proposed ownership map, not an approved or installable fleet proposal. +No provider, session, project install, stable activation or host timer is authorized +by this document. Existing source edits and legacy branches remain untouched. + +## Why not install the deterministic baseline as-is + +The previous intake proposed 14 specialists but left 14 components unassigned, +including CLI, control plane, Console, integration and self-hosting. Those omissions +include the tool's essential implementation. The baseline is evidence for design, +not approval of a complete organization. Broad source roots and small files do +not require one standing agent per directory. + +A read-only file enumeration and unique-owner check covered **124 current files** +under `src`, `bin`, `site`, `schemas`, `scripts` and `test/fleet`, with zero missing +or duplicate primary mappings in the proposed grouping below. This is actual-file +coverage, not validation of a finalized domain-proposal JSON or its future globs. +New files require refreshed coverage and an explicit ownership decision. + +## Six coherent specialist areas + +| Area | Current primary implementation surfaces | Files | Integrated outcome | +| --- | --- | ---: | --- | +| Project kernel | `src/kernel/`, `src/design/`, `src/evolution/`, `src/control-plane/` | 19 | Analyze arbitrary projects, validate reviewed organization, install/reverse safely and preserve durable identity/authority | +| Provider runtime | `src/adapters/`, `src/runtime/`, `src/mcp/`, `src/schedules/`, `src/telemetry/` | 15 | Start/resume the selected local runtime with fresh instructions, durable communication and honest usage/wake limits | +| Work and integration | `src/backlog/`, `src/checks/`, `src/resources/`, `src/integration/`, `src/convergence/`, `src/canonical/`, `src/forge/`, `src/delivery/`, `src/importers/` | 15 | Move one verified exact-commit task through scarce resources and serialized landing without losing recovery evidence | +| Owner Console | `site/`, `src/console/`, `src/artifacts/`, `src/observability/` | 21 | Show the same real project state to the owner, preserve drafts, capture feedback and make consequential controls explicit | +| Release and self-host | `bin/`, `src/cli.mjs`, `src/self-host/`, `schemas/` | 4 | Package a commit-bound tool, expose usable commands and manage stable/candidate upgrade, rollback and removal | +| Independent QA | `test/fleet/`, `scripts/` | 50 | Own scenario integrity, real qualification harnesses and cross-domain acceptance evidence | + +Remaining repository-level files are explicit shared boundaries rather than +forgotten components: package/lock metadata, `.gitignore`, README and distribution +configuration belong primarily to Release and Self-host; `eslint.config.mjs` and +test-integrity records to QA; architecture/specification documents to Project +Kernel; the roadmap/owner priorities to Fleet Operations. Evidence reports have +an originating author and QA custody, not their own development specialist. + +The analyzer's `core` component is a root-file grouping (for example ESLint +configuration), not evidence that a legacy `core/` subsystem needs a new agent. +The `src` root component includes the CLI. Neither should silently remain ownerless. + +## Shared boundaries and implementation authority + +- One primary owner per implementation surface. Specialists can ask peers directly; + a management layer must not become mandatory message routing. +- QA owns test integrity, while specialists propose/add regression scenarios with + QA coordination. This is not permission to weaken tests or block ordinary peer work. +- CLI/schema/package changes need Release coordination plus affected service owners. +- Config, durable-state schema, MCP identity and organization changes need Project + Kernel and Provider Runtime coordination before landing. +- Check/landing/resource/delivery policy is owned by Work and Integration; Console + observes those authorities rather than inventing a second task/gate system. +- COMBATRIG importer changes do not authorize reading live private fleet state, + cutting over COMBATRIG or modifying its repository. +- Protected `test-torch-config-host-uQKdqL/` is excluded from fleet adoption, + checkpoint scope and trial cleanup. It is not a new specialist's work area. + +## Coordination shape for the release trial + +Define Fleet Operations separately from product sequencing. A proposed Program +Director is the owner-facing coordinator. Two outcome-owning domain leads can +exercise specification section 32 without claiming specialist implementation: + +- Platform Lead: coordinate Project Kernel, Provider Runtime and Release/Self-host + toward a portable installed/resumable tool. +- Workflow and Experience Lead: coordinate Work/Integration and Owner Console + toward an observable verified task-to-landing loop. +- QA remains a horizontal independent service reporting operational findings to + Fleet Operations and relevant leads, not another implementation manager. + +These are proposed roles requiring review, not automatically created managers. +All areas may be defined while only a bounded subset runs. Do not launch six +specialists plus management simply because they are listed here. The trial should +activate only roles required for its approved task and coordinate the two leads +without duplicating code ownership. Other specialists remain idle or offline. + +Codex defaults remain GPT-6-Luna/high; Claude defaults remain Sonnet. Per-session +provider/model overrides and mixed providers remain user choices. Automatic wakes, +daily reviews, self-claim and automatic landed closure should start disabled until +their specific trial policies are approved. No spending limit is implied by a +count-only wake budget. + +## Order of operations + +1. Review the exact accumulated source file scope; obtain owner direction for a + local checkpoint. Preserve `main`, legacy branches and the protected fixture. +2. Commit only reviewed TORCH source/assets/tests/docs, no remote push. Re-run + committed candidate acceptance; the dirty-source gate must not be bypassed. +3. Regenerate the repo/spec intake after the checkpoint. Convert this map into a + fully validated pending proposal using paths/evidence available in that brief. + Resolve any glob, shared-surface and organization-graph validation issues. +4. Owner reviews/approves the exact proposal, roster, runtime choices and installation + plan. Build/install/activate the candidate without launching agents or host timers. +5. Separately obtain explicit provider budget/turn authority for the bounded live + trial: real task, two leads coordinate, specialists verify exact commits, landing, + stop/resume, preserved identity/work, detach and rollback. +6. Record evidence for specification section 32; source tests alone do not complete + it. Default-branch promotion, remote publication, deploys and COMBATRIG cutover + remain separate decisions. + +Current stable status remains generation zero, no installed version or launcher. +Source qualification is green, but the rewrite is uncommitted and no live fleet +lifecycle has been qualified. This review does not claim completion. + +## Pending JSON validation follow-up + +The pre-checkpoint brief at +`/tmp/torch-selfhost-intake-20260930-0952.json` now has a corresponding +human-authored, pending Architect-contract proposal: +`/tmp/torch-selfhost-six-domain-proposal-20260930-1000.json`. +It preserves repository/specification bindings, discovered checks/resources/ +schedules and the evidence graph. Six specialists consolidate source-component +paths, QA retains its baseline verification paths, and two proposed outcome-owning +leads have neither implementation paths nor owner-approval authority. Existing +cross-domain boundary observations are remapped rather than silently discarded. + +`torch architect validate` reports valid, no problems, no unassigned analyzer +components, owner review required and no mutation. Proposal SHA-256: +`eb450375b197acb438e38f603f22b28466900dc807ae80feaadea4706622d326`. + +This is semantic validation, not full-repository coverage, organization activation +or release approval. The analyzer permits the observed schema as a shared surface; +package/docs/schema primary ownership remains an explicit review boundary. Root +`*.mjs` and `src/*` component globs correspond to current ESLint and CLI evidence, +not permission to adopt unseen future files. Protected fixture observations remain +in the unchanged evidence graph but receive no owned/shared adoption paths. +Regenerate the proposal after an approved checkpoint; do not install this dirty-tree +artifact or interpret pending coordination roles as authorization to launch them. + +## Bootstrap collision guard follow-up + +The bootstrap validators previously accepted identical primary claims even though +later Fleet evolution rejected collisions. Both pending Architect validation and +approved-install validation now share a provable-primary-overlap check: identical +normalized patterns, `**`, and literal recursive parents such as `src/**` cannot +be owned by different specialists. Shared consultation remains allowed. The check +does not claim to solve arbitrary glob intersections. No owner approval or collision +coordination note overrides one-primary-owner semantics (specification section 12). + +Twenty-two focused bootstrap/domain/lifecycle scenarios PASS, including a new +required acceptance marker. Focused ESLint and full isolated source acceptance +PASS; receipt: `/tmp/torch-bootstrap-ownership-20260930-acceptance.json`. + +The source fix invalidated the earlier pre-checkpoint intake. Its refreshed pair +is `/tmp/torch-selfhost-intake-20260930-ownership.json` and +`/tmp/torch-selfhost-six-domain-proposal-20260930-ownership.json`, replacing the +09:52/10:00 artifacts for current review. Refreshed semantic validation is valid +with no problems or unassigned component findings; pending proposal digest: +`e093821ada37d09876deff95508f951c33eefebda3f7034562416d4cc3cf3ca0`. +No checkpoint, install, activation, provider invocation or host timer occurred. diff --git a/reports/audit/specialist-only-bootstrap-2026-09-30.md b/reports/audit/specialist-only-bootstrap-2026-09-30.md new file mode 100644 index 0000000..e019420 --- /dev/null +++ b/reports/audit/specialist-only-bootstrap-2026-09-30.md @@ -0,0 +1,36 @@ +# Specialist-only repository bootstrap + +The stricter initial ownership guard exposed a baseline planning defect: with no +implementation components, the analyzer added Core owning `**` before creating +evidenced QA or release roles. The generated roster therefore contradicted the +one-primary-owner invariant and could not pass approved-install validation. + +`proposeDomains` now evaluates QA and release evidence before using Core fallback. +Test-only projects receive QA; release-only projects receive Release; projects +with both receive both, with disjoint ownership. An empty manifest-only project +still receives Core. Future implementation responsibilities can be proposed and +reviewed through the existing evolution pathway instead of inventing a standing +catch-all implementation agent beside horizontal specialists. + +`SCN-bootstrap-specialist-only` first reproduced the defect, then passed across +four real disposable committed repositories. It verifies exact proposed domain +IDs, successful approved-proposal validation and an unchanged Git worktree. +Existing primary-collision rejection was not weakened. The scenario is required +by source candidate acceptance; integrity notes are in `docs/TEST_INTEGRITY.md`. + +Verification: + +- 23 focused domain-analysis, Architect and kernel-lifecycle scenarios PASS. +- Focused ESLint and `git diff --check` PASS. +- Full isolated source acceptance PASS: 38 required groups, test/lint/syntax + statuses zero, receipt `/tmp/torch-specialist-only-bootstrap-20260930-acceptance.json`. +- `main` and `legacy/nostr-torch` remain at + `a46832314f6a32c6636ae8ff672df8c3e67e2672`; the older development-network legacy + branch remains at `b026a6e6fcd855b9fd158c388629d93f297e486c`. + +No source checkpoint, stable install, provider invocation, fleet startup, host +timer, COMBATRIG mutation, push or deployment occurred. The previous six-area +proposal remains a historical pending artifact: source changes invalidate its +binding. Regenerate the commit-bound intake/proposal after checkpoint approval, +rather than approving the stale artifact. Specification section 32 is still not +qualified by source scenarios alone. diff --git a/reports/audit/stale-work-review-2026-09-30.md b/reports/audit/stale-work-review-2026-09-30.md new file mode 100644 index 0000000..e499595 --- /dev/null +++ b/reports/audit/stale-work-review-2026-09-30.md @@ -0,0 +1,52 @@ +# Scheduled stale-work review — 2026-09-30 + +Manager check-in actions can opt into bounded `stale_work` review. This reads +the existing authoritative task ledger and managed Git history, prioritizes +owner requests, and scopes assigned work to direct reports. Fleet Operations +also sees unassigned intake. Blocked items retain expected-waiting context; +incomplete/truncated history does not prove inactivity. Compact summaries cap +items and titles, expose counts/coverage and never include full commit histories. + +Delivery writes one durable self-directed `manager-stale-work-review` prompt. +It deduplicates independently of normal check-ins and asks the manager to +refresh the read-only identity-bound MCP query before deciding. Revival, +deferral, dependency review and evidenced closure use existing mechanisms; +neither age nor commit messages grant mutation authority. + +Example opt-in schedule entry for an approved project config (NOT installed): + +```json +{ + "id": "daily-stale-work", + "title": "Daily neglected-work review", + "owner": "session-manager", + "lifetime": "system", + "trigger": { "type": "cron", "expression": "17 9 * * *" }, + "behavior": "coordination", + "action": { + "type": "manager-check-in", + "manager_id": "session-manager", + "stale_work": { "stale_days": 3, "max_commits": 1000, "max_items": 30 } + }, + "required_authority": ["owner"], + "retry": { "max_attempts": 1 }, + "failure_recipient": "session-manager", + "source_of_truth": "Owner-approved stale-work review policy" +} +``` + +Cron uses host-local time. No `wake` means inbox delivery only. Optional wakes +use existing explicit owner approval, exact timer-config and runtime-budget +guards; configuring this entry does not itself install a host timer. + +21 focused manager/schedule/MCP scenarios PASS, including real disposable +Git fleets, due-minute checks, approval refusal, one-message deduplication, +unchanged task bytes, unknown scan coverage and blocked-task preservation. +No host timers, provider sessions, deployments or stable installs were started. + +Full isolated current-source acceptance PASS (38 groups, tests/lint/syntax), +with focused lint and diff hygiene PASS. Final receipt: +`/tmp/torch-stale-work-review-final-20260930-acceptance.json`. + +Remaining: installed timer dispatch and real manager follow-through require a +separately approved live trial. A queued message is not evidence of a running AI. diff --git a/reports/design-system/torch-dashboard-activity-review.png b/reports/design-system/torch-dashboard-activity-review.png new file mode 100644 index 0000000000000000000000000000000000000000..5155b8fbdd1e75a56bb9c0188384878c8f5fb21a GIT binary patch literal 55673 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 z za~<8kVCsBz>HceZ@#Qkc}NKpu#hvD}GjWli4EA@Np8BdeYLHqV4wTh??M_N0n z(@FG(PB>#YP zY};{-S(G3%x)2IOH3G~MZ5>o{Z2{Msb}`9lsHNrp7%g9X8@fb z*YEz3a3qFuN3z))auBI}aenf$4FRfZuiUhqoYvks0Xv$&`7IqTAwnJ`iAhPLy;8ExkLHPb z`a+URfOr(h&;HER#(JB|M`)NA?3TfE==ZxdiW7bcOLBv*M<$sRE^Ao~kkZN6za)G3 z2hwEBmr5fWkr!WO@kSQD#eHO3TRkG8+v8rlnW9U^|FI{`cfT}r^{ORBOk zuaf&zf6At#4dl%tkP6sIQuFc!*7_2U(HYOO!Gz9G$}1Y$76|>`3dR4P( z&CQ8RcMQ}EeX884PJExadOK<^EY)~AaLFcfTnVt4Fqm=x_PD!hMknaF$ONJz$U`-( z!*Ew8!&x$M^V8bj!Sq(G?8#3#fvjtoyP~IO0z1VHUgoCD@zeEkcTdeHI(8rmbUSoqA`7sNO$6$*5Ir;VqXu|MTrl)4=rnT-Xe%d+lEUN z?Bi1b%_j4Y$)dY-@q5R1zLoCZ{bDOm@X}?!V2bMnu0kb0@pgNg(>0|>qz{Kgji1o4 zlR}yvns(biu0`wH;hw?X-AJzKt1VA;;ft97XI%PRv%&P4wGdC|c`~f#4~vLmIssVV zNblG(JYCoTy=Yb%qs=)$?-aDj)?(-jSnEt`A368zxfe|aW;LwfNi);}lFKQ{)Pu^w zEddv+`|9m;@pEX|u}$WdW=3H@EZ(J3@O&cHuY2COu}&zrJSN2VWnr9T_%IXNv@|m3 zMyO2;g9}IEG}+Tflie{KJ*#x#w~+1)OPpVPka_jQilU1&T*v%tq9w}N0hX}I0|q(T zE#46O*E4lS8*2oTEWa)?UDxWh#8pLfS#i(8SKC!ZsYdc0f0a6K*PiDGsfAS>pJzl5 zVkPI*n?9hXoC~DJ<5^#CB+^Z_N~AoCfVxu>aq^+Airj5I_LHuLna>=+IvJBLQFQ9loQ;}%-+g~8vZj6^LuqE9yxd8@RVF(^*13CFN zjyp~r$QavRrjYvXLv96P%G8EXIx(W5p?_$oO2w=RN+=y<8%Hl;FBBJy=ixnA^mS?Z zKlo8(KSpF1H;9UN7e2tyUi|e92^VB%UZaGpPl>W?@ko)0Z-&0C^Er%x3%cjGc*wgC zes2YfLAuG>%njf28Y9OwTD-|$+^u}h#MwMnR?Qi_5F4`2QDF82diOWzKK4R{qr865 zjhF2*{VSQze>PvW;=cXYUq%1%hzTJ7lMtjJ@{joq4f#L33{v#-u=adb=ypVxS@@wk z7Mfc4G&5GB8Ta>FzZ(7*x8UD)Cau9sP)Xx&I=qc%e-I|UHSJxsPvfgZ*R*V6glgDm zk3?t>o|ao;b(Usuaf~`R9_^KPx}$XweP8$F?4g9-l^_32dB5A^!%4p&%Psp-v%cPDt?m`-hAj12 zU~$s8QT5rqlby#V?RbCn#B{-V3Y`@}cXo5G+`{L3>+J1y?73$3L5Uf!_LY9u^xpPm zua^doEOTz^Mfbb=qn~N4y%?aSwq3O;B|ESG-lbd{*}Q!-171kyY(FNG6aIOwKrCqF zYsb5ow;dflz+JhMYO>DvElIJMqmt#Ddd5k~bHgvMwM_~uiZ*xgxv@m0Ub<9MVX9@Y z?BMa_(kq`XoW1?eV(HehZ@e}^mvi67AG_3f!Rz(f-N#;h`Z9NBg~=h$-GORrb9Y$Z zTEt_QA^ky36+F+zut1{LXO+)%?a)2TmoLtrSS>BRcP_U|By8a1I?K=d_ckiJJ%)^( zR7{%Gc}Q&gSx?4rlLeP51aPC1~{$PCEnJjWag!?7n}d=gungH1STYkn}5OG*8>! zuTuHd{AaUK;PD`pwG)<00K1`ysmH%GJwJK(nip?zM&Pmu9IdQT`C*1V=hiG;w9nN) zB1Mw0`%-#u&dcb2X>tIz(Co?G_)`TSir zo}Q1cyZw(yUiYfVCG+)#J?XnHS3JlDsW*TLcx|yE* zd3oZh8TQ++__=6yO{!3svs6S!^t-FZ;j3ZB@7N&Q`WJnhc*1?B$l7b#FPWlcZZ)KY zYPtEil`rR8`u4@A@V(b>u{B@0dCcqUf6rr;6M<9xtU)4^ve%v8J!}2>5362T#h%>v zU8OGBRBM&b^Q@8-C4na`Yd@T7Te8V4Z2rQ@2MdC{{Qs!3cvZS>oh|EjPWqs_kki7{ zd5@;$7j1a|ZNdCCuisiOcangt0XX_vxs&_MugR}&&3kvNNObWPDJ`A0+RLdgd8a+) z-L*;jMfR>=Q6)|J$Am>WmhJg@Om+3T7N7REC}r<%8+{MXe&YkHU$e98&D^Tm^J&Y{ zg_oz*J<JJHlN+nfL0PM6^K4wK`~G1T7rR#W-ab@@c-#B zyt`o$();fn-5S5s!r=08Ax*tU8_WCB<-Qkvk(qyl-R<3(4EgOhb330EHZI6?P4fiK zy8wa1#~+}Y3X;B3j6L5e(G{an^LB{Ts5OTc{b literal 0 HcmV?d00001 diff --git a/reports/design-system/torch-dashboard-check-evidence.png b/reports/design-system/torch-dashboard-check-evidence.png new file mode 100644 index 0000000000000000000000000000000000000000..094e4f46dc48ce38dfff8da0fe0ca02810eae1d2 GIT binary patch literal 119022 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

Dy;!%XKQhc_TcJnHC!!&Or== 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
aJZGEBptGe10hK6xYMH}%e1^E?0i%-?XL%Rn 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

R!}GfeCpou^A*X4Gr9p_0C>_-z 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

x(Lp)J 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 Ae*gdg literal 0 HcmV?d00001 diff --git a/reports/design-system/torch-dashboard-demo-mobile.png b/reports/design-system/torch-dashboard-demo-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5f46e6bd5ecdabc100ea0b6e726aba2c21fa09 GIT binary patch literal 61683 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

ZHMTq%qPaqm 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 PBk$y-lqG8=OoILk1qnoH literal 0 HcmV?d00001 diff --git a/reports/design-system/torch-dashboard-live-refresh.png b/reports/design-system/torch-dashboard-live-refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..64c54a127fc296b3e0896172f25ea7a4b21accf7 GIT binary patch literal 152082 zcmc$`cT`i)*FI{;LJ<*As)B&@-a=Cm5D}2xL3-~cLV&19Z_+zR@4dtT0i{R@y+eS| zLx3c-5PE)o%J;qRx_7O+?z`^#-J8G8nlrO!&*Ygo`#jG+6aHRJiR>=j-78nFkSV`; zt$F3j&F@#PT;IHX?N3SZ=jzG} z;m|82|6YrLZoB=bxc+}%v9^Q#@0uljLH$bX0t_x9&N1=?lQovipN4LP5gVc30#PnRG(5mE%!2u9E*_@Z;!mOpSWKCy(+CiGND2e4my7&j_!i%lwauVJWwz zRg+z)0#CMa1*`~Jyo>6;MzbrL0#LZ)gK2xFiyOcQ7a zNXtAGndE-WU7iI~{MFU4qGN0;u&u9huKXzbmrbjuF>E5>3LkiPYA%St`e?xJYrh z@T3L8v_eDC17{#klFJ65&J^JjB_A?6cKM5XOotu z7I)57cXg*UO^=m=OU2bDF-L-jc+>t$Qv5zR=L7ed%e8o4*=l!=zfrz~9JL=Jo?I}M z4%JmMPDbH#n!92K^zUb#6wuX09hn5k?C?}S`1o*G?q@EgYx-Wh{E1&k7U9QmJHvQq z6|rZWEro_*pwL0l5%mf9J8Bp22CQLMfTA8Wf!o*hvCdnD)zigte2>jJ!{* zVrER_B14MH;~#4&0=a6f*t7XIs%ybV$!xwT;-##G77OEH?prkej%)i=CoCVvre&0v zk^B(m3rC_{bp4|YTS#w{Ki*jAgw>PfK6tkPu^z`JZWmZ0HE^Vio18lqN+KudJtMJ; z!<34z(p>dTi>!;Ax`AscNwyD9n_{k!c&ZwBM!UT4ceTlEJ$YmFUKLFIlroGG;rclm zF-^SSyk6-^jzjJm6@@fxpL?{_eGwX4OPf8qy@?c)U9G*&u%2Eqk*HXO=&v3A9=J|o zMRrSqF6{}To6y(>73bSnu>YO(;4jFgQ<`X;qSvStChFTSi{_%_NVgH4*x`3&lT*!> zKc%P?&+eI&=sMXroVy~#Im{+C2FH#9INnhldE$2LRPTC@tM@2eG;lP&QRgMliLy!b zjt`F3ZPp!kRq-M$Ju5>8E>1ncQm@!uPCzW9h1Hjv;|qFGmK>x~Iqo@XzOc|BHqO4` zhbp44^)9l*Z&c40&9&mH421+d&&=V|^8QlwX(2WnA=;rp_81xsjFQ9PX3o@k;Ayit ze^g8$PBr!Z=0bORvruLrpj7Q%bEyL$A!_SDtEu`@^+#X?J>sBIkKuA?5F>F8XCrdt zadjtYt&|^OI~HBRi`_#WzlmjhK}L7wi|&>B6&B_oy#K&{EJqvWwRv&$&csId*i28k z=jC$wWo(S@`(KFufL1E-t{U8lNG9W#dGo#DP-3 z3m>5LjCt;GHa~0A9DCuQ13h&M{^s0Q;NekPvi6df^_}DLc4S*Y9(^+$l*}{F!j`8l zm%X1ZGrC^Lrj-*~gR%<== z>FKvd3^N^iaV`e1kCU7rOi)j?!)9k=F=Pw-!C5Wqbi!GqzYRy`Daf(VzRRHJV}DlXj1tAu zQYYP1j6M)Hu1 z$TZ}?#lywNh@{Q>*tuL8t7ypKbj;T6RyWh}E$#E#aAO>byPT>8F;mLgPXM<_b+%4} zwEPyJ9--6GvN(m=lm|ydccEfGRC1N2>_@YSn3qLh#upFw%G=~#(lWH0+3DNbuHu=W zy>Zy^F>}%h_-^5f^A~9<7u`E`>drVFK(dNu@_AMmWmBI}k>@ZzUmDlpvqoYsP#Mm& z&;Hsq>^w*-+ncIeA-h_{j9SMHaF%lOjhM4zcr_;Z0vZ2oMSQ(;*{2lAr|(f!;;k14 zF&V`_IL&MOFp+W~-#dyU5-yE)&VWUK>fq0R3Z0jf&iq*MHvTf|m5yIwTs|_hiC$-n%3)+l>}r^YZxn zVTP~gcha00D|vZ&&thptv!1gM3260r#J~`%-lari=qCP2O1|8>o^#w8s#S7($e@j; z&B4Z1<#c_=V0%aqCTBv;WMwhb~4pg-TsO#U&Tz^@v<5X2>o>cPTjU-rIYNq z*d)YnJxY|-Umd| zriiQD?rauPQ3xdxz$!6o2%VjdZtkBjJTwuZ(1&c7ItF##B@Gqtm=5(t{J7VmPs}R! zq!)~eaam*Y*U~shtwacU147NK&on-_pSPzBoE#mh-bz}=dj|NCf+`&|cMkKtKRj>u z-J2)F1?D69q;~cCC%1geeGOi2>><_8JrWu`nNKe>g3hdsCEs+*4$S!L1=<|`wtnjq z8)s?CEXRH}(PpN*(e!|vS!%bj4JAxL^M1R5R^mAU6Ejm~k64X{D8cgEp`7ffU*txT zr!k-wJ&g`mO4>v6Af*3xfa2_yT-76d_J`C)Z_0V;AGR1Z1@2$lxTr_suMaG}6d%J{ zGr+F+Uq~9<^B?kae$n?B%`b+tnZfW;1OWs(Oe73~=iRhVo}@Kp=Py2<6LZGREr^#) z;YxtNH^t9AG{>uO8LZ6-i3{bI&H66}jFKGR3W*tkMVg#D=A2QE4>JvJ~E12Y$&_a%}(us&O|z@IRe^*D)}i=XLqeMu)83PXjB z4`kdX ze|*knw4~qSA8jrNBl!S*TcQZ{@rCnh*ToTNPnqCA=rg;IQiQGz-kz}%;C}wm3E@Jn zg-Wh6Tvn;kwMr4QkC8$ybXIvggf~v#kMqWUb_JiRyd@6F)!8?CqnH;#3&YEs)q3fE zU-QEk*^~RM^bLU`B6!AEx(cDvAB!uqL=&`cN_Y%0%K%NxG!wJa)csn%bu>)C#qogO zlPXX)?N6O!b)rd8FnJJ7qeInNW^Tlq)^_6YjJ~0iguXBvq#UH5xK@-62ak`&MA&)R zv>@Uh^eungp31HU?mhJ4&0S%9o#3;gDGRUtJW;lx*Bg#F9A_R&SCy4&M^nwd1Kc>k zgjk+gOK5}xgfvpY;0rcflMn?2L7c}!(Wmq}d1vwGdbi@}zihwok6Q-H0dg@?} zY(ew>4RgL6;^qg02)z(;CgSYvi==iRoMg6wxGrvf0A}%#s5sS~SvTa=B$+D(Of0Wg zV@P>kI{fynrXtd3CqYTevxK%d2JS}&u@Syaz4T;jM+=4D2VX4&c{%_7lJ|x7||HAVM6c^bX_lnoJD% z@O)aBQtfPI$+&LdfkV|(7Y|{SW{l#SV3yt_p>ghvT6Alnlj?VhnkVbS1uj_u^b+4P ztY1IboIB}{K9Cf5B(LJ+AGbT)Xm!y}E#1ta7r*;1`;cCbwDVHLSI_F^`o&z`AojV| zvvlmMX&+mc`42#>xfwemOUulbCSbICRf&I$4a~m$_5H|na>P&3hq{h-7r)o0HfQLb z4=46>rRW>1SLIRt{Wd2pR1{4_k(|qkOY=j$mNI6IVr>3kna{;!hljEFJW*i%SJ@nk zW6`DGO7in|=&G*EHi&O|v+X(w)p+&x?arax`YdbIXnCFzJzKzRdfziViNvERAD`s8 z^Zb;3ZhEigJZ@h$fKn$2&ry<(BI|3{)StaIg?{Umo>v~sHpyRV^MegO($HX=Y&_Iz zI$1D@LYeYf92Ley8v8axt__LuPOdN0iBt z#ul90D0)~adX)QgP~t+>nih{PvMctR_zAuN@8v$5KPP#rfa21d)FK<(@3(zzR>WC8 zZzkDTy}m;wTF_)`vRQ9Vt>@1T__C7n=!7o`zx^klZb}!Jw{Z1&DPTa5bQTAYn zDjxZ|HiJ$rWK8?%1h{|9)0Am+&8w4W6;5%}J*;_#$Cv47GZvNJN=RZqPFv^NmaoDW z9d)|)!0XUT;^Bl=wrH8LhE9^>hZ@Si9ulAIbrPcH@Hqm7{8~U6$mwZq)#hvcqPhrTw}agQ zBt)=#ML*jTGuiHWrfN2R$F0F<_p$f^V>7f$S;`M^pnC`URFP z9GDyj`-1^4EyzRHWOC0McY;rYw1y-k*t&;_FS&MF24<{3#;Grm|wzrr?v>Tq`f4^_+acK2Il_F=b(5q&`$ztfI8! zPUw7}Xm(hYHHXj*L`JE03u;UdM-2~(>nY?9!r zKfWiw9hu9+wMv#HS>}AfJzmR@-xuT+`Nf&^xQ41` zJt;Es4H9;CpdEiTJPIOtb;FW3U+BSZ%MB4%22 zh{mkPFy@!-raSZ~Yei>|&bvEIL1ylD3S~#~kY(ZOc_+l6-uqD@G2#6DJJx;{E(oXz zYeZG+(n=o>%cGM;31+?QLIZ^dm0Qv5n#+u0P~W{*Q_&ggQ(sK0rZ%E(QKgu)&gzb| z2+QPD6k5t5hZySpD7psXQ8US=?@e{RR&5Lnpzg1~>$Jp3H<=rGzl`lfKN&(1=pp?qx1)4#Dz>=^Vkn!*h2Ncf-r(3=zT z($fNRtQ(zykI`#wd>F)>_Ys}YQK!ZtCN1hH@DDdfaBnIIOmD)@Ou~4zfc0Sxp4miU z*@Riw{vKv>nwduDZ5Ds;iM0>yF;NlEZ;j^?ZzE%pD~*h%?4HcGX1cLVkAXT8r_Ina zi@z|F6vZSh0^DbM6B!@!(fe8ij&-RUYUge>q4J0$>~;1b|DMWcXu|P;jT?Ou`$$lb zy|b$l(agwe-E00RCs0a5GArWJVW*DB{q`QyI&N}up~nsLV{Pm+cdQ?dVgXNu97+eD zz~WwJyO=OkG%7%gA2bmg*7X>bud7Co}U zqY);l2})=E>CW{(^w@=5R(9Ky)3-KpKne{aUx>J98W`NZY*&>np+T)Qrc_!l`FWkBfkD2EAL^9MlbH60k-p!^f?qnCAyv)dGBN#DXPGkl0ZE{Va&iq+ za^LGc!iZAk>dsuy-`NcSVek7r2{HAxI4gqDs(7q|i7g9KPtAD5N!@#A_Anb0^tLhZ z=C+Gpv;mVTP1RMRlXe*Smb5y@{J@dj=}^@ZuT=AJQ_A+Ev!EgfG+!cMcb-0oV}NNu zj2|9n4Alhks6}@r^?(s@Cj8Nt^^leltM!MH6QCz)J45nvsThkl)jMy`n^=E)rlvx+ z8HI+*WdgpQZlm%b1@k=o{3?=h+>fL$wy&J>agh)KT#xpCE9Ep=2)o!A_r3?{de@9t z6&iCAb~JXU+wa~_G?t9>v*eMvt4!Ix(L+UHfOzc^-Cyt~*{c?}>2AgYmO3Ed#HaPL zfgXGtb!J4bb&`$4$C9zg z)XwV}D?8Yw_}w6Px_Yb6a!AY421nE=**+P>M{9k&y^81(v$esP`hwr$Yy+Phs+xuh zTTDBvciy1+YtPut85tTqQB=zmIycVL1$&`YWZ*E!4 zLd{&20psbURO`J3~uw2|qy1hS@p zoL{W+3<&o})_=%Ka;#+H-T(`k(=wYj+%${ zC+st*JdpXnR|}UEieXxBmy|!ou!Xmj{L}bnWYxNg4ew*pYee$@iwcQu1RAD4{ki9= zVO`RqrI=0iAKh_DL9eXMdUh)0XM1N;6Q>Ss!haVp7kK&G1IoiXot2i{-JRx==6RZd zkW6U~-b#$r-{Q)ju$C6_N1F9(sB6ss!dP$<3dS*gRBXwZ!j+yV@%>w%c^2SpyoBdRrq3K5zX?GffGYb1 z0+=8gW~n_w$S5BX@O|$_!as8TCt5WsGPgr&I~%gT{5<;CDkVx=(CUozp9xD34;O3r z)Srj^Y}l3>Nl?xH%8~l;pRQUzrmsB9Z*sbbU9C4A(IDS&u^bUZHJrz3>_~inI)5yofuAI zj9Qn$pR-6AWg7qh#b4O{qvCu8F}4%QH2YACuD6}ZWs!G4N8Q#Bb=kY>2wB7vsSC!+ z*@@GmJOYvS=+=Vm*ID}yGwJJEw}ACi#&1uzKN{v1f{=0M+^A!O{n%wyLZWIcrz1

N;zP&0MOm9c?krr- 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 literal 0 HcmV?d00001 diff --git a/reports/design-system/torch-dashboard-manager-wakes.png b/reports/design-system/torch-dashboard-manager-wakes.png new file mode 100644 index 0000000000000000000000000000000000000000..4ffe475401d8f093f4960aeb9f55f7a1e9c77fab GIT binary patch literal 35270 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 literal 0 HcmV?d00001 diff --git a/reports/design-system/torch-dashboard-operation-outcomes.png b/reports/design-system/torch-dashboard-operation-outcomes.png new file mode 100644 index 0000000000000000000000000000000000000000..96dbc5a3deadd356ec363025ac853364c3073893 GIT binary patch literal 62240 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�Qh8b1|_f)^!~}rXnvCaW9s!f7MkD3NfY$*{3a=iYC{P zTGL|$dQBGcYq;EX;;<=@Av$JW1zSUNO~yM@sBzh+!9H#!WNjEyi<@WcU;);Hlo?Cu zp{re;%w)X(FTqY%R7L6sXccAS(BP3 zkWZGyNAVPdBA%32R-NT*V)~PZ{ixT6CgZT@(=K$)@#SFD#+&ce_(tZVj$MEYfWLKw ztZ?rJ<}sl}27Gn2!1#pd>i0MIc8v<8$W0m0j_91#G-(bsN`#AtXTR)Xa@L}uEpRN?z8%AAGsYtDT`-q7J}FmjV9@JfcH*On@)g8n z)PNBJVCT{D-Grc~?wo>x2ZBHXuiJp+J$iTf2aD@Xr`g&f_lwtePJuC_XCaym>4NN? z5ex?9p2zwv?#-qQDTl6V!{olh%J{=?!evW(e>CV7zyZhMHa#D%X_hw~K)%kPAKYrG zCqnfHlkIny@A13quzKo((44RElZ&e^QTB$Xyv`S{D6w`y+j0G!B9a@3%~pqt zrM#;91tr9+68>!aTcqa$x|)Z23XG730Sr&xI{iMbVg3RxrMdjZ|6Rme?v11APg%^G z^FzNZSApi4u0?PlB@%iB(hziY+uM3ws)C;y zgE~n}*%=$qXqODbjzn>67;MX-f^;bQjwt#z-)&3yJ~oMCUgf4hMVE(zkkJGsyN{>Y z-O%wAB;QMzUhqbAr-1kc+QjsZ*DTwU6h-yUHF6sGvixpPj&T+9f5e~8fo~AmX=V^YbqfAr$sun2F)#>EAmjrsL_D;msV3)3=>p0;@*#L z6B~{#el_9tX}4q1vo$=)(t~0i!tfAdJAUg#0YtEtNvHOpx;4?vl1QjlLeAk$TZb9! z{p$q&+s&+UDEvWpBP}}Z-?aeBxOVk!N7h{d+uik0yn{X04-RN~p^1oE7a~HTZt&Et zoSfFEdmBHz)f7L*Tq+kc)b}0PSSmd~md2mf=_?o_t{GlN^HT8B9=s-Wj3UjM$(0}B zNg0A(q{rp96^M4NqjV$sNXck=JU(2%5)IkHu;u};^j!;-dAdjcYHy73u1}<~ZV21b zP_HB3?D;e0rC72{v?gV;@Jy(tzPM{ZqvaHas&c!?qx1LytH?zH(@2niMolGOZYhZ- zY+3Ep-7+i&yAP8Fltbcp|5l}mlAtzuYc0fKJ=nR(pVXTjyi}O!d_G6hpQ7SQAD4Gj zq@24Cl_+EijNS9Ky)RK)vLWxCZq~i(LS$2**`|@*-<{Rr`1@pOGXWo zFRDjD-i@;H1=~cT8k%WnD5c`ph-0637kSjnN)Ju;XO$FfG%fz%PA!`g@KDsLoz<~c z8klNWo*WT&jxeB)K$@w&HbAh?){cU*Zqhh@s#;>d=GG@-g{A#ot@y2|c`CYd|!j2}$F(Q6m zIv=+*t{%P#0|rd(!`>%bZl+wYpCUZ`0PVVs+An_Anw#b1Xl_0pr{Kr`jIKe^MP))N z-VrX8T7FdX7FF-SXrCgscVTqjrtW~om)~)JwMI@yM0KDq*F>R@S|X{9Z-ZSe;v-OS z62rji#iuIVA#6zf{`r2n=h9UXD*&(r6I+@$L0-N<>$~Ypz)J{6Zt*Cu(z(w(oVz;G zyfUlrksIpXh!J?{N=}5|f31dV1((|Y*>DJTWgLFxqzUnWb!jZ~&RgZWJAlG%_ibrt z|17G{IY;f9CtAYV)nO~M4`_aj@5#M3+!+l338yu`s| zEBto^-qs0$?)a$nj+c3@9=6-52|P9a^eh6gBO0H0=LL&GuRZ?JAl^yxt&o!Zw!T0^ z`&Zhd_pf=`GZM!rwdSl<-jt=mg)7_)ZO%x*mFa91&IW1FOGb#t!Z!ilr|b0%cJZr2 zXfAjz!zUUBo>MA^C(RsZ1_jiB01n|F1 z^q);+*`Vz7M-#L50hT4pw;u~8^6l)zi3PSJas_lu{*AA+v73IdF z-An79)d@ci{{F`&-w4^%xvcZ?DI7+62L!af*qu%BNiAlb*-+ZszRD_LiNgx7--rlf zYx{cGnR6UDK4r$jZM(Ri4{uJvH?LztwpUBIg^c_j(;!J{ECQ1H57*x@RbJ#@-*wr( zEp`1t^sb=lsPJiagKusZcktGsS^9QCEA1_*{jsRbIbu!kcp$7b|8)%GJ%@r_)f8bm zUh9t(L90nlbq!S)K*;NmTk!eBi{TgL`vr*0<@ldP`*NLu8Vv6X-Zd4}K%j7r!YYIH zwWXFxf_3j4o7|5YP^nT{(^Y|ThG*~J+P(Q(&`ok_Etzwpv49aw-W77EDl%++-G45O zJ~mB3uOuLkmS~$QpP=E4#F9H*x1jdLZYak3@B9Ca8D+!X|g%fdraxKZvTGSoJ^rB{cyC7o- zh_tw%u`+%i2TQ_w3GDdZ+wyqnob%&#v8x#ujZ1=d^2Dx`xG^RK(&=zitnZ!%pn(Up z=@gZhS~jn0GU7Iqv2v2?;__u?d4@Ng7g+K%{yvxu#ovw)>Xi~q7&5cbnN~~CC5f$O zG3t2v-c!L-&V6@NR{@QcR_alog!AvYUkGbu@bZY+F1RO9Ut2nT6gfbc)YAh`ICMxU z%RHPHzq(wWt=%N@Q60aseN5V#ReQ8AY$@xRDgo~SvTtEmBD~9V{AGt#kJO)U1Z@|S zDQ@zsH@@Z_Eem7l4FJwKHRc~-t@oF{Kj7$E{4yx4Es}BQUTFTbXR2T0;j*ex5*p{n ziK0<=-~aBE^33+J;`SneH6hc&(=AM+Oy}zQF=zG*D?ER(;&ACiMb}qF?lMUae{c%l zbiWltV9H_I?HnVE&qYnRb}OXyKB>YIIK=5~1ifxmNKsM^zQSMi1)$whij&o+1YNwN~u`pbz&7zF@aO%gIMeZb!J}a6Gr=;yO&7?|l5R@%Pe&0y&Yo z?rqZT-Ui#{BS-B6DlAiy##Cn2H-x186HLdxOH5{^nw>HK?rp!I>s?sfeCR}iAo9JQ zMjy)K!}*Xes!rmam2_!5#O}b(z2(#vTQIJ9!&lGxmMBsXZHeY~ILMcP`0u4#k=Pcp zMPgl~H?oGuNm~-JaZF8Ie^560p}!?-EGaC^wD{FM|MHF8HM^R+E0>7sXxmY0e>7kI z^*{`U+MK+|@UJ9V=vdVY7w zMXq#QTUZ>)&&a-UIq~v#vp#3Gx}T|KEyM7ts!$a>YKTV~lkP;-CKX7}KS4Qm%5~n% z1{AhiR`O82o}YdMb|@d($&H62BHzIij^q87&hrX*pAeDfvKA3B+G&_AziX1i`-{sU z`jsdEb>5x>u|?*(k5+glPL;p8J}Ux+LzY5;R(9#;D&JMn zK7A~`bG9Grbdc}&8oOO=z$VcFDUR3!L!L;16Jm6I%KQuvL(Nab`N-mc-w|k&uN=Q#!U@6KKI;#w%_&4^k!V_JS=NZv4pgo&j2fK+zy8(v@$&CX?{(9>5%{m4|Dq) z!QJwk;vOE<19rBX8QFqA^Sc3OL2cp`)E%TBLBAb(&3EhcD64ZwMJjr`h6HjpBNv^D z?aYvS*Lj|$Us#h&xPluCKooKxDa5smooFeB56+g*muTToZ1^T9jOxi%s1P?E>^Ncx zPe;N`Pxyk4OY^HjH3M2*H@w$O8vQDS|2B$#L`s^Zkoxt~F`)TGb0Yn_db(PrTakxU z{Dg(oVanAjL`r^rO25T*jsnA-3_|c{;hOlvw-c`8^1($$-{4HzaAZ?i4LK_tqffP$ z`IAoLmISrK*d@5Bfu@~^!d*rdy3LFYsT7(6dd9`u4KRfKP9{%KCoGJ0%IitJb)^_Qm^D*9CQLlwjUr*bZy1hkZQv9Y!=`^ynwagUE_m|=ce>fWSDeNe4}VHFgW z^nggFX00!@hs=)mD?jYLE6M5+xW3!ctvufVcMTiowOnlRyx*<8sQomr=h$`s;|C~l z1swZ#h@9k{|N4iJ{#w~ltQvnFQ4H@V=nG2EEyHpWhl+@#_jDmabjZqWPoK~dwby}k7=<_G8B zQ_N>L^WkHBZ9~?6O53@DZ{E>Cj30aP=QW>^tp^*Uq}f#xpLk>H;|%&+&oTS1p40XV)U@bcUGPcfufb#?%mU@@|#rOifboY zZ22ys{z!YaM+}TIIj$;OoZ`^>SyfQOuDMCxwM_0h7z3so8F%Vtz<8H))K_pzv-*`- zcyOK_+8;a}z>vW*q}a&h+_Tz;I0zjwg?GSjLlE5$(pn$l(^=K_GDJRtPF1xu%*4tI z%mOIo_{V}o_q5${O?Mgx7#$B!cLQ)9};-Y+I#BpdLx zw;)*e4kzU`t7|WgOlqn$%1V+SP3z9InrZ?Jq>#bHX0Ci~MluoZK6v z3c@-N6_akWzoxlJ0`^^P(HoiNH|GE3tB=FEx;yPZ;1EajR<)qXf(CLf!je+At)$xU zx-OCf5b75NCUu$J<^%;tYtG-fmJkkke=k#JoeuW89XC8W`KU!h=RSSB*yX}8P3S2{ zO46(!6|F}k;MrE=h788(Mrlfxh%AW691>Jk@&rYcguu)4W@Wzh(&3$Z8Q;^;`+eO9WB*Q?^SIfoyu^32Um&p;>X7N}v){95 zz(^zjp*79DO|v1R6JU2>V^A!f_(;8|KF;<>mWI@PHW3ve6Jrk*0$rHXK-~!dqIfkj zin-;?>TgL{ZV0>Xj!GGL`L)`agu`BGVd7S-j6xG3SyEl^GG{#C?x$^%;1t1yy7Myc zO=O)d;x&ew_^nCrTjm3jTi)e+(c!`29CX)1Z#DVpgLzO(WpU~|ndNKn^n?gIc#2Z+ zx_{Vw64)i7OE4(eUdettj0n6oE|=~1E(nyI)4g#(+HCTpaP<}ffIE-~LkIp|Q&|YcQkc*Modb!ZIvEct^*3mPvl|N%P0PYuJ zcj}s|>!nVW9t=Imkw8dP(rqv#I|#h*S(e!XhB;^m+%GUHTu`6QB**=L)Xe$B(8#d@ zNE9G+nuEvXlW^~ca^C5b+9uXkPlOkQl67@A-kK=QdVepcfb;mzA4R=+vtw&%8Gx2u zqI%2CRqAjgg_;X0#nC{*9ttGo;$Ya-TfU^BIXSaaOq653*}il~Ow;u=>_B-w>bgI6 z0@D|2vlf`wTt_PgZ<~oIznGuE;>ihgx#4lhwbBP9>qV0v(kvxHtV~=CG{dTf4?Z`` zrA>X8>uRyz=txI6uhPpWFIzatNNhMeL`5+bk34*C3-;{GsN1>3CZUt>-DJ&>O)u1c zS;ps~`E#VjDNR^%KecH_9pA83s_6dPe8RlIL7br8&DFWei^~C#!l)+C&A+P}!unm; zwC%^(}hUwNh+-;h<` zfzQMOy6^>DD7D#HDuu8|%gCiTJxC9=FXGwlfTO(>P-;I0$4)XU8=ObeQ7CatC>BM@ z0`gda%c$CLD#x|v-3*yuIu=fNqc*v&eeJQ)cN3fcQt z8?DR2U|@j{x6UEdavJtHF>tS|qW$1BHg7LwkSBiKWIdSowgk{9_i)#--oJKLltXQR z$j<`f3BJfo+T$1W@yTYPwkaS9U#m-9#2A=^9~emO+E@1?L)_U(bB;w*Bmq_z195zd z>eJ0ZI;wZNG`dsV-pS5P#4!!Zw|zYiY@!IN5WHI&^$(jV3|Ky>!0~3Ps>oHT?L(o$4-v;kMA2=BCxAq>34~&MRrlWj6 z)tpJD0Vw$91$Q#mMiK}fa=*{Bo7&(IMNS8t?$ePtpRwS2pGIW&anzqp4-?)iwj6Y@ zvr2D;%}hpBDox6wrr?)*7Vqu6?O8F*lPYUqn&u1Mx{q=O953>Z%8Gao9y)D}A8_K-u{}!FyQd1=3nvzi(rw^2 znOG)R<-)k8=NOs}Yt}Q~96Xjb(&W*pkmH;+netmTs}nxWMJt}0@4tIN?gFcQecD)i zr<_P0)5qp~aD)Q+sSC)e`R8n}Wa$j{vC%}Dny?*}wTpQq3rHu5MupEc>Yzdjb7w34 zs&Kb$#$z(Eui+>f&kqUT$%V~OGlld(kv$Uw2Jg{XQxc1wafQA;*aVH`pOFbpt|AXU zX+nxmyEc~KhJFl*fuJyGQCRrJKHRR;Jn=J-Bze!=2%5`1K!O(a(SdQ%q&|Y-01wx2 zcp@+xT(0~vIX?M*&C%ZS);wwYhc4ILHTzoSrPimMO=M^GYu;Gn1#Q!7;1!shichnge+AdG?BsS!SYkBn)b+V=c## zY&EJD1JRJR-`g>PV1bfBs}Y}vS<1|Y3x(r%8@+~Oj*gjTp1?=FtB1FcBA#K`!!Q?{ z<~{|NhQe{e1|IR~Y`&v~AMM}{o0tXfbgjv&V&VN^_uOu)zUS9xGOE0B)YOasdas%sY6;E2-`GZF} zw$qMpeOf662;I+bGcZ~>JZd+Z=4rr$Kv%NN(jYmZuhD)q@`1(&b|77FJfR4l$;ZJk@Od zY%5>;F>E~^b$AX>5Q5^3Wi0clotk<#?dzLftB8kkpSd?p(j$-ekJJTaK}hrif=Z{5 z0$*gcG1eXrzI6 zU!zlrW*iN^OpC0ZNanml?6{UHBD*DcD<2>`YmxuTNIa2wW@_amxMC@<-|U35CAYMj zuHbQ!~O>I8e-tQ@UKwCo$giP~;b$DJz|1{a~lxvcsPzSjYXeEk~ zKQ1tny$m92P$*ZKjqS{m2uc7yOh|Fh?&jM`#Hk=a=K#$P-1dNcsZHlbo(_N4D>r}3 zMbVCELec7Az+`47Tegzxq2c1Dy1xL3A(vCD{2Jq0(m*I(pZQ06WO~9|K)Qp8Y`Xpk zaw*8-lA@*lSoQsz5+VFEcPM%?phQ9XV84lGl&$v{3e>B^yGm#SV2*^r;H+7uz|x0Y z*N+_N9VrsI67UJc0UUq5nYrmP#@@tcEHE^CU9NTW)k0?`&39LQSTd|NE$DzYT(KroYyK~~{Ow z&B9dJglFazCuyjo8r>F5r9U{knhkg~8LN{KA+%j}i#O0XKQv4E!4$*HcFg808=PHh z^aiNu^;o)cWkyn!9k&y(=i&>3UbQe3DAaEKeC=9Ot*^*n%KHprnaILrY6sSI7*s->k zag1dj$~RwRJctAO3-^g;_PcixSO}kpZmJ;Wu4OMw!V-J=-+^2O>wd3Z61iJ|ZWh+^ zY}03cVZR{s6e-h5_a>lbI#|*;Gp=-78sKI;*22a8Szw^HSVws8&}3;8VRD;x`S$#! zH8A6=-R<)%1K<9u^r6e)BQ6%1WKs!lxpdQFNGsejGU_FqreUo|dOf+#b<&=K92}3U zVrX1gv#AKqsy)rTbf_90SJpWj6NjCwk{$3@dKnZJHrlmaxlUWWE?R_C7CPKW;L^fiWrgz3nQZ|~+wv$AUZ;{f-hrR3q-H?}`5DX0pFCaP7?H34TDra<8E`)t z8=iH3_@`g_|IOEEX2JRgxU=r**n*B1i&_6B@_n26?V$Md+7IG?oNWL1edTLG zg=ygg{d6@zy>W^Z>C;jEMg~jWoP|)o4huy*a~#xD?hb!V<85gm)t1&=YCPD$-u*OK zZYXXLg!TQXjbe3wZ)ZufK_Y^(6(bG#XQ-{(OQeZ-Cq8P*nSuZ$$r9@9Z8kVmL&Z=|L2of#+ zOpR3|LA`TJJ>OY6q|Hwj&71Axj~HTQ=vkeX9(zk?I=PSzHZ&7)3YDKty{e#>mD&N$ zpSUHAMbtNu$I-=f5cH89m!;uoeklQg57IH@_Qx?cb?WLvNhueO;{lqOPlIVCD15Jt zDcjn9GvCjc{AVB!Z(ygWKf9}Y9u=lw-7rDo`osWoYDOK6bFtzt6?F8=UW*#_{5)Ch zTK&3){{1UldZ;8C?aKKL|B}AB`8nQxyK`#T;OIiZXu(EOfzz8ed0!%SPX{X`n!EC3nYZte4W24DQaJ z%eb2Y!L=dTqdeXw5t>Kmp{Bn5-!Yy8o790rwL5#w+wIctILuPwg`DP4dAwSnc3-(w zQ&k%&*flYQE$aYGd7JdpVVZt}`AK@wA{9#JX`@VGk>My7am1Ml;oF#Z+Yhv zw9WtY%5Z8cAy527ltxHk4Vp_p(XvD^6Wc4xhiW6KCRgJO zgpmCv?MFDvxKVpB3IlF%OVti$rsKJPm1pZj2`>u-xN?ofn+UJsI(lOHUg&Ks)=m@Vn zq=Tf=RJE(Qh7-vU9)c1a9etTgZt~Qry$-iF$MmYJ$CP%g=BIN5QsK+YiXrY_x>OXi zB*XFVL8)3KyOcXNHi|AI0G4dxG6heO7LnboY7rT-M2iU1^%1RIRS%$aZZWRignEPM zRnpv^>pUrS|Ib*{k$`(^WS331GV;aZB#|`ps^kPyud?H&H&a;To;o$0pRPbCVhWT_ z8HE-*?}ET-$YrCLl*2KAyqcL<5=tj_F&HIiFD3qS85|is&N$&ySS-cB7vyd2sYTLL zA|^u8@Z#^}wH9v4UD(Z(=eD{q5Km^bw&HmizucY?lBQ9qyNKMnt>;w6!iO zTk=^6u*D=+q^A(IwTtso_B}&{dI`0SxiTaG!rx^5(FqKDOIX7GQ}j|uZ`t;HKYc)Z zZ1sUEy*YZV2vI>1_T~G%9nHJ82b7>qqs>O@C^glqV~0;}!*N>r6Dh2+Yv8ur+CK!* zgfQX)wqJR0*hT~>>&utOE)S4YE900k71e8!Z4K-sB}7S{Vu@7EGc`?z1~^*HH=f7!@v2qh z!8pcoX`ww_O1woKg#o&8ad)N~W?!X@)NBePHmK#9U#j!;f9YrPXBzwC{pvL7jt$?K zfTK^fc`q}*e_Ux_j&(CVayfB7lmF;aFcgN_WN9l5NzBK9eCHGkYCihHCL`h7vhYMt zd*+2(PVhoZMLk97w#`*vXI7?6NXTRq2We3X`s*mK%bD?g%w)P|SO)E&oI0hMLXCPI zyY~K3o#F`Nacp5J1J)lPi9n}o3;?+B;^5P_2XWdc#$93_#80`4%#&>`S zX6Uz3eT~mtWE#DJ(!P=-{c6H3^5sxx>QptF7J)HYNe78A&$h^*m*w2O)FE4?pQI>h zfb6OGHV5i_d8L%7UuXmFBseOHFHr{FY5eU7CjQ>X7Ak?u7#|S=-zGMw29>Tg?>Mrn z!RNQUF%!OPz@mZYcz!a4$)@4RG~E6>$Xoe+r*#4^-KFsv#zoT{DIyPsHLK%*^c6-K zt0VnOxK_s{Nm)^q(hN&H<5dniC6?`+W}UOqYEQkO{AA@3D?kpAywTjkyAE`ux+J1i0@xrOle^xG;9p9vX`9Qaf#)GX;xTl7w zfR0dva1Wu(hN?D-_t_s^CK?L!3&<^5H4YXGzP6I)s;Ph9OPVI6J?k}Llk;PCK+P<; z{dM*cQ!VK%;*EHjewx~?dgwf)s&uVI=o&$q(lT`+Igp}ms=hQIG(-6wPp*-bO!Qb# zm6mw#&s92zdKgd#EdrvLGMv7yh5iw*C{Ra0DU5inszP%m8>@#N5yP1G4z#~yt_JyK zaPq7DNFaJW>f6$H@%CCg0|3i8%~oK}NOK`Z%FdlBOPY0r4) z581I0LupAkUj^OMtHIK? zrc)6!smknc;)Bb`Xr9w-RM~3P>JR=peul*5N>^ci@UDn7%3#;&P#4g&w8VZ8{i%ZP zj~0Nynlbj7h(OO$Y|oZD(t_ZD>qMQ4c&f`M%Tuq&`O91kECuYcLPs%(o3QGe4|c6u zRd5jwMh-1Q5Wn1{bBt|EH03Cjr5gYVez4#0&s(&d#aFfDW%aZ)lGCYP0s0WJkNB^< z_yJ;h!@6oUx)!&8s{LYLZo8R31p zryu=)=FuTKHGol}8MKpdNPBkFJo*dQY{1ifyeT`^?KvmS(oGo7RN^p)56+gNtmfTZ z{&tN`7&yaU;5hZ^$?2v%MnwNk=Ui`5v&GvU+W4I)9%C$H@kF|p-{>`xLRxdBmr@_6 zjn7(yJMH8A=D_&8_o(mc-R+U?-}O$w^Mv18M?125Lw2BN2#f*pTjfV~8rz{^lDmGPP*f%VuW@fs!O1YMt^m1SK8b#Q0I$wnQ zZrtK1-)r@Awi5j~B428)yz8~^nYOW$W4N8yLTR0}@OG=FK22Ow7~-~cB{%{Jwtl&- zD>#=09u6%Nw%!x~F9$s!`RoElP;WuJl8eK8=w3>FSW81*QarZi2F(g1=VO!}Lgs_a zCTnUGYDPp=FY3nT_5zIbG*N*u!&=bU+_&TOJo9}FboG2(z!Ua^$vx$ANNZi-(5Ydf zSqb()l-&`5?>an8)SF=i(!tD)N-MLS{^TO%fNIMjcx6c^00Jzh3}a%?87m|2d>>{JEbwWTekI<{4hjB$GA+Cq*2xBm0#_p z5zInzTAqm`5NiUtSI?UFtzZATZu?)zd(Wt*)@bi{yX~T&AR^L4Kst!@8WE8uM7ngP z_g;hmQ4o;cdkNBehtQHJNDIA}KS6X8U=KB5f1iXi!!q7D$#rzK!u-!t20h#KK zF%PGkfN&N7+bIHS<@lAUh;U?+@ap^)CUDlrT*Hjh z@$3zspYYQNoql2!Wj#St`*g?#LreI)B?*91J8a`2nA(d?0$d?YU*otQ--2p_+JaY? z5QJ62*-42)OHc>F;?=-ohMqbdaE|>f!sMXOL*)jmgvjRnxgcF>exnzH;NSETGwvql z%Sxb4j5^ks^j7ld?kgqq(-7v)umlNF@R&bg9b4MbH)ERYjo2Z$iAe24gSeTKER+$L z{s_XJdlNF|$O!D^VZ1s&+kAY6VmJ;XSdEQrjfjI(v=DW$V;}atd@|gY+z@+52LaDu zQZ@zc8e?`Nc2(Fx5%8Qg?qdcp26_i9c*)T>#g%Y!Af6~yZPt&nBX9~>sx8JIbZ7Xi zjUB^KLNL{HbVtjKs7oHpb;9yD>@0!F*V}q5PQfD_SO4kUM*LVDpY)l~TCL{h8#MH$ zd5?gd^*x%y0*Q7WN2uBK$n#V=!ZFn%emq#63bcT?R(pE}m$qt1c@zFlBmc zCANo?FzVmZRfH^U!a|yp95Fo|U?F^D{uF-PVwS-whrqwf;E(18)Nig*W0k9zaRG!0 z)XiB>%*Z65eMbGTM?wiZUsQ^iWF_kmb2(np6Q|`-c$~Obyh=4?zb!H8IG4^uxEN$0 ztTj3ARU;mInm_ZYCm1TJ8Ru^fpfI#j$LAU12S`PJ`|XM9o!s$RQI;9=?s_8vRm_1$ zsl~DNlvT1GVV>_p>3GOMlqS2{EPvW%Gfx4!~Go_-)R_iUIxXvO{y4=yN8c! z34Z$+Exv;&1|Jcf?@a`2J0$j1x0MAQL_w~xY8YDYYJ;?$jJqz=ayM(CC!BB-hz=Cn~gu{`Z6mDcsuo-ofM0uc549Y~MLm>wF}- ztO(!|#*bDIT@BjC~i#zJmoB>JSG&E50l?=|0VpNLwz-?*y~hG@5KA zwl*aL#4PNjc}MP0$k~k!M&^8Rt=le5QOD!$!$h>TJ^#!}@9*9vmCS&82r>iCEz!*V zbre?(BN2AfAEd?3_HwUBO5!7ljV{DyCPc<95^cWj-Ck@zHk8;{H|jdgw0X;^U)dfj5{kQ^{R*4-r(6KBPq%S^2PG#D~k(lzfTMaa(E^6;e(*+^w(89jE1{x zrQRIrn623{pI>h9RjHZ!N+*+V47eA22`G#5S+R3xJ(%R4lhf<+AuzZjxU;J3JD;)h zhRiU%z^o`wWqCH0eZyO*J-xVL*yibp?JDF9wIc(lKdjK{7C4&fn|r z8udD!OC2d5sV8Djmc~wqBVM$p%rT*(0?CQ<&qNPr00sxK_~5OJLV|zwOrpw#>T150 zrt1k)@2DpF+vPs(p?|TQqy-s9?6bBNJ{y~HzWpU}`NDNM+Nou?Fxz9!=_*(X?C9p% z+aaSHqR!##bXE6k*m0HIORm0BNkLvsu6q)VfDtmBI%pR$2m=u#h6dqxR%$9={mE#5 zBo`l*Li4d_4jeQ)->)1{vfiz4P>#q?^d$u^Oz77gEe)h{J3P7($s%<+$kLDJLf5Ve z^Ja)`V#s4G%6*Y+`qjWzQsx~s#9PrJ^pEp^+`QUlgcZSdz|c6PYBL|EeQcb>qo0w? zc{U)vCg-`Bn@rlLT3#ciwrOM1yz$##Q;73#@+npoW^) z{DvNK#`@6NS^l+y+GagyGpZhcwc|G@WyTfmX{(1pz2Q!K!HXTo47qd=37#_Org1;y z#?BDRQ+k6=-1j2)DA<{Mt*#R0te}i8Eyk?{Ns4Xa5}I~sc+(_>re(~^7B+lnlAU8d z`8zuFxAkR+%+Mt=3{ito>A`;bF(G4<3--}A&N$RU2Mh2!X1jS}<~)DrKn-|7*1PBc zA4~2C8^yifaznsYJ<1LYBw;2C6cjf`w!ChdZFFaCcN1G?Ud*1dc`-z!Ev#IaDdLC zzR1|pqn^k|60YO*mAZVVU;SQhPE-exHT>;Qy4A3Y4-Va1Av&#uElhkvVIYQ4J{{ae z-Q0)xlR;Js!XfG2%ICeHm9QQmb(^p zO}cc{Tm)fo^AnY~J>L1+x(9yVoA?!reEs%E6Kr&p8;u?4-+3q{=DQ+>Af4Csp)quT z(yA_W9W z1{!qgoRs9TahKK_s;?A|q=UpA4=`DIpuJBeV<|oR!+#=E4fSHL{0mrFFVo3?H_Kb$>WP+Hl_K84sHzKD<9YCLGBq)?pL6FhbcX_*59j zd$DdjJmw}TN@mu}1Af=MtiHY#JX=;Q>LvU6WGfp-bRF;I?F9QS>Pw$2`9rY`T9VG% zV3PP^!0BY`q^Oc%2B`Sr$Hg#72ioxvQhxrMuYWsWvyKH>ZsbZ|a?y*A_)(Y%-K?Ju zD3N3EDmkQVh&t>Fh`k9Nqu)Q=el8b5F+)Av?z0a(N3qO{8Pqy3ot5C`9Q4|$R(QcI z1PZz3SvyIoxn@ZV8Y3MPZh#4pYNVRPc6ib+=39LX)>Lr(j*9l z0dCk_7~AyzYI-EO-z`XXm?2s3b%ehXF){l$>J!Ll+No9ya|2%O{+;;!5#N^xbL!jp;Y_WCOaV90R8 z<~|i}Ch25pa=9Pv=03b(2N53iZus35+HFDNfHN>VXaoEycG%UVVe)33_Fly)nBS7%lIE6-~V5()CVS;mmLy_ zn@rmibg5YO<;TCi-{bm|DF5>K^{va0{-1Ysj@^}2U3WILcZWEM8bYkz8&qn#c3*GO`YFd;mguazMcKR(n0f{3xePrea!^?Qrqw+tZHsnf#f` zBfy@{J9Qf4x8~uJuUl&=B~B>D(3;99|Z&6h9nG7I)oxLdj0eroy%(Ns3=c$^*St_^cq-n5n} z(p30@o^cr}C7b90B#Sz%?;nM)wj>@8q^vp=IfyPhL#)eChc(JnI!bJL2@LKH{WYW9 z4SFoB={n-+7WxsTk*iYB>h=1IM%en%(B0OblAGNYl5k&eb@QdFcvOvf6d&$$voW%) zH#&7&@5_|FC}wYee3K4m(X(b0Ui8v59)@0v!UE9sK3z4&W)0?gqb0XxsD#@+p^pFN z0<2@46G}=wX@t0ev78RUlc|wxK^i%%Z_fB~KOfn+1;vw!kYczX zP9C$3YMOp-$5V~j$W88ZZOT5cAJX(K^Hg!QRfAR%c-;(7wI9=`>UQ_~m50J!sKxWt zE-i;EaXaS@y|C~oJ`YoZze|x$w?Hxo^%vv6zT<9#ucxv1=LhVogw ze!{w5?HMKUwtRnJB858~R!*&pEC2kdGqBdih%~5GoBsYCuiU7Q8k<9REW~nrl+mOh zlc1^}09`BYVVC8Z*GT7z8KkL4n#WO}C1Ir7z%m^WQx&76Df*xU@Ybfpmw~>WOx`RD~aqEMk16Cv7!r zk*#+X?sFG*8Eb<=m4=?@>I|)xj~A};7Fcc;|6X^27;wBfFC5dWN-Vv7zEWZxBS776 zSJF9(g?{XQ_|{=Uih=y_IqZ3(xxmQ!!A;`v<)PMk6-QlZY7vzf3Z?7)Bvgt+~8i8ocBpN0%&6v$&J0QO9mWB1VIYP@1W zA|fPVzlq7ugv6O^6#tIXPmLjVdB~*(ROXsWmC}lMwI1s1(tpsb%T``Dw{SU4TNjPw zLhsz6<~KJ-uI`N{N{T{Hg8iD!dap>^^rqSmu9t|Ki(UzwDqdAqsl}U4g*;ym87`=O zEvJ0yx7Ag6DlXQ-6eh6bRc$*7cQ*q&%vAZM89n5hLGg(+7lu(+4=r7L>|ELptUU(G z8tY2qzbHNTaQdbhGM?gH?O01l2Z126W6cMyj|V20Bu zo^z$^{43?9R?gj4Z_14A&~-c>uSoBDl8%ZtwR}RL=HoX%JLf9#4_#IL+rDX-;Zci} zMX`3U)a|T-hLNa?ELOH}xugx&ENr!}{oTL02a0DaGHt}UlJD2mbmiY?wx}K!zL?MQ zG(XKtzZbNP6l({gDcR|ZMxV^@PyS7Z5pui6i&Za;ZYms=%}9EdZf||iWwP6(J00`F zIy-|U9G#s{pG*H*#$ci9vaM1AJ)^-=hEk>T06A34L>PCw#sM(=V|zpKX3Gg&*h$E3bU|> z-WH@NF<%bmyaEw+q+{o#-92R*Ws}i0{gkgL@HoiFF;rLJExhs9aJ?BW!pDu&!LpuB zu;=R)M9y7EH0CU9}hu?2IXO+_dkqpenCoS>6gZ4scAN;t}-l?pc=P zi~Q%DvcOt&7Ger2s_&A6w9?p^=QSOm+73k9s+NT~cfCV)UC@K6g2{;EI(bCxvd&6q z3q^Ts9iCo5e9zW9&x_w2EG{n#mC_<53>d@vk_seTf6t5!dNQfLf;9O;MyZA#A4P*u zDXG)Hlx!$Q$1IA`=`4j=`BAjt$RZ_NK`Ln@O=L}f8idQdtk6DfoaKAtJcZh;eR)Rd za-lx9(gD|7?IS&d=}cS=PqyGa`YAv2+*j&8FYKw?ZUWWPfG4 z94w3~H*M_qoW4D{m^m-u=275&o*MHyJoG@TZjuJ*nwQJ^oXQq#5yD&IT{=5D1>ca* z?mv(AH^t}qSiwK$`^;oXgDLMH3up>I%v;(}PuLcX!;l0=D?M@?85i;YJ5-98DE#6O zDQ!f-8}zgB>XG&gfc7xM(d$|Nfxwfn;MW$QgE})=7EL(xp1=tBg&Nes+;aAnqP7H&3yPTi~}hFlk2z_Q{WAw;+k8vpTAqYV@(O1;S6_ z$rlmldgBtRHCaWcHFEMQO8qq{M}7zcYVC>fiXvN?YW>i7k4INa1>JUvk1mRs?ty0Y z5I^9O_fJDUXFH!S59H?Iv06Pk6?uJVp2D!Vu*PAOnx^i_NLl$Y&Gu!Q)Z1{jNz>vX z+?u1cg!?8^eIz}cC6e1=myFcI*!q-Mws`jb{5Y#w8(zWtcpwQ$fYSKvz8*vOD85BYkGa!(T8UwqIq!25Sax3jcQ@BXPo@w7C@vjHXblRW_ z@R9};Vq2GdNkgWG2-FvL7IC4KJ3q{FpuZIxKoL z=@wJgD=dX&=y01fD42F3r)6|hS&oTLOLs!A!8Amgf5CHhs-ktT-=PnvRriF%qqYkJ zp{6Jsq%bS)t6Qq=Os)8rnzX*07JgcK6L{?fKU5L3uB!LR!36|3p%pBN4X%2M>HkGy zfhHuX6E>t+k1llXk9eV48xk09fp<<1wh>7O2MoBm1it-HT^#t-Gi2ouK;JaL^qa;v z0dkP7_iZ3x7B;$D4hd`UL1rV%_ZBgb7$H1>NB?9o33UiK`DHY7ba3uF;CteM?uQfn z3Ht}sWMSGQfO<*6*5@~P^>2lp7Nf|)FYsvBZY&Vj>IDAK_nBV{XTG=<9IQJZS%#e< zJjWkJhIuH%h4JrVaf|e#3lm6p4Jr3TEjrKo-rhxU3T`evMbAEZ_*G=nC!<&_Kaqch zbK80C7y$0K7uYLx;e#c<@Ige#nwLlVruAXY8%Bh=gsAa(5t_zrxpo9T^(MDu1X$vz z!;x9q4u!LA-v&fAQQi4AGW7-rPTn853OK=4g7JR>`<#w!b%OHo1qqr zW0KkT$Fg{#ZA57<4zO(?7Y6*goyj-w`FJC)!}|xrw4hz%#by%9_hRlUn1axyV-#tA zVFx(*x(5#<>^pGZvuIglodK;H$?`s7sEYGZ(J{mj(t=F>igYpVt7Wq=D1~Lt^6XVc5NH3n z+;|Z9eK#gdK9J-@BEv=Hyh+YyPdoL#v#K9NoHry<7vwcE^LVFupK{QQM?bAlyAP$d_ZjK4J_dQmS$H7p{-J^mG`Hx-1HlebAy=fT zH(gB?C2uCIO1^|>@}wkBo-+e$o!ZyMdJDA7@Yd;If$I4{m8Xfao~f;IOfMIQ zNp!~=>D!yAxww2eo30nE_}o5YCAHT|othq*%6B*o3=_cfBvd)BY9*jtIK3v667!e5(fOGj4zfnVdbTD*^0&%?bNNjPiQ( zR8H7KGxBS|gX60VZ`?&Za@>uW=VVu;1-CrQUBfdh581ndqa$jzZPd)gx2iG4E`fhi zq}lgz89yt=;WmXn+R5fEzvJS=?wscWH}UQc+DnO!1DM3@ zWZL18jpFG@$|!HO*^YzvAMPLlMc0-6++W$=mB-^>+$X)64X|3GtFh)IXMY9_p(M3c zFBM*Y)AW``)J}fi?HH0d%mnFvVe9ZaE}t_)Y1{~tZX)fV+_uj#UIF~DzJHRX&R8&F zY|I?>hlDqjaAYn0#cbW|rAk9r^ss(p_cqALcy!|McC;@$beGuMT-ML9TvasS!P)cE zvpi1PJJ*(ZtgqKzD~CXgCj>$RNYO!9asza#NLl-PERCH_I6sPV7{~8Hloji zCzZg&mkq`u?y|gLT3?1H+&?_^^9wk?%ng6Bp*b|$o1YVvtG=*+q`l)hsMCl~8uerB zH7}^~INW|f?)O89VHc4#rIZP4=wi7iq4RT|?6yLS+k<-vrmgo%sA>6x17EAHg;1~A zg2s35k+L}kc-wv5WkVc2U9u9*3EQg59HzKKl8tlzxk+`$Q1lybA9KmkY>M8q!H~LT@}k))2LtTPzzCqpz3Bgx!p1 zT~vY?1$pX(?ij^fQl^C-0nFN?O6A1LaLrtoZrNc|Rr(^e2Ztol`5Ap&JueDmh)gY! z^wpNXo%-wesveRCC!?nwe&0(VOCLfJ_q%ZD`{2R{w)^aL=%@k9CcYL=e`$mqd5PC) z$m6r}?-$i74BUQa7uoo_E}O$Qu?u7zvD|Oax`UWs1D%ww{|uXL`r)lxUa^d-oO$sj zs0!1R-W&=yg|V$Hv;N*N2`FI6HpUGeC>U2=wr zDm{1hQ2y%FGlkYw_s{;)9yW)h(mrR4$k>ldz_XR)O{3WcpJ9>!_S?=yj&Jn%RJzpZ z;m_nsN1E^Y@76{UVbvI?kuIHp!M^XYA;Q}wQ;n6jZ)2Ww)e1Mn!;5+wJlUNWG}qPc zBywfM3UdNbC6q0SXsx^t?>N(_PaChte`+T|8O`c6Gs^quE}Or}n8{Wkwy6q! z!;Kg&oX&o{Q{2S5!9+$%8E7!~Ja#*y?Po z$}pQ%y1ZD@YqP-4TxBVoQP*On>d`~uWcWCpPg?uv?CQGs{$0PRD4h#q^S#X8D{^jT z2Mztz4_{=as8w3T=+{z&N2k2+G{C+s)_?xQQ9UyWVKu8^MkeD*CTCYG6Rc5+5)wv@ z<*+`VN*ER3-M=2NR_d1iOU!fCaX|GvgV(OnZ~$oF^1>w;NG?$1_*6~&0vUjltHbmt zdyWx{Vc)$f9$~%yYqK!?u+~Z`oZk#Co1O-3lae_N`7Up|2Rlb5FyS3POleO>!9~Y2 zYkNw?goals4x@u_iKKAmMkmGuwgd)xS}Jvq>C z(vEP|TYM_Ly4J|I@2Tf*D{Ab+r_O7hfa&In{(EYzG-{tzuMUM{3TNgzg~hy3qPrZfTB7gy2g?;>x_{&HO)qX#xZk)%DH}kOBI%zo z@?gW(p%}I6ioVdM%QkEmPW$50u^-YN5ha!~tKXEEPzLq5H=SK=XOirF9QAgRX~||1 zUZ2CUZ>zw#CHY>r-qIpejZ)1f{@UYx(c=UOYFwXxMY-opi`h{=lbfPj*3|gP!ccE% zdbE(Hktg}BMzsFP8>?`c^>8s&g)fYeKB%ngHYrU0_k1fp`AX~9=~8BeZe;x<`t9C(!}8~Y@vVOhlm3stb}F|(2YOdn z$L$SbeL&MMP`}S1`q=%J8mHL+R~iQ%FSGNIX7O?xE|Z;3qspVvXD$~}bzz9lpX^R; zO`0#G@id`}@ddu@w0x%o4Hxlw@2Dsba&P5i!G=-7b@$qv;|behCjCz((;3nMtI>@F z=8u`^olrc<$oC=l>Q}YsaU;X3mtmBjs2P>8=@dn7c&!3 zB9p3-;mmoQSlyM;aMV(GC5r0v9=rxV(*u+uK%un0#lvU2rs@maFi4uq$d1^w3)uJTz723Sd2A{jA?eGfmQ9m-cyM7w zognTCZTFd4mx3emjwd>tRwi><$Rf0FFA{t=W&9lRA3cd^4b18GO369^K`2|gy$AF4 z=*A0M&qh9%;1dP!Wp#OM+OKa*cx(*Nfbc&gGn>Ucolb4%OA*Dk@W4Wseg+_oiz^UFHjB2xt3|udgNM=$_e`#>t=?Ed}*}+S%Q2U+A zMymwW!2tO$Z1GliQYBNmFgnRFjQcU)kmjT-ouR@&(HRs)=w%o$Ao+ZYUUn6 zMAipOz-QZa2-I0HPwmH@Y#<(1JM3A1)@HD}i1GToV{$!!s?Xtsm4)W;*cIY2LXUE* zb1Cth+VacFq9-)!j?)wi|1S022~4uCPeEk1$^X*olx!xPV%jps>yRNIrIa-_3(NF60gk1LNU)WIRuf zn_KJvygDz>`$B_+BnihlpDu8b50@gFa#2iSIasn@nz1&?^9vbC&3*^V(03D$o%Xj_ zPNP*aq87MCWvi7Ef0yfFk2|90XQSh1503k^75j;pQz{NRoDgTBFJh>!v#~`7NhUxe zdwkcnHa3pYD1LLQ4~;fp1K%Ou?;dEke8r9DmrXDKntkKbExoAm8hDNuP66;qUQ9?X zL@BEMzagTn0w?QoLDmsnaiRlQKh-jRlROodhEXTXuo z%8Lk{F<_u81~UK1nvrDHs~+G*d=FRWASV zDv-G|KRPvnVa2fJfqQ5u6~!V5$8ZYbjqD^8x}7vFd4c*MYf)LC_Ha6YVYAumJ|vy! z)#Lj6WU%4h8lfy&c_TR zD3`%my0P2ipSJ9}8b8lOCfNFZ{27|Q!0qI7&piVZL0Y8EnCQq$dFtimwYcCA&$|wh zvYwvxwiui(GfH%88FXE83@r|Q{q&y*o$#goH;ytAuT5X5;akR>U-nC!%wuh$34h$c zf<3Ckn-9}$ujcNYfF9v}=DAzsy@J9iw9_oV`O87|T*H_o@I_Qh|Kzuc$` z9B%_&+miz{ABO+E*Cb+!a z9QOTVJHS-%qRWldNngW$_W}rDkJgzz%}y&(5?(+0pLxcXd+WI0i@So3KY&_&Y82_I~z@VP}l-7t~$!;p1EDA!A#mM!fGlNfz@1&Uo< zm>6lE1i)nCfY?=W=dlQ z2?kRC*ugjFX)7C&dl~?HdLE&tF)mh4XvOy5>55z;OnajyDv8V_51f$iMh!$qm$vbywXiCXL@9c0kKq}JAY`B0%4%- za@cZ@_jnUOSL(kr)R3E6eVvg@G*VEYO|oDU;P$^$cW%8M9Z>ktdFNqChoUfNWLz|w zaMW16iuhs?aacpBb1r@#N0>m~Txf-OyH@Ra^yx9sD&O4_ojpN@&X582KccHe#c~(M zeCnLW8FM!cTHR_lz4CUdBagN1F9Z-z38&#M*XLu&&}i_7qfUcyF-!-cVm%%`SQ3OAZR$at5+y>DL^Wt8His^ z#HKMsu|@BhIb?2vlQ|`S6&NZZ!!o@MIB!B8AtZVJVFAwPJidzd{t#G*0%> z3Fsie5US7x(Y?dm?J|ySgwR3YD9uzX8%#zq`1#u?6$1^zmW69C7Op>p}t-q&hV7d}-g0jMFaTr%wy6*G^fcA`Bm{^vulNxl@#LF-nt|E9;V~SfgTOuDng_dV`qLDx$Ng zS_U>VLW>KTN@woRWk4j=l+R6J6`j*fWn(OUvx({l#|=ab-8UqAzs6*r!yH} z1fP7kDizs}xqYow21pT+_J%vMvT7pd-kbJ#OC;B10co!RF&TQI8=L*Ice>flQ`b1B z+nJ&EELgN(OQ*-|AaUHY)la0*bU5MkQN!q~rflvUU~*C#Y|Bh}t*fner=nfJr8Uo@ zbgJ`5dQ4OSP$jkYCAZl^)^*Ml+^620+*F~yV8fFyBkbS3l+I|CibiWPPjV?@GAkdJ zp$ub&JX~L;76=2mCkw{8*nF4Yyr=&l@>0uA4ezqvrHE}TlbZbm)=QsBOfJ?=9U49y zwTC*_j|sG8(b71M)p`Z?rSB)8^lc;H6ar^)7yEIH7?OWHOKv>=Z=NN)Lwef(&z2rX1{hw-m8{n*X?<__w_k9m6DDTAI$IyC9|it^c+_x%h> z$`lMiQ=YDm5mTe68Ki|xEjzb~Uvp6VOMU>cLc1?LAl8nk@UWNc+uZc6p;TYt4f~dM zys^-6Cj`RA#s%%xA$CELF4JKeS9@u0AxoZ#nK2ty+o{fd=ktT*f->&!_%C?_sR8Zz z{JvRfSb65==Xs_&OMb)&uffrY`HCVpWn}(zqCKvwCPobMl4|UFXcg7c1obzUI-D&l5OnQ5gq`__G^j#WgC&D&Wloazc+;El5CT-e|ftr0#P1ld5mA30k zS)&kP;BEm?3S)}t5ZmUYSE)ss7-=xjp1|Mp4CtGE`M=q+sAkZX=31(@XX--4P7Ej0 zvKClZ`;rjV2J3Z&yjsX*2< z$dY?<<+r@ki@_Ls)kfw?nkny=(erG@KDN#<=2yVdecx&oU+-{r*&DpL&g`= zX5eM*21HWE`R`C=@&L9POXUZ zQ~8Cz_6hRZl)H8<(hX4ot!*zem5?qT4z+G*kB@%#Rt}E)c~cR78zQdO#2a%~qv+L; zv8o<8MW)G~O>#bUT=zyTQ&)i5x?EILLCB?s3^O{(6oi6urB{EP&&uz%p1Bf#DbA_A z`8m4u{To}C=6H_jXP71_>yP&@N?e7U)SS8&B=lm~PH43HdU0)Bx$OlbpPBTo!|Xm; zb%km8v(=Mv^@B~lcGMLo6>s@g;0kp_iE$xZNS8Zn`bilv=UI`H--Pk~&7CSdrcXT| z$RxTMS;Hv<>`OA$_`8CU`{hZWP;QuzsYSyf75fXM9#*g+ekl9WSBAQ=S&p#uZMv5m zN&1>7S7vU#INYca9+AtJL;qm$3J71}OrR}39E+3J(eP!}hQ7(KF7<^PG;sLwvGis} zhbG^P_V_*)V;@h^tXMPAqsR{*^Drx4%sK8 z&hJHo9`dH8CdZh*qt#D%e5fbCFYm+f^QwX3yLTGy?-K$yD4vPmU@3IqGZmRwjs;xB zf;-1TjB8wTrbVK-2M`z>6tO_waLmz>{xJ6 zE|xQ2cO3^;qD%A}{MsG=BL3~Wk+jL7=_{AdC@fRrh4-ecpMK58bzLT!BrS8aATXnXkiKX zzrAuA8d5G_>4vmjs&fC!>4p-bbnZ~`q}`I~)j#5-Eep|3^&9iNmt~kjSNkS7Bk*lL zb)n<9(Ss(bY`VkT>dq@m)+o88v_T&qgU<0G*A^;TN7v3x(p#Shpk?4A(bZn=I8FyI zQJdnUuJzO9!bgh>@_f7cGgW^>Or)JME<&nwiiL>+OX>5W^4zIVk-MW5OBf z!8urUP=7T<+>__UJ2_7$_#h z{B?bO=3aL<4bIQn&pdGVG(fV{&F+d#;8$ zUa>q?)}CEv|{l2qV80zuEQ7 z>=p4z^!`!i1DuO`nS_keOr!D<%Tg@Wid-mzR`kz2fb}9%gnhF1zv{$Fn2h}lQ5~4s z2;2Gr1nYZF1n*yq_*aWXCQ@JXCIy;$ra37ZE_u>(7+4zyN}o(A>^zdonQcF8*~*be z#J(hgc=j~0w8~{)$nTsz2QpdPZ?#_3KJW0!DJN6kx21U2AD!HA*wAq5S?#PQ@Ol7K z|>lwba{fa=hb zhQ>BV-|LR}hMvaoFnMhU9vy_m4>GS{`eV1D;Yh}6mdMwXen3L?vCJ=E8#WY zXzSVcNoG-dMFYDET(h05nxXe`{yxijo8suTabKgDdXP^C2`6b<6FeYmJSokwo2E(E zwtJZTNPV24NB@DEoI~bTjW*o5lZKQeT+iA(X|=Nr)~uac#bP9%9j6{JV71D#KI=af zrKPh{Xk-W~>13s<61d&oX<(DF;!jFIknE2Ng#^Do4uq;DocS@<>RYI9s4=eo5L;#N zW=v!?RnZDnAIGGBAb%wLOJ|&GDUW7!C0Eab+DP{_vgO`>EnUrdmCKQCE+@{cX5M_| zja{sG^+3)T2funrGq1pWA6_^?w=4EPg>ca7wVLQb5<_6AngRUq#@&KEjjd#61oIOn zx9O;+op7xNd$A>q$bWZr@_`mN+6avs6>SChIPqs2~fpSWtQN*olflEZ>p2eXYP^gL#DZi$HIL=B zM}yy{e`cD2oh@~CnHqIb05SG^vO>jIr96~o+5>5TKB*6D$P=ZhHIsd(73mw(O|N6eFT(7=BSF~Jrq+9POYtDtP(Cf!; zA-D6~?K(SlqXuEM9L?g_=)jPCCBvx?I>l-7rOGGziL##GtZJT8i_J_-OD$j(uhG#w zO4O*8rO(b%8M(I2%WXJJYw&hbJDc=B6nLhWqQS^D8T#`&S(354XJrxpsr!3fH~6Fb zlbx3a8boM6TH-E^sH0N&5^b|LF&*;tyt}QZ5stO^=~Q6$6H8+V1MH&;w+d@9b#glw zBO!hkMw9$a(_h!VTxP=NNcJursIHutD^!$D)H3xy{WdiwQJ0DIf%Q*NbMyJw8JM|e z-M3`t{ZNmcrt)|aF9p)<^~zl)njR{Wrkz(^mMQ+Fk082IUE9?hN~&Hl<-~fs6Rr9^ z;Q?0^1y(8ZII;WxS9xz8*4FoT3-()qwpd$=TPf}i#apzv26u`DEgrm3plFM`Yw+S0 zpipQ@aMuKAaCe7E==*!;y?5@N=RSAt+?hM`SMnq$dCu8q?|t@O>$BEEX>9)5RGnE; zKU_+aWXiVZkb9mdAc`?lQr~dV26!%S;pKA(dqnShq%6Mv9#HoO3nayg`sclF)DOs{ z3MRe`r%`6~w+OWA{dxKn!42(liN9p`wa1wis6Z%?pjRM>H*@&tdi8r;pZ^y#v-SK5P3cM7VqZeUbiM0W7(p z1n8glClclhh1Xh{pO?wy%ER+Viuza)dh@Px%VIR`ZS|aV)Cbkr>{x%CAp+t-Q>}RXT5{LY=lh5Z8 z`{|~8!+<%Lo%=!MV&`?pBl5Tx^Sl`6n02qtw%qKAyfHJC%~Kk_1Fz@6S{k{p&hx9R zxxeI?=p@9S9wg#)PisXXt!)m$I5xqXB@=zT#k}~dY{rRJ3KOBT`l%FRb;H03Ea%<3 zh7>T1)Jw}o)_-d#0RFk?aiLZU)LMI5;+g^)jRaB;)mC_}zcYSAZSHowVxXaHBtLGW zee~YVY)b7|!eZOI2=b$<$3Ne9BK?D;o{V}y4UHfTpYA|F=7f-TQZ|_r|8RO%Qf|Vi zX&2UEBE?b2Bh)gH;wg|-*JVWN-JXND!))iw^3c+zpj*i|Ex)4(rApj7}@& zQj@cBUGGrp05WHdqC=AigKmwqgGs7dDtHq&$H6ldx4_<4p?vSs_RU6Zk}mta(_D_B zmAN}3G-8qi8z>2v5b`18?F-j&zGjI)lUfViBRLZOxGOzx4c`{twGYf61K$mJNn}IT zKMSZ2U}3f(g|fA%K)RfLi1`h%KhBWjGQhw$pc=9x~!50+dlpp-`3eS(aeAT3}P=+lK$dB^abJIkZ;#Nl^m~b_0!T#CuAwPX;Rh8 zg{T+yQOcxqwaOEr+*P21TVpGw+|ZXNz}v9qq|E+X>EU{J5JP!)YW(jwQ*A=D=~ zV96B%;eoFNaceIs`XcyB2((X?aE zY_b>$=Fe44o&fjFDY_3WEen%Jx#PXBB*a=IpSaVoug4pN>WPdVE(>L=6Oh+iPFWyS zn>jU`c?T`WWF3T#rZb$=7G2z6EAB!OE6993*7cN_==6Gzhrw0It#8~T1$7pYZ7 zd%|b%0@1tudkZpq7W?Qo&m?>9X7hmLzY%`_;}rkSaaaBco*wYh`*i)*b)jh2GwRO!W^>j%+fOcP_5>N20of&>wTOyi)I%}S%m96YsH8K?JTA2R+ z#(U$BMbu(#wHs{2?K*2((`yzb{)T)56IG-EJ5e@aBQIo|8hVn z%om$+urw;4Qx;5AXQe2K@7x~7<=QLv{;_t$j2}%qn6PJz|5f)&*hOz2xANoSOgM>L z%*XFxN_H1|{HRsRFs`?Sr!kwbh5f@ksewRoBO+4%CmC1t~27l%m76Hi=Fq zh6OawNS%uufL(<}Uj?nHyXg@M@A-dp9z@w10oV)?!+&Yy2_a1SrC#yB^wK zuC8;_pDx@*UhN1j7F|5fav5_dbNyTn((NVXN1eCvC_Sp>d>+ib#6`{d)Gu+Nx0;`W z#EeyKdCBS|ru?mkA6SlxjuHWy?)#>3$Wctc}BEEa}jHS>4;7F81N|GV_92i?S zVkISAxSm%|!g@i?LZ2VGo}Na8^7wn0Hp@E=W651^+iPlGT_tRP+3Uz%XZ>RQpLPLa zQQ%?(%q}N&)yh)Q&|rOHwNe*gM|lyZE@r-uozVcCeBM6tS)P}6Q_@IQ)(X*hwQYa% zO{|x;Wa=k9zb{)FdSQh+ciM+STRYie5Jv4=HI=A;VFA$q<|vDvz$I*mUR*Z7@EAYM zIukhhXiJ}etiBmSq2$=AQ*HOhLH6wz%6!&`8MdwtV7mi6F(@t7vaOHXHC(H~;Pq6C zg>N1rImUvtdQF3Fl3070>{_X=Tx{9pLy~z{U@Jr7b^k`SBth&s& zuB^~4vN6K=1PcX-r0v{9#RO+p9INnMc5Zw+eNna^V4G=X`p1=_6V+M>T9lo zK{*wt-C~Cr3`bR1zjwxVpzPD9cfA1-a5s+hn2eu4M_VQ*m$&XFe2z>ibqR`niH#*7 zW%1sacW_~@wNY9(5!7vb6DqL{kPfc&c}9L>(mx+;J3+9i954R8Vqm^K!g3@xX>!?C zpCzdrXuY{MHaNGB+Ka97qQL!!lU^;S>P3=2OjK2E_<)E{L|9}AzO0AHhfI?UNcJjp z`;h)^p9OkBa}vAJgON+hy4;ZhWZ{?CnsNNr8B^2VU96{G5n|sR)e2FBnI^wu_&Yjl zC0!dCjhzcg_GK(Gz0#*Rde=w&zfvcy7(NHZW{;_02*LKr02jB*`vDQY&`qu|y-+gZ< zTAT}SUHia@w(B1Z{?V-?_}C<)R5z7O{sdQNr*QF`>7xjXlz6?ALL?gSgf8qMtFHz?CT0kXx z^R>=&pquP z9%9b#?9V4A)fauEl(*Ag11Yv+H!5Tml9q2e_HWC)M~{26rvJmZyKeI+9UwOR+{8-b zd}(AgyORK$6p(dJPS*QwrLZ&P_!yg9m%kJlAYPX8O{3apJ@O@_8p-3->|l7VfH^O@ z8A`h8GM+$EQT;1XKzesVW8Dsgfms#NGn70KOUeU#U9U*Ovk%-}lAV;n$PWqvKdn@U zDr{Z_Qv3d>t`6iqSInrG=dgFxCrVmQ397@gEVIL5#!@Y^hu0XCu|d6;gWm0{C77@o z0KN|u9&sXtt4%@YWDK--ckR#JtT+GqZ#nTZvj4lDfd9Q7I@~M2CbI5UPythWcl$Qz+uM(yBA%v(fut3%lqhqW26phUjPEB< zn=dX2CYogAwf&gAt8DIQ2v>jdax|;r3w?+3WxWNKrg*pj?Me1LFbRXe)TI*Qnh$6CtEC?Sicd#GJiBMUN|(+bzg6lFSXn%s!47yK$PVdo|EEBkr> zD70b69_6+}Sy`Dbz%-K%<*bp{_1XaHaCHmi)jF%>IiKzKY%3J5$r{;$xjrP=mpg`) z<+|Qwg1COzE|M2F&-;^wNY5(UGH0C|ckgNQ?Ri*0(m&tC;?6I1>4VyU`r9uCD3(U9 zSrnM>XgF)23N}(>@QoyTHe@FC2lWy}fBs9C8>3Mq!v3{_X@qWd_@ZQ-hixf_yUx~K#uOV6_dmX ze(Ey$wFJ&v^Y&DYZCqQn1_`Tk#CsmnlYkQJQGPpbE)A-4x>t|JawW?)@*Rx;NlBk5eTYbihN?^QK2ZUwDJqK_>4p-u4*#say# z0T%8_v+KuaKf}Cy_4b>OI|G-rl?|FO0OlyzV-w}V?u^+QO(<8JPdvKfphLx)*$%s_ zJQy#IavfaVKNtOEqLZ#!Z3FX(@LBpV6cA*~LyaLHbyN9;#862gDSUMnEzJ>)b@_(X?;D1xx$!mm}rkDO&19 znosNQm~(7KdvMguM0MX-JeY$#jFuS{IkDhHeaQeOQ6S4xP_!3Zc@JhC^Z(qw0?zGZ z@K3m%S|a^=aQTyjr@Wa(sPxFR;zhAU#qlB5)2#F~0slYi-{$;=b0eSpbJ+|U+!CU& z8F4WI!D(DrQiBS&9=W`JvWGHOZ)LccRY50qFl*EBC8nImpgd-FSkvulZa&DHCKwBw zF>$WVefL~svH$ejj<1Xr3bF$QshG>BygF;$j)q|IXpB<6ADrDu6ksUq@B99nOUJw~ zv%Up>D>0~8%V5B zZ~pClN(Hpt!z&EbxYisNdH(oeMSUJW6Aj&qIg$QZJfCdMoX*wgv7Ly|?B#S_>2F>o zc&2XXUoBScN(Cu|w=UF7)_W}z&ERH4=f5p49>ty#N0vD{$%KE4z5K#cEt}h0(rz}M zm;52(^RI6$P2J_L)To3Ir;v)Rw^VoBGq)j{dlq4kaUSPmqE>QLYOn^#L@=paV7&ou zs=!2XJTktBr)`Are2TxFy5l!W9Ttr(>mlAgY`lb~r2v z+Q)<#oo(sa^lEgXBb><%o1L=+s+q{6wX}kfTaREX7P>g8-!}WDzbXxawsZ?3?{JJs z$H-^cvWK6wN+D1`=BQ-iY4)P@ft&l8ug(*G#xWL4tGP5)5XJh$9=N zraL0XqhA`*3oUHPd;gyV zmAs8sP)y_FJCI~t*lmrMD8L5@Z%uIPo0-Wwgk3RIKc0Xa)R=1rrFCbcZuUKgpUDBp z9c-n^(q$dqca=a21JjY@5skVX%U^u$_NGH8ZTb%|+g^YE%%N^I+v4u%)FCI?R^I_o zetuBLo3Zx@bL;s*b3)m1)27W5s%*%pUw|2h5(ymXFchsO73oDrESoKKXjy30KM zO5F=M9x2rpBeAev=X`m@lptUJ?0{;zbt8N%pBsei%zwvbjf^XKxV$5aLUQ~?2jMSK zYwQ??(hJ-&7H=>gV=?PRV2-dH)fl9=D$VXq{&Ts z>Zy!Ny4VXA(Sq(@-ZkDM%mP>nvo7j&JhWhp;Kmt z*VX_xd#Xud*pG?G`OJ`Ml%0M>5E|#vT)}ou04OnB{y&h2?+8wsgmW7h^$rVFG?h!` zGPz)0<8|4KZA&@Kv1H4+R-PvRsG}@O26$wl0nBxJ<>BcSJ5>;yQN`sJ+RxdQ+p30X z9?9;KizXd_y7O-%RM$rZ!O-g@(!;)E?+yLv9kv>#IQ&E*JzVkwgGsgsbX#yv5epi4 z;;|a%zu=EA+I%#e+M;vg0`mT?-%#!9i`*pF`#)$SuiYiAV2RuAx2q-ckA7Y$T^7o} zTh@a79u=af8^>J_A2FKp|8O}l7jIRrcRj|3$&++;U(XjQBT`w!?>w4=JeBX1f_zJ^ zsTdeDMeKKmqoVhC`dpAueR!$TbSy`H82K}y?KwIk%AlPKpRXNLK4C|sb3Ws0rS$9=ln>3#&n_60v*(z?= z1qF3^^;6CE))So=6|n;PAH+?S!=pZ`x*u$br|J!kW}C|C+$>V9`@Ea0dI9TLC2k=< zrA!CvB?ZO&IrB%>Xy);1$9dgV(Pc{aoE|uY?|n(({udT-#j`fog2F5w*#grjw%gWR zjPGX{$5-P6xb0f1!U}a2(bS42O?H|6y>aDd#Z8zGr!5$@cxy2 zZUmX8VgDO>)K%ouZju}OMdB}g#4Ah1?uFJ-f`KLYC}C$oqA}PLmG+szGGnzI5O6sk z{wRW9w(T?L)c4Dj?oIvHn2l5|>^gv$hTf`LPo)-4JF{jc7^T`g$U&(#rBPPFmB#h; z15Br~|0&D&j|FDbMGa4y0ufXa>v!6*Ox`=?fQ;>v)bK~$1m#_<**ICYQwHTCP5^tT z@tb~P5zP}MR?H8mIUjM<#m{ep!&NoNCz*Gfi3JSbU9AkYv(#eLLTnerrNrXOAHO2z zvUWHJxWfgBqyF=h#D7mq%=oudlQuKWJ73jTA^-a7|EsTav_9*a7h3?FU$f-i^7gp_ zreH-L7Q6<*uRE;#w&<>R?`$OUxhDQWJ)p7{cxm+bkq@Od?2|ylWc@{FCy_*f^0PW% zd2)8!hLIWcXbC$Z&mHC5f+e)y5c=;-l24E|Jrdc^Ef_7AzVG=EVe8tg zr`b5}t-tzEqn0eZ8x_s*I_j?(s8czF_unl)IWZ(nh9E4rnTDMLk;&X1HodM&5e*fK zK_-2MXk*@mmh}VYp#Wd=#WA~0h*Z+_0fsP)<9n6GO1>gH{X^>afq!_!nZ@`ype`$ z75!W*lx-3Dx(}?dti4TpJ@Zo0yos&ZzR`y2*aWkpdECH!;UJ8%a%yz#+n`@blj$Jp zY2gt8J>xATDXp(esYpURMWJviEpxvFR+y6)l#g%0+?GJ&%2P=lh3ZB_4-%Hx54SH z$0V$Ynh}0hjI7vw_nn3G>dhQdwH|xUM86rAiI@hK+}2qJsv#P*J62I8L0>t@NvD{U z)E5ggV6SbpBtHdcIm*NM3UvGFT(kr!;NQCQ1&TKK>;swj1i7l);daBN%9x5a^ykmk zYaw|GoGj)FnRVQ{HWFb?7x8lkL=WJB;q}?%#s3xi&gixCsOi#@gVzdtzJ{Mn!_9(L z7H=bmW)KwC$IE>qs5MWryWngfY0y>BT>hZZr2BUP8&#JsmCM_C&gLKT+D2o~G1v=} z94gZ-A`N!mzUS2&iAo&%@ZH)JkOVgyl9?=o(wP}^E*yGEgyLS%_<^ z*ek~gShC$KdF$>H7>ksECzeE6s3;C5rvqC5gnRdnkw(2>yT4OtNT5Q1R8;N<72b5k zIVQ5rPFC2ROt7*1K9~M60twQ}1uBjnCOl-MV9H8y&SInUFQ<7GnswAketlaFyN=gL zod!OO(yB3fobLWRBi)2=^FA!xB;LQyzJ!^w+isO_ptqQ9k1#|iG}$(yFt|{`fHrf6ZH-rN zWpy%4xEaUU?0X`VicnL)%RUUSRPT8h^z)h>M$ zH5dKD^k~$BJT4TJ`%$Tx+b)V-Z4}WI0CDb0#7OebU(Lf#4vo>j%xtL`raPUrhE=b{ zaof?KFP{`%oKS=9kv8q?7Z}1rP7kyr=poLpd&f88kyvXU{7pj3h-HDmH*)}zCUgJA z_D?W09<^ZW1QN8)UJsrtz{I5ca>D>{ueU2JJ~q;uBZ?IgG5tY&E%>ta$ZN{mdcA9> zc`NX$wX6>rKNtHl4b1DWdzg)nAzYG~x?W|`-0Vji?FDx_1-@nNIcRDjox=KzSvsbtG={Hakf5TJp)GJT7+L;va3)XQY#M zZ;&k_l)dX%w(1wrEf-5YpAI>lZEq>9OFRa6^rOHUJY$3wzJ;X~-z9g3t2_pAc(#co z^GLR;D99ZaXmI4%qm8zi3c6g&V7rt%)iZQxkh?cDU&yLq6s`spv$jv&dWBiR3{r@HOrXfAnv!p5G?rY<6vy{hH*+*$mKYf zNxFc*$06w#gJA0?FkuZ}exe4difjEeK+(nG{Zn}dbOK_5s+u#P&P^JV1}Len(|8k_ zW>C)eV^VLDUYF;no&;#IUzpd{?IP`8!N1LX%7`HxXsV1n!FZUb#jC;%)XVtsP$N~Y zsh@M<`2B-eu-8ipvPIOjx9On?(=*E^;A8Ad{gMrp*%f8pftl7{b4v$Uxj)QC-X!)L zjJYnup-G*Si77tO{0+2g_J28MENpl$FT{EFKBTgLaYvnufhh*%O>-eZU8fz+y4Q!O z<>)8cs;$&NQr|4#Rv*%BZ?U<6Vsp9l!+Su6&I0ikks0N6N)%9TO^+JkZ<&Pxv`=HZ z7V$!Y9EyyqQjeUyY#d!YD^nypGYbvfKr23Zpqm}v3Bsg&!Z;*ur{krkk3kC#0x{jEwi8IXNnrF|7`coLtlVd>&qf(k!mc?%H-X z`=MR%(+S67N4%r?)M5ahQ3pjGk!T7im-f%~XbPEls|xlgGU_>nTtk?W=78QnLzpDpF`{DeNj= z^HC_roT0^x_^k=s$XYGsK{Y+MPvc=Dl$J>Rl)H<_|j>XXb0y@v8`B2aem4`LAE z&=+(!xI>=XOwpVNBl_C!nROAP2gSW-JdZ!!y-NPieq|5sNQ;YrW@XJ?_k_FMI^C%F zR(8)P-q?YLzy5jMe&n~*KIE6XUmjG%egaQC8H;6L2sz1`)n@ah^DZJa)v9XU9x zp-<^SpyD8I7q&B6ZgQ@93LyK_%LHGe+glOA4Yv+%+tYs|83HOh3!c51h<6CPtrhS) zxNc}jKRq&l=bersZYwSpLPZ;BBgyi|!sQh$JcG zJlaO(X8Rg)ySI7Zw)&_4T>6m;f!po;%AeS0L>xW7UODSg*9_ue_4C`}Dt7|XfJ#N< z$K*76l<%W2i_fpmYURKllt+yig&jM%lf8H}1!BKsKQ7;H|C-p}r`Q&yX&~{xM9M97 zn^(5_9^f4qu{mxDXuq^jYTicXi2s}~i9$sNHR-Hy?J)lMD^Xa zl9PRIgKP-PH?KRYT7Gpufk-%>l_>Q@L6ijQfBDO7z`{g{X0JOl#3_mV--lBPyW1@x z;vM7?jBmzn-`$y#>~7o*jC;mW?;l4}WeMp{qLRD1B36G<3TGYeAzOap-_qhdw)*%{ zAfj0~cGrI)=kk}lQgno)tI+L1&Q`yJlqjb(P;j)$gpADj~-Cten;XBcPl!mEPC>(*Jms^J*66XaG zub7v2dq3k~E+~!Z2z^w`^2Eo%-LvU}Buo*?Lewp%y6L~cyS~99Di{pGv4zB%&m!D@ zmcADEkk9JA-HU@ae&7EXm@V>L^Gn3nuU|LMku`8?%u{#cBM&T(aA@@Bx9IjiV~!-D zQTFN}_uroyxc((fqsF?kg52uNLQsK36p_Jj2bGr6KP4*8=Vg~mRuA0I&N>?TDe%QS5EoX8V3SGG0YZI||fNqBbPy&gyPyP(Y}u)^z@u%3Tm z0RsPIZa4jbTb0`+dQCoAlsPi+yuFTCAEgAX^^t`R{7R!r@vGb$c1(9ZoI#e2%?gW) z>@-iYq>fPb4z|>^SZIXOqR;(a8>x9&oZCEuKnCdtdrZ;9Pk z_bP9hV2Qw(@D`P^RPUhR<9stRj@R4ndoi42-2U-uDJxz&tAUik>Sk-Pz})eGq6h|MuUw?>&O^|B%XIez&)L+xP!eTFL*aX(9iu4DA1Ej4P6UakFT|suK7HzA~i& z#1|hFGrrGNcU1-REi8BFoiE9m)eN~y=h(@!b#Y~0#4sq|cVr&_+&a09+I6GDP>lVG z_*RNGQ7(96sC10S#(W7Dy++FylzxjWzA*H&&A2+P-rjdx92HaPLm_+gE1*rhD3uH) zi12YJly2Qu%vNXkqF1M-sOsP58<4|`@4S+b*w=VjiWg&ymEa)1=u!Y;dV)x8M{j~V zv>p18yt6HiRA>F0E&dR<%iD9*9}D5)$cY)hFPJ2YlQwsjx9rKUK8J0#nl3i~g*Eoh zPk(5?ruB*V!sL>as5n{cxk)Pa7~bK1s4v6w-D$sCV%`S)y`o$JJt9N$STO;mfOeni z#>M%Iva(yD3z`QfQNegtMe{sN<~Pc+Z=W4aTMQ}*NMSuo%}=MZErr{-XMPm?I^pQg z4jVH5qP9*?P~lm=Ql+loN;%)++szwi`MX&@iu!W>xN@?ATsu*|zE#8Ud@kYgHx8YS zW$QE1;7d>Q{z9UQQYv3>{&K%oPn3pzpT8*vjh$T9g{vHi ziOq7RSE%!Qm-)inHX6<5^rWx<; z*&7JgO_FzF_C*6h&X+OnA+_zz&2%GvU4^-Gt0fsU>;e=`F{YMhvL1svb-w+skXP2D zi2|942nI5j1i@B$nkR|6wlvyR{U1HGzsvd7#q2Ou7b>#&8@9cTB3OSyV^m3v-)Ln& zy#YzWD3)WgS7qKfrelf$jVLllv|?@~O0C-GSKyxVD<^+DoJ)jC1YJ2~=o}O(_*J&u zG`@2SazuwM`-B?$aYj1KHAaycxet6S*Ev6RIK3see;_esg{ZcamA4Pmwav)1EIakW z25~s*GFb&HdniO`JIcwi7-$zvzGRF6KH|fOV|*yt*`adjtfl*( zBP@70QhAF{v(;2 z%X}XCC7p!!wCGeK*^ojI!$Q>uOP_(^VeBIr0p9t8w7RT7ko(KTFk z2Q*~ovN6sOIwCYpG%-bRy(45Br|Hv%%zo$FE1jy31|ew%xF&bIC-w9 zI&Uqwtn&nw!Frev#Qd$^u+GTKTGLu5fA{eQHP<^9uP9yvWPwb2?Mi9IwYxYE0g0Wr zVx7mN+@GdhSUVY`c;UxwW2W#PX3)@eL;=bg;AMQP;Vt4b(;(tE86~P0AS@)Qu+6_G zBQCb#2Lv3E6vA4>@qMh#Sx8p z#*gaPXoQS`c?;LiaD2F>75!ti?kt=z zu&~@SUbw_(yGmOsxjjxds3{x>y?1zs{3nE*jF3p|y!0xRXv-l<9|omT71v}4p#X5a zR931J1+5_*lvPWFsT~!@d#V`K)a*gbkuZ`FbLb)9j$mixsKH znMg1?a^{7J4-Qnb=!2Ny>hp?<8XLfgie3F0_8l@MU!VX>qt9YS*V+F&mS$X;-WvZz z$VFK`|GN3+#FwU4*qP?yV^{xP7(xILU(uit^T|Htbnj@6VRHHbjle;Y&}O8Z{QV=% z!L)QBO3GhNzh3fS9OxTmOW0qlly0=h7v&1MIT(335#@frQqXAL7&Qa5d75<{a_}?O z5D7u@TzgQIc7+JCeH{a_hr0J?xR-%hxFuTMHpd~q1K)LBq2Xk9bQ#O?*Bo?3{9DHn zrmXj(_DVayaTFG_UAuktOc7xip;wFgwrubb*D^nE-Nb$9s&hC7#5wwH%Y4Cy_u#zV zg0Y%fFPEhPRs1#21~3g{!>Zjea{->o_0YWcBR$P0oI>EB3#Un`{cL}p~YkRMdi<)6@gUxk$MBc>C+zZ~x{V?5`!yqqtk3^v0m$&b?7?HOnEA`EC)n=v&Nhs$JO`H3E+)wbHy#>h|oc z147lVhyV$tHP`Wy?Pvu$h|>^DXlem(mRI5&EIfjA4Yb{s%>7iV z766h$)+lx?*9k{kYZol02xuW$=#lvpNlRxPAwINR>Q=Pxv+G~n4)}`J&C|OGdxplR z|0L9&R-sW@n}6xaX2_0fRJbl9F=h2Hr%45SO0OxW2V33|#p8O%jLkF49(b4hZPoJ} zJ2)u6@_^k)Si)OWFinI)X0LbIf){=^weP!W5sQzO9n(`wJ5N1Z#R@PIv$ee``gi%5 z{PifWRP5$XDs zDAm)2#kkuMF5~-jOb&yZ+6Cc^+ALWiiLq?3JzqmY7H1Lu!9} zSQJOD@CJ#{QD@Dr?jBs+kAYhz(?q-nthF4UHOFrD1aa;oNa8k` zKEKiV{IlT@dG2YekYb!P*pifC_EI!xKEG z-w#71Vib#?OIwVQcICs!Do4g+5Iw^R#cjO70v92uP^J!#Wc4T5!JPto1kd$mmr6W{ ztdB0*@SvGJ2ML#R2dn-bz5op^>1eU?vWvIMwk`R?X?;C!-QebxUa1=tHT^bWD+Mi^ zhcw0~$a2bFX3tx8uzVOgofgOAG7S@@S>AE`dSg zWSZ+oP;OOY4f|4q%Vy{w5gDLCfkHn=y4g!1Qui_8eAB0Iftxje-GL497^JlwnR9M- zji1NpQ38qCN-8Akuo*dc0c~}ypWt`oYHl`%U5pqx~UEA{;vM2AwnocUT8HgNh`qrO-@~XIPFR%xJ>SYQA0;%{u87XA27WZ2)+pUT;^iECml0G67Gd8VN{vUlc?X5{ z)`OcydD!%FM(bM46e9fJb7Bd3u(BI8AHGNasW&1!5G0%IOKHiS_Kza)q2aBlQ)FRJ zPslQzz;1cSiG&-r4O0`xQ+hOSLbKR~op!>#Ne0K+S@arn;KP)JQ5cX{bpqRHV`yXq ze#;h_Jds0#_kC#pXBp5yJl;|e>Em9S61|2t=~&yaa~63QoWRQ{t3yKjvN&6JGkMDp z`&>2bM#Hv6M^}wRie@@4K{jClf!S2Xhj#)a-o0}w(_Qa`w6ug-jHvoK@){FFW4VPZ z%l)pKkR-yS@g^kHD%WUdA~6={B*1At(7uvTT&uY}r=Ny+gnML^6{8O6&KNf|Ufbj< z9n`{;pQvCI=xv1(WnQxl_K~Q~h>wpn&+r&22KYNeHylnMi7s}vTtB?~<*$YQnRM?d1pXVwc z8ZUkuOJ3~`UrnL_kbtQJR55g3*%RMT8&(DP3BoiHfHa6BrxZ5ME^Mg712`T~b5_S5%ITK}R7$>K1^H}jZ5i0!vi##QuK;7&Hbk=m z0UZCp-l6x(uR>Js0-OHFsi*#1k&FNQxc_-b#+O@h?^CkbpT567UTFb5RNNg33@CXg zF{kjtMZx^zRl;oUr9IQ}$>pErY2R7cJj#!Z3iH>P{qSn!#}^Y?g+~;&8pI!B&PLT3 zC{Q(kfyoqL=WnD%>gFdME4@v4&LX{{>+_jg1Ls(aD%mA^>u?U;g}HMN1Y$SS_Xr}6 zYV>;s|CWly{Tuf56 zizGtas|6W3=1FGUs4thLb`IdhuhECzYXf}s-McYHN6Up0Q&(8O$rzaaZa-{wrjl>~ zk$oRn#u%&Va7**|Sjn&L&m#OTD%;w##10!yTiS0fHbaSl8>yDb1&-&PN4|d6NM$}T z5Ba;peJ0ak@95;Njwe<=sa)Cu{o1Z#M%OC}wI}HPO#L?+PG6eKVrEn??+B^j>VcE$nO*%D-U^w^{0JmJR)LL>o-)kl=G_^LH*>bgjNjSe_Dcu zuMT$MlZC>gk3iv^S0*y08&SI zHBQb6epE^A!ZM0fw&WfHzVeE)#bb8brhYvlH>^a;#_F~lf~A1kNfFA;SahAs?&7=x zyx;ICHPyg=mp{E*6ku5sIIvhv|2__q=@=F;Ss|&xh6^%jB%x&X% z(zC>3F&s^o@Q?89vTPiFg9918mehvUY_H7kQwLBE_rVp6t?S}SSy1y(Px1NZPRw=$ z%4N@GelwJ(P?gfKeW2Y5wm7!;X=XKVxTlL`^~G5Dd+T6Et)HYo`DJ!Ha7Fla8UIP= z%>1gO4s@d@B%3xlWgtUI-+%NBTF-NWJ)YV#+hXZ#ZLcxME|dW@rtk_ADU5*dG?(r<5HT#4)5p#2lf=5X?HPflq4Ca;w^?8?{biD6=DquwMn7PGEay3~{{Lgav zU6)F{z^MF^+|DpGcHr*GMIRf4AN47lLl0kw<7cj)_aM^gHXl-IHq0ou&!mho)+1Bh zO!v5ZU(-Q|(vAN~uIzCl)wZo}M&z{%oX|5{Z>fpau6C zlNLo8_7k$LYWJ|sH%Zjy*6N1W`tS}r&tlUq7V=#dX=u7>ef%TeZ1P5*wJSv@Wux3l zFaG?yqD@UAMZc1Ef;ZI12*f;}HBvHB{6lI=*kN31tL%-NXbSsN`DF+Tp_Ph${i*6I zLz6n_yP2E$VWHI2ZkiwyPkUneNiM|FRPETulGich0S}MSL^FCIEwJxkf12L9Eso;~=!G7*cQXO}YP!8KkjZ(VY*%Ql1hh0i8z zh(5B3)`qmF?=~WI{uZ3K;yL(fCz8u}ka3QaaGNmL`*^Ug<(g@_yKG5{+fZk80f!Da zk)mv_p>S%ob+z84(mg$DlG-!(aT%a=sYlj&vNm>J!J=ze?tK{bdycwqwj?bP^vjO2 z5pAh`PX|c-kg)w?<9C*DNXQMF2(tp2QuKOEBWZaYG-H$IQsbQN$>7-GMeV{NFdoqZnDL!TKW-!ut-t#?sA87Lz2P)_M)Xc7rnU zW`&+SMOdYpV1wre>!radD{1QVN*n|7i52r>!NszAx)dk$SOTnSC!t!YO;6agc^?7P zgQx+0E1#YH3lJ&RT*j>yPh{LpK2)2!ywa@ck@E{4TW~7^fDuJYv8;BLif;U=(_e*V zRF*#z;M5&nV&^&8s=1ZUk!co8lLOabxhe^lnUg_;f(jx@2(EMeL@-fJY^Z zSbkXnkzQf4?-2Hr`Dl9dR2ccA>$Igu0hbt8foRGQRRH=S!(L7&7Hs#@8l-Z3@uWeWfS{z0wOU^ zpRTDfe>>*y^^>L`Q3X<7Tp+LcWl}ZFp6q04!0m+FyvsfcE$GFdE23Cm4OT zw!93Xw<>CE4R;Ct<=s4xK*`(8+{Rz=a?tH$byYby;nLcFvp0Tz@u*m=>CGI^o4D6e zp%v%}`yQ6x-iKh7j?;{_a`E9w!oZ&bZpyp^2Ij3*tTsOw`cD9=V{*N;m&KVdZ@Lk zYMpcT*(Y3HRtz2%8x{lv1YSa1SP=vS)D{E;yc+t)zm@YKG7}JxUmy~~f0W&`F1p|a zl+<#*eawb3qZ|?$1}6PTHU9kgOng}>LWoPgD?rZvQC(mN(IQZ)UW|8JzW1|QQ4Cjv zT1&5euh9~Sega)B-iLmoCzPDp&)=)BxD-SS9+)_|s_iOW=8$;)lKYP=bQ=!u?GG0oj2Nko>p*2O%j4h&$;2=yvJ9NUd+`ul^J<_mORP$*gU;i7R0H z#dDY+We;lgs-bYf;8bI_q^wQDV`g^Ttr7P4&1_ean|HVHP>>*g%o~z0+zghh*d7-s z32_4Xe}{{pYSLMr$`_&|+Ob7CK!+|`BJ38XTI5Kem_mD)IfLT95nuEeL0+yH{zzi_ z*U?{($ws3y4S!A?+<>3til@~2hN`@ucG%%j+Cu(G2aWl24qq)QFtcgF{s6yL_a1_T z`Re^q7Nft!XQGw+=P}psGhDS0g#(VvW$6zGPVaUKqhkNgxEnrq0*#?;b|A4Q*Sn3z zLONcLhn7S?NDgFuH(?jP7iQXpKmhXxP`=4zfDb6&qSx(=4EFaLnpm+v#fB2IhS)W* zwCm;FOpT|mw&G1ri zNGZ>{3K8}vnQdfdlYn*u@(`rfuglo`{_I68s+LP-GBo@s*RdoWA9tZ43p}LctlN(> z@pARv{CyTtmCN-|xdoXxT1ftj(*xl)7GX?GiUY<=)j-+xYDI77c5mlfAi^}IAHNER z@c^~e)6;0Z+jSVC*x^|ObHcWHt}YCgbv%!el4F%Lb{G}o`9ekWTm3u%D;6GenW0k* zTi1xvN-{4$mG39EN0@w=pd$FZEVi>xMsxf8wQ6}5FKHORss~33^ALK8^ZM{|^DKGU zZ8ZD$+uk*KkPg=UGA@1Bb&m0i85?l3lHQpr5 z5%b`pr5Ml<_~uVDJvSnlLZ zr4^RHo9r>~R}vvL-eyPyRnAr}uxCy#dRq!p}ZE@h)#PPFn^;s)$}OreVs9P+jNg@?=#eMA=_+UZKu zbkj#?S#<%|di(+6@BTv{F-UXw&GtZVZ0PVpZ+U;}M?4wj&5J&Y$sM<%(uD zuEXX@u3^>>=>C5e7cN&X-O^MZHLYl-S))1q;oAIEEicDSoLzPG z!~ULX0~w&}Xa+S3R7xG7f#7rL-AM@_>PQrJQ07~Twp1UXDgas(jl@*rL@yro? z(wH5Lc4m7wf60tt40uGEKr?&1$A`>w&c&?u`=VD#=>k{h8jVWbLYz*51Pnb_bxf!rmQ_?rWfD=74A`Yzb4(sVI`_LN)ex|aSq3+S^>8soQra@dBEqIT*^^z9?`=p8LR((YvsIn~QNn5hM*pwkBu*e}4>{HKe>G z&B(r&fy)OI{JiEHaE9|n78eG6ycmP>+X>$HJ$hUQ5J|->Pqx>F04W)=-vX(U=}=&u z587p<=dli-(3$^Ht8Z54I*6^C2fe9c0zI%CgeWIJaD2TS1HGMB(JXFY??Yx>NemGc zK7>Q%l+bd;|7qZI7)p>7E6iQe3-Ix~)`C?N46QpI%x9XsAzC2B@>7IIsbEk@95)Un ziSnddlZ(t0(-u&CoITwY$s?qi(z$&ec;LlC=fyT7=jXj!Dy37x1uH8Krc$2=c?P8= zm4k2`CFQc?>c?i%?2U+M6I3#>(#C#{CYhmT>!_zEea*nf-YEkietBK z9m?er``#0j7V$@X(lmeoRW!IQX|T%X{G7Pvlp#IsNu$W$I(SiyWUqrs>wS$Cr8Ilf zns7KqpW6agSa;GsHBX8XmOREaj=^_%*S*EG;3Z$bUzq*b0{)quy9$z+Q}+Bq)zO}1 zXv~y&w{go1O`6TG5mZ`1a!ZTy3grzMJky`|X7UN^fn?b2K{{c`p_~z_{Jg;s8UN`S z-fcD|Py>nYVVL_!_wn%TuAMHyGq0ewW9Uf=cEWg|8%(}P)1g@J+h%P~dq*8gEEFk` z9=*17q*wnvhLcY`Ay6-IZKyjRCYWvkJfcEUM~%C(xqMn0J6SVzxj&UCe;1sO2v_sv zJfkrYKfMVthF~`F8ZSzvp3>k~)GcLvUCcWi0oMgeu3VFr*g%cA3wZ`pB2^l*HAv!s zdm6;l18;crt*C>XRb zaSY2l{HM!5UZgR7eAxyEo}_W25cQdmJlgPBJPK9j2L+aw9U&in!+EMEjpH27t@0E| z;D$nfwBMseX)l?dSzR)h32TBWf2Kk!d(Y0a50okLj_Bn@XCt{D<}YUQpWJK13b8z8 zf_2EHX7}BS!F&ZXl{rEwRoUZuh=n&esbb3=rJl~1WRuE{Z-%8_Q#*!?w&tI5QUm|+ zOcYg6aJtm3GigF<0uRWPNMA)Z)q4|1DX9g4Uo={FVlJzzTqs->dgU3x#eeh!|EI4iXF)4e)6r9e^s+$>K;_uDOLx^=c9a z0AdUhTn?&{4n{?X^uc7z8g|tG>rGI9hfI#br&O@@2Ly+ztNU0Srs3O_q?- zXVkem!#@PSm;IWZm11dO1xjiI1PKaRXuDDvtKkxBADWJ}ydXz%nj>8_BO7?!RHh5o z%`QHw+8c5^i)~x0nrmNZaLLg{Z$^g_DiG#%BHFF$JjS?Daa_&MVaHMA?6BJ)t}+{=ny3rqYAcLI?NB+qhC6+{J%r3iY|@ej98omdD(N^q1#{9m8SQ8nU3Mqhrdd$Cg4&&yi(!-Q z31TMu+qCJvG1{)_@TSb?8q^PPvzW9(6cwAf*)FD5>K)LRL*r~|I653oLb)qepFNX! zW0Gb-V(_(4FJ_XTH}Fz({%F`D!XUd0pE<{Gs9t{546tqRMN9=4z2MSC{`OX@g?0}oU-#PB`(kf2Lo=nRW_w^ezJ0~ z7hhP3!?tAML7#^O!VBnRcYlilX31Ysi&+t0TA~mZhlj`Zw(;+--4(dEYNQlcsa?{< z@u~kMI0uI)X4nM9F*841mr<8X31SG&SpIy(%kAG!kZ8Kjj*C)T9RK3~Ic)rU%7eWM zsTDCY7_kGABQu71s5N#LBqz@2h;K&*hnhf5m&5dE3H0R)eD55o@Cr8q(Q%{ zybpf2dKZhGNEwtj2eeZSJ1pU%v)TF!5v2|QZq}Y znp{C$nLHZ{q%XDp6kxT*=X9oeRLAJuS_WV{c@0(BT;r@e|I6a|UbayYDQu#gg24)^y^8*f`2U5ug#bRfXoc8Oo?G|`naI{9A2DDnT zEpg*DO9ZexOef1?Sn$0~j$3Y3SwjCP)b2a4uP_hna=Hv|IG+jM3w82cy~CR@DsAli z;z!4G(5BHF83-*QoBBv&9WYVOyFx-z=w;&ol7i^yt zKy>;rHkCJ9#*OEr%tswL>BBsALtrWme8$d`JXuo+Y>ZUg&*~~u&e-k67B?k0)hca) z$HpjjnBwdF7o^CL_1})=NgWlu4-l)>Oejbch18^1l-EI_dbmE15T`aOF!ZKHBg7^` zZZ@Z4sLno{nkx5)Yj2{Lf7gllR+dW`OJPIt5Wy5v7*Df6e=N)3XM~SAe#f05)-SP* zC$g7}oc8=IDXej=T%D;__Ey$btnm!PC}$YnD5!fX-=_CfO|NxOx?@Tg@9nFC%+}O+ z|FbL9^Ezr^%~#K=5FZzhPe6x0h0%(we=<*XzC~Ytw=lXj%$Ud1POHQHB$ul|;H$2! zX!h&U*OO!w@xq{~gE!TR#Q^8|i)gnw_(H+u@#UwE-}MRiV^vWAzpYqKzm1B+AMSQI zW-mq8EP}*3n>PN-t*z5)ZrwSn0g|hVd~uUg?yBTUCRnz&_dJbR-;$jdjFP8}NThXU z#N3HN3Zo81qlYf$EjAeuh1ZIprXD}?&`|DidKwii8_(hp&0AE}`GOwqI@(2*jAdP} zn=8mShqFpelyRfQt3cs}$+v=vor@|Izalgtz3g1Qqbch%vH#87Pz<0|F_iH!J%Roj z@rm_?2wRy@(x9DWZ=vw(>e`EWcykhgI3LpQbZN`jP^YzP$`D zRtQP>Nr=2m<|$mx+hk z_MGBn9@de5nB3jO6Ee4}slZ%sf|r09lJeVoo2X@EU#}UC*zmx?d=>a zq(A}bCTQ(M4k7$lt)e|I%)*h^g7={^(OK;vpnUuCWmh!Rmu{vH9qogD0-N@P^wW^^ zY}Iinsh?~c2xK4g=CIe3P?U6xqUa3`-X!u`qfbuNSyI;?Hhl+@lnl`#Pm^Q1iOY-~$X9_)LjZgQu&rgyq zZp4|*sRG3VUz}>!hv?~ZDBq;_sX)yomX0fAlYxkOV=#+GEC0hwfY9=#}@(

zY)Q1?# zLcY53Q%4`9EYyDQiB&Kj6RB{Hk?l5V%lXAYcwdgY<V5H6^UF5lLwC=5 zqDsM}vJ+!rMW`Ykp`PVCXjt?%69rYTq?@R9=EdHC^5!z_MZ0^L_)=?kqRhZc?$t0+ z{;LiS=HuYk_DD2RjBciQbl71=cCLVT(VnEXlx;0vl9t5@c$rb@%?yimwH5RJj{}W1 z%AocFl537pI4NnF(KWtO!X9aM1Kj<8ArPJ=Nu{zT5tUvzr;&(r9b?_^f%%gc(e&9h z37533G0A=)K{u=qTdIP4Vua#wljgi+%h66-OC8)+Rk!SN?nGx5rN!Fh@Ve$i#3o0m zmZHA6s%rM60$GDwL5t!w@uQPc(-lw3vdXFdI2AkUKT9m(k4geFM3rM|9lx@>iu30JncpQ|g<&I|Qe}-5osZLafHJjN6+d^5ONRh-=a3Vi z;&Husu2qdz2u0yUn>8oqIk)ByZtLOqCds6#hHmR=DkLvOw8T`wi`P4h=WKxek!5u? z&sPL4o_;8aHNWx`j7{K^XX4&Q-NnkQEcFq2=aZ{}Ct(O!LlTd!Z5IvFv8 z<9ufE)RvW16^E0%mo}l7G?>=3ap_@}E83Q(%Ucr%yXzJPy31;b>20;q@WWpF1)3*>l=Vs(ncoH6sMD17sasCe6_3n>PRTbpbrYD?$3Ox2bT7k!FQ zs`W7RsAJzkGepTjR|Kp`vx9pZVBpCIZ9H%X9c zU<@wFJK2@tFtoPILD%PaHeKDK<#G5xE_&#=@Wn$l0sJE!x%d5){e%U1A_``9cdQKd z-1g_3R>$rx4L(MLb0V8(`}@5Hn^B9yiKp{)(VWJfV1P%1)~EwmBv5c*v!G?=Bw0iq zT58)Gsr1;C9{HZ3u%hH)f|Qlh;i2K>k9L&xH5NXwIn$Cj8J8h8ayui?sF}NYch+ax zv{R%GJ;I}bi{_a}{ogam z_4pi8X=>n9TmEK`J3^{~L?qD3N{?=&GsFJTQ?fCy3O5kxx=0{F6!DYbC1dMdY~Jn6YuMYx8Z1#))oM#|8zN>3n}f_Hz{ z0IhC8R@a~br)SCdnv(XpoUNBnBUM8bIM@RmsjPe(y~v{EwEbRmE_pB^4KLmSqy!vJPn!W<+;fESeKzf>^C?7m1)tZ^#O#1rY_}8OiwmvO(#v+} zHyvLC0CPMJK^K3v@AO~!$7JZScE>#ViTX5RAEE=*!_(!_W$9x1meG2s=RjyZy(5y_B#QYf_iQ9ef%`L98O40@VLSd1Ya5xf2qp;>#dbRHIY z+eRF>I&|{*b3;yiM$*u{QZ3&60IP=8vigo9#%%v*h3O{Y5$1&`U*EkUsMxO&1H&h* z#<8vCD#s&b%JyoQ9|&Rs_9bbBKb4s-iB>!4+1mssWB#lO9&qwzHc`rw!6HF(OvFDm zFt$ULF#6L%J3j>iWKL<2n?$Ul4;{RN{kx;OcOltSD5DXemZ}#LGJ!wH`b-VC{-Sky zOFf0?@3(maCn+JPU;b|QA6!h7y6c!dQ=Zz>#C)K9$WD`@QWWxz_wIK#pL{p>OM^j@H%zZ4=t6Rliq+yQ3oOB;gmSP zTXd0hR1pYly3idY(7BO?Wv7==Sjk6bhIdfRj0llvC0UVapOJhW46I_kyDG{H9DEiWEum6QoFa-Rb0GjxL7S&WakmpXOC#uHm`0WrA>LS9r0`f<1KFS zI7per>?QR=RyP;rNRNm#~x}mk% zelv=AjOQ0osprepaN+5;0Or)t-_}W!-l6?IqBTbxsRBgGnKmz$<2q??mZmI)_ZRt2 z17HX;9ucyPJUSE&qk^=CuRAGEP9EG9_K-F#CT$`RS~e>@OX5&mtk6z}4V#vc4-zvd$cvzjYrP*!~t^MaC zYn!wA9IIaTJ(CTrMMLwe+hIQY;k1Ym|8(~^#ah+Os%`&Kv??lvA|e!TD)@4G(^Kk@ zT#w#^;o>3-q7Pg7lii|?^&{T+ z?(0#GMu)i8UEZC8MYB92c@>In@hZyF8-aCq<@&T^|@eB4$iN{mU1HwLpfA zc|}}_7TG6l;wd`p@0BP`V$ra`P#u>g9vA}hM|N5ix>cI*8BD*5V>wSaw)`N8os)0# zZ6nDBg2q5?r8?1F532U-0irmmgdIC2UF$Yh;^Q#gDTOP=DKh-b$aPzp<&<3jX2{YX zzE#aj#t|mI=Z%wS91=G`7R`A^KHU8;LK)*owPRcf!mM2(s6=Q~4zW(jV)nXdXZs+O zoF=o}C}XL^1cy{VMV;~!twekZ)EP&m`Gw0YB82RMGA50L-$~ZK-^5s&=?pM+6oa}Q zv>tYu^`bV(A`XM)rTiME_6!gK7WgtoW4g3o&b*^LaHo%G6X9lr!Oae9un=i zoY^hQCMy*SqD}A~CU;zrJ6k$|uM@C*nZes4eBiP)-e<*8O&?#5s2pkLk2JrXMR0R3 z%ug5M9`H_Pii&_VN0S>e{vbLJXJIVLoSGoB(ZPRli5l zXrPTN6CoW@*x8x+DO0|xlTePN#+jI_ZrK_M~zLY5hl^%%a#6j|6z#XyG_=%%B zQF#q_@rO7w>FojZ|c3jm%(uz7S*gkBm$*nFo&MruupX zYJ0v_^}vevjv7`520I`;A*|5oRKw`i4@ba2-thZ$CVH+*|8y(rzNssu>R(4^Q zeU%_ODG%;wPTzT%#9Bvn<)mh4?V}SpySlXEhJ*UNxC-p{A@=CWDVX#`l673JohKtU z1v}nCi&#wN`%L@lYrG_9EkG!xNrGDO8L)4VlJn#QL8`V1yoA);rab6QN^kEV1oP>b zucV-=Snl}97STemO`6KL&3u1crqAYJeyCk?zfwSDC-l|X2-#hm)bn-iEZe}syU~9N zWa2?P_au`A?rJe8JrQxWN2o@iz@z9S>|P{+{1HCN_Tm=NU(T8m=X*OO)<*be+L7)|zy2T9&j0%02NcNPYn+|r|BocR zkC$#mBVl3FzD%ux&nqSWn>M*F-%bYZtVmFh8@ExKD4dM+9v9v7{ExYO_<1uaraQ+J zqX_o9sS2H*iMz=4oYbJ??<@1lHZ?yma@yzGYs{)0(sB!!e9%E$NSF7a)MyN9y2hV& z?9=cbWHu^MEMfdm9X20#d+7z!+O&ruJjp4j?UGNFRi1y|HFToK+0vbl6_x^h3Il)(%>{ zcBI~sr5bf7^fU+?3As*3%8t7mz_je!Z-paNHe~&KQKlj<(j)p7oH=d!j`giwmZCMwBc&44wAv#ow?O$&30PYZ00^|2T|QCM?;qT%cFp8)N&i6 z@l25*GO`*?lB4(6i9XzxcUD@~Lo}PuW{oduCq(O;+?6eyW4FulOE*{4s}_pa;wTQ^ z>(T6CY>8uf&|43vWeDk!y_b4(GlMRRiHr~5wZ zp&aVsYvuA$FD0`+Q;rq;?U|x~-R=T?d^q^zrHBMYpv=1b6ZvvU6*Strlg($w6Ey}E z;7POAE?Xl_ib9K88_gprp(G!+gICb!Vei zr5uTE5`KN+ZDvlvG3g*?bUIdA7G~B;8s!d!FX_usBE2HD2zs<4X(3ueo9bJA07Rz{ z(f7DAqhWEAtyBk9A z(n)G}2^`dRu{3MQL8@nS+E1x&{`hSAOgpAw#ruX=(xTy89l{ud~x5(A1`@V6r;aM%&q_SH>8b9`rIh8p7r~Lw_J%> z99QQ(t9gyx?Sen+Aq-q8NS3H4pseIq`cB4asNQ)5es6AVJ+mq$q(84EeAhu#je45Q z-{T+~3PF4ueN?12cEE~Crnj@6AEvSuRcT|tVZs@~ea)6s*fFl1{%K*@El&O3o_O%n z=yJa8f1TiFf0K6e+iNjCp53Ntrd)r!@(ol~WL%HI-$3G7`o4d+d|xo#Qh`8d{_>G|#t8vpGJsHCl#{dlGS zmiSuMfi?OvQjc0u{Iv69fhi35>gcMQJmA6D-gwV&Hg)*KF&3#x`PNoa^*v0%yN92g znA~LeruREz@wqHVoYd2T6m9r6iNm;;W&wDA7h3rh51Cx6xm9@Cexk7gpO(L_z7IqO zp>6Oc&w3PJroVFaMDIK+rk7p5S2ds|e0eRhw%@IC+Y8T+^yEVe0Vn58d4Qetq{;Rk zUmH>}tMBRWbaFpU%MRtc@TEI=-h_ilU&qr+rHpShqR-JDYRz|@#GA~Hlv%RxXMSLH z}p(-%RF3f)6L1A zwPCF@k6MRsPD;*x-Dg#a{oLy{+M#cTZ<^n}NFAkz8K=#qG$W^(v-(Dpv0g;G1U0Ey zFJHLw@veHSQNRc(mV~>|bqYJdf!ue2X8C0N}!W$!l)#EfW8=LdB?^$LU`~ zQqz>+Q{d0@%P2>HrFz`7tEkE!KXD!GqNFyqRV-M`7w+Oc$WMLWaH;TGiwl=M!Yxq~t1(G@UGnHj4IY(BS5_E6@ZQ0}uYg z0ubOzCp_QUfqPI6jEeI{SlAu^3-gJ9PwdN5{4jH9YmM#{DcuhVx}%H^K}qc@z3E~W z@mp!q;EQE#L(jT7Zm=G*-Iwp#U@mt?U+UD~vW{Ye8@6ud9-y=-+6q8|JCa4A!nPU+ zOtNq3xjLvzke&0FndJ|11{&)26v&=C`a3LS+rB$V6T~#k{zF2TQc7+>R*OyJVNTyY z`$o&eJZgnVr->b!EnHgx@2hhD9LmrA+rpYs5vYkL>m3?f0deQCkfuImIicl2DQYPS zy|!OUi_K@*_d)H8?sqHYkOwkembWy&4TUT*mo&X?3#7!Gvsa#F-);U`SkJ+Jt7&!L z72$%B)poIcx<6qrepAF?iq7`wx!1w-vGVr&K}3vIVql!R{%Bndd3S03@pPstqo zBtW2yUW3D7#vf&)Ma!G*fd=M=(+Kvm`v`>{#b0=rz`b$HuH*%`B{d3(6#0oe_mc>4G?c<2Sk3eHJyly^x$Vy@XP<~G~i)_%hb z!M4MuP;^{05bU_#zKEWThQfAnAN&gTcTrVRL2hYEuLB#8JPfnGIh8JcM#PGMm4WL} zjhw8eNA&ps;{^I}4RkXlKhB$vperg0r}Ep^tl#FRq9!JQlKOQ54Zr@8I$}DDM_1kc zCA+|~cvdXs)pLot&-VB8Mi_0dACJeLzU)!OFD z|6ISS`BKx@VH%i%g=2-*G2^;~jLKr*WXj*-yB*3PJ8Q9gDNwi$p)D-DU#d944c_Xl z!!*nU)*GDW-m7Kt^){wBA=9D>K@Y)u<9ol4=7=GH%Zdr;_v)?FEdUZgW*?i88e*Fu z%_A|5Ameli<$23$EkZJ>W;?~-ynq>&CAE{yEZ#ALHdb#LQ8@j`Xjd$Tb`kF%J=IF* zq2xU6Wf_!=pB8NFe|v4%y+V?43TZsMAQ115cT>HTH=8l@gZS4+K1CR|{PK%A)ieBy zs&8ZD`wOEYzD}Wj!OECeZzKD&;`47anw&8jIj7`$8_BNymeT2iFylavhTbqZ6EKiH zR%1o(*x5Kp3*K{7mjAR=1Rt?0eVv8O5Y`ebHuMv?^aZ z=A=ZEg`1)P4hA(5XZ-z3r;XqYh^oBGc-Qy4{9Zl7AV|qgKr>hGot4f>nu$FE_IC zy8ZM83$dB9-W}IkPl5i!w=gLt!oW@0>?z3rAGgNKSZG$kTW73dqXB1jqv64OcGE93 z&sGWZ_P`D?CMC}61PQTs zDk5Eof@GIGloT_tWzMUlbv$|0;Z369Skc3!8GKa1_USpKredKX>HWR8UlOS&3%~W0f)2qu0v?o2*AmK@%jt38gqp=^5`y{H#HQYR#5llQ!w&2#YPYY zJ(kU#2Gk4sq@1Fk1z4>lE#l_G@e`_zrLlsrnO6Es~=;t8J_ks z)E4bl-Q4F`yJh=QCFy`#GKA_6wZth;;jCbMwvm+kAXh*%Pl4%T<$HOph=T#mS*2wv znBlyeB~5((T`|ps^4H=|m1PJM#st(kcl>t#16(K$q`x5uIzzmXIgNEkkoTr8Kk*7M z5|duXY~H+|D<72|)2!3q#rWY#X zrVH40B76E*W}e63`jd$_4)wbw_qY>Jsbzn4gi8& zk$94^fFD@RvrdAK=d@K2MBb{U!EC-M+doJKOB6z_S|edj6=71XZAxou7Fv;c))+)! z5P$Nby_!?G#^*@h&V&j^u;gfqLVL!mJ{ zNwg-oq&ouI4~z>(ojjMj4&rN?LFa8~Ug|uwFH+7?^967aTOKgUUjweV&*(fpQ)o?f zf7zuBy%JLkpib16KTg#h<=gQhvY`rfaj(t4ROhUx;ff%mfjz{Uj}l1h4^bb5XOAF_ z6MxbOZS}Sy<8uzgmTAb7{2>1EqAWLD3ubt3*JZoUdlZ`4;dw=m?LA$fXleaLt%NKD z#-ZI4w-Qw}vuj2)a<0ZL-m}u^5*g^zw&9U4VPO?Wi?=BGh6Oqn5YXXnUle3@)+(C| zA~l`+pe|pQCMD){k|T<7Lo4=OORmD?`x-GFJ+;xTVQE#Zs@eKG+?eQ7jt3(+hoZhi zK`30USUA$BE$G+cIV@BGp@w|cJcfB|QTIN%t4TccXWUYv&f~JpAIfz4kzjB}I|s?3 zHIU3V%jcp7N44!4i{HN!6VYFtIDPZODINJE{}tPh=3rKAyrK>R(ik6aKY6;Fmk(r0MXK7;K{gOHUT^HB~GNWfRQ=$oUW^`cCNKl>BsQ$it~ zvPR9`fmK?OTyOQr9^m4EIT9MbVbY@a8$Ji6*vv{p4fK}sDUMefB=`{$QE|9X5zHXO z$ZBBgEdb7#!>uIV^-W6+2r;1K=%jYqW2|n`rz-76;V`$-n%wq1|JwLHs~lG7*f;R7 zv}78o?T+~bsX}3?MJ3~{ys2jk`(4b_cqXS7s#+iWbE@JW925m00?7mj*}?srblBi`bb^9H42$wg(AN+0HcIh6 zSmySl+VChy#x?gTjeA!#J-Z6{`8KR8qM*Q=kyw?N_A=~)nI`uk>hK|sU%wmOsbpz2 zJQ`s1=0d&SLa-jT&1iWZo7=fVXKQv|PQ^1d=1F<}w*lPgy{$ii?pPuVFDoe%=Q=dN z-m^a3E>L!u-HYa~XeGJZqM@jwCnz;dQs4$rkSx@mNd43};fg@>5nkp1#m z4+R~^D=@A}h)Z32*$E>Pv}!mf>86&mvpAhQCFV7~YCTn}0*iP!2>`f|ypf`ACeU>_ z_H@|FuOtJG{?O_lYi;a=AI#!5mV18l)HmqYu=<@g*dCvq_1yQ3yF7Q+eM^Fl z{@eTeH*4FQdk^~mG)U|JzR~#u1fP{gVrX{WD0op8|wu=vq*cRNcY~ztei^}laAsV%cxJ%S5-^=j!d585;DF#QE z&}Y%s13!8GaE-nMS;e4i_ZbOc->CbJmBqBbB zlwVWO7~EA#sE^Lx<%m7JnQKb&T;b^7K8$%!e%j>=Oj;ubg>DJH%NMC>%D!I)KbMePI9Jtv3FTLBAf zEH>{che0BX+G>g>?LHVr6YvPNX*R0$RYNcDF6E3mvTf=0kYav53c+tv)^xGZDReSZ zQ-jYK-gjk?+ir+bYOnEh7uQItf2I4)%In0SXh|R7Ha$cAzx!`nRn8Is`QXgRSE4qB_ufDy+)G4y1Ig*Y>${xv)@E} zqe?v?UnJABrLr2Gs`u{3u|+cpGdo%h*E-UJv|>>bL-Hl%%isv|aB=YvKmJbOCgXF3 ziV^pSQCHPp^2B*$40^&N4jNCH(wyM7%}Qv_k;p_9Hx2^SN%C z(`kUitmNzd2j@ujRj0b+74ziGyVyxrO+rdYM4&sz>b$O!qI1*oQ8|U2v=W3`CkZDh z(F3F56rNISNdusuM8z+aT4H=u7rVFjhp(R_X~=2i{oRT_-`>^j zS}0=$J&Lt6_{V1gQRZv=o#5!JXjiG)sq`2h!g1vC>|5C;CEXM)D)woYSIN5IZnlIL zMV?cS0Q%odI&E@-vxPlviKsA_{jryptZ4g|(yn#nI*Q+U6>3g?bdZM|0$_;h8OhmU zEvN)OrRzg_jch(fbOh7<0mktt8SxE1Fk4QKa~f~aGY#c9BgalxKjMYU#$}J1ACxFQ zhaR#(Z2R&xOXGHhS3+oGAKezv2^P1{82A22towCDRC2mKn1hYN>Sko+^ky;__2!(B zwmn=?Ag?DYaaXb+^vtMGjw+%b9eoyO{R@y7=nr}nQ}he_eYce5HMYliRIMvBA|_+M zf6zTXw{B%&QWDXeId!Q{xHXEwOsnJp`mrvYvh&s@9`FJ{O z)2UbVOoo3VmFE6Y95F7pbt8yDJ3k+txI?)|v}^N;XZsLfTgTWxgS6iXZ7(THE!B_~ zIJ|YrWcU@yUS#9zf^Vf~(A7@z4Sls1bRSP=Z{CWeb=UIM--c5pqNKIA#t!1v&9r@S zl>E9fz}N`YU&rW0*w`+?_K@+dgx@TWId!!$E6C`%Vxy(4MR9Vx(Zpej6dNu$V4Pkv zeTdJ+wd(a>%)M1mT;bd8NrHQDf+n~(?vmipxVyW%J0Z9PhsNFAHF$7$*T&s-`v0Bz zPSrUzH>c)e=C-@Ks%y);-@Tr-e#<@E7o{O?8lczB+V_BtZl9}gXonoK?8DNwL3fAV z>P{yIbLlX8XD2`E^Gw{F$nY)TF9+M|9Hv;i5@vtP5gTe}A5dXT$^RxPsE$($`QHI@ zim9j6A0uHYqq-L6s@kTU9FwT(AxvKPKyDFxt%xqIs$|?3cymA!y#7(4FI`4YjnDnW zu+ySjc|}~3;rra(&I~Vj^qIG8kz>ctYI8kTkj!&HnFB71AC} z$n`Vey{atO4G~_j+#<$9!0F0318s5tZ(~04`t`(fL|m#m9z-UY=0}gwCiwMsbV^7~ zF!T>beW$d9)_v7MAC@T@a9z(%O8*oTqi&>|oykM$Tey6=Dmjk3}5wymW2B) z5cI}gvF2(}Ws*Znm)_LRZGDaF#%@S0FdKE7boBB9El69e{G~Zbq zEliWz&)PFAa+WdV&io5N%7jDLVle%~D9!7@Nk^%y638TWk3 zD1n!8GGwgyTyg4Z6GvXGiCn?9s(`Tq2Lt!bx5KK0{kZX%SB@BLUT@z8KX6VzvG@nkO z2RY#(xO- zD<1ZPsx6!bU;Sf>OiLJlzB(51D=3nb*T_}`ZBW7yZU~0=UP_eF3Jn&ma2b@T9|+;k zo(5k2(54x)K!>Te$z{IXxl2P_j%rfKfn*Xn$A;`pnBvP;cm*&zS;%?JFfs*ihxei_ zr*)K#&*s{-H%=-mE!Z(Jw4I&VcJoC-%aD^$?0<~zI5@HkOqr0(u4d)lsz;18RD>n& zA)paAR<_SXjwN<1;(p06>xna=fj~RJoC(z+kJkH#EY1&NZn@o!esNt zct({>L95CnYCI-H#=;?JF%gG%P`XhHIR=YJ33YO&S0c@!JHoFm^+)Sr>QrLwoads( z?YX4rKponFbsIVUJ2-~2(Ms`WA^|Mklu@ViKN)qR{Nwf3?%vqy&zgTqErI=hB|mcU zqhWj^^2{*e_lN2G-ikMBJ&4rQafWHD=x_xa9d&8$_Xo6Aw)4iQ;>m>fBVRT9K)N1j zn#z!Y`cu*qdVhoWH~uADB(`Pf9%OBrZ{M|(zKFVJTBQB~veq+VpA&FUG05Qm&78L; zT&doz-wkn|NyyV5(US7+F51YNsDH60C|+;wnQ?W~?_y!J-G_(;z~84AtvUsZQ?ai# z9%O<@R#U5dwfq|Z!XW`=5lu^t31mycIsHtV`9+d-`=4B_3Hq{Iv+|1?URIIEha`j+ z0-lM|(ow5%&Ou!S@r)PPw}Cxb?%HsRO?}S#P2f;`DaBh(KrE#cB!BAey&SSrZnktZ((>X&`Lyf93 zr~C&-8Oi>#2$(RlIhj`3S@W*9q96s0A=j(y=BCr14WG06Q9Bq*-tOs*w^mIT9#xCnXTbOQ3NoPd|NY5d`TWmwpnsIF`2Frl<%i)L+zoGr zm}PfF?P7;+Q7wPaOrS&$S8~?i$Lz|--NWIp9*Bnm!Zf;||3AVRIS8Ifi<%tRYQJvk zyKQXFx<0NIyAiLEh;ugMpDk^oBva%Ye9)1~-gJMYJdmPN7&YN^bUk*juD*1=AAIsY z!eH*ftfF`yQ0MIIaO|s>iqvR+Q?t_^SZw~_d#brue>rc!dlcNdm9{z1dqd4=;Ll4D z^jqwKPPQMtd^uGJ_ZksWd^*3K;RZSw>UbHIf-BUpTNXTF;IL<0l21~&s0MLK;`qYc08-nR8>}%Sl6M7vl1qH)ioF0 zn&K4}gKMTw1oUFv)zHOwViiEWR!RO%Pt5rg=}3SjgQ$(0-0#Fl zLqO)^F7OuF$j>=Xf|r+$8NHp%fa!)G9e)cx7?gst#o%XBMr7|v?LYbf@^2zeDKM?T znFVAybxsJa6rY?u(V6G_S_FuJ+(VKdy@^SU9Te?f<$-rx3 znkc?Gy*SjVY+=})AJY&5>*-BYB0dh5<##V)SO|}vhwWZ| zUnS=d--s9|G==fC;Ot!N`JJ=cDAC-R0n)E9mLdhC+Pk`bz;2u|($kcjcN^O=c{my$ zcXu31Nr<;{<=7qOJ=TQ<tJy z*OQBhhRr6+2xtArl$2|x4T6^ud9WQKq4864OoV$3384I><>)!V^*BaKSv~&aBhRnJ zg^$}2_)PY>UDdxJXRqzd?S)VK-PHKF2&-~epJ{&$$514o_IN4gOxsU=A!wKrJ-eS4 z4&_p$_w7ZR!Rk5QV^sK9tGs+3myA0#GPYx)`_ZR-!-~aqF-5i99^Djwg)*mNuRcbb z!n3|HPtEo!K@_E1_Ix4*F9J|5s}|XHBRq{Gz!ZTfQpeoD@l#4u8(_&%3Igby4+N7B z^iQ!nAO4Y_UX~|qbJ}(iBDwT{qKnxl{dwW4Aa>zu@b&Pbgsyu{sDpX~8pq>9qlznE z@T=YW;R5`uOK;zv=~mHkFBYY`HlIsTn%=p2>}j%s*5bnS^zurca~JVFNom)!rpTg# zP(!N2^FX;W-hks-go5662wbKpRwunMJXJh^)G~eb6NHuI;LeuXCUPIuA(&RTOp_tj z{ms3M7@GeabBk`$$$D>mhSNA@qfR9%qL)Qpo2-SdDOEx*%4hP4)fD{B_XvIJ%4`~l z9#FUlxf?t#u`@3fJWKiB&eC6ekYirfF%S0*6-0ZbpU7A`>FaOsZpJO!3`cY^m}8;g zUfw&J;5|Ingw&1liI!0CyNv^0bU$a%=R0257XN+_xon{fV&i#pyQOXVoTidRzX^@! zp}#iYzWN-{CF;FIw{3XMs}JVyV`B{uXUy6MOpZuVdzAr^Hjd7?6yIricre&#hsN!V zxlMNXz4#Sg9j=QIYXUvI$M zxdUsyc9YR&xB5JLF-QJrcFNj|srZnrM-2(Yi{#<9zJIh!wNn2@YXCb0e&3cnm)<7# zwr0Fs-}WQP$aWfnl5dBZ0v_;jr4yFm@YvfIxPN<>)^s_{IC;-2u0$-_`+3isN}#0b zn=8P^A@CsDCUqtLe#_-(v1aGXJfMcSG2%_yvd;K(R$(1KImz?Vgev0qrow{y2O_fb zypy`JZx_oq0n>-+#MP1B&CJs@jl>sSjH2HgJ`QMbuTUO)7jA{w^vjqIrngcMoFF<3 z`oMo{0q|1HqvHIJNi za#q`3XXG8_T4I+`jUfyR8|$!*{CF9ic&{#|BcR588(kP12hDt#z9H52;l7sZSO0Ba z^{SXnhmK<1r%g6F_z*-?=epJCO1pQ7m5mahsSi?e>#n6O>CU|Y_ybmZcl<{E-Pun* zzH6_%+YEIPhh5s*pC^9En_EvVZtm!$a*X<$o(H17C&aG3=P^GN3^>ajH(eExw3~Xu z6t9_`&u{*f8inkPCvm9@`{5F@iHBR4j78q?MlPDu7-QXF>V6GZQyEf9jCvPsdcIsl zN_e!9qVRLmUfjW`jZSwY=;ES0g4j;7N(*W9CkLimx5bL=bSz34yK>qhW7b$6w3vk^ zc`hJ<4eA<+pi0P-y7vfkn;~eZ?oLOs1m5QM0TfM5`iCdK=CcW=ATat3%JWz8OJFKv zjHajprw$~A)}|$e@~F**oc|9*a830b20vEMEj%W$Zq?{|NOS(GztEAiY&nf7L^_tI z&Xt0v4l(@6g>u>h|0cZ(G``J2NIIyrqj+#vtxLqp-DOY0%arEnj^9eX4DEmKu^7cb zCn+m?9n}5qO3!g}QobsyC;mA}AtsHeyqp!Z5Kz&S!_vsX|UZy>Zb zQ*POgg@Z@;?sm72xC=`IEw#LN*|^dpf@8){buH$o+*uZ;hRt z>hMSj`jg>J*kIzLy^-Q0nq<&tOVc69;BWm)w+2zah}q_p+nj15*IGmLuw=i@u&(RF zjwyp#62lUFxA6jJa#CXRTHYb6^0l6R0Z*d4n)Bj3^z^c4>0|5Kn|_>fW_qoRcUx%1 zmtvdIh}@S2pZxl&y)|=!69s=c-teOBiSThrO+wtgzM3?9i+OP(5aL=$t)Y%`UlJb% zaNp3G@7S)jgR&w0-!gG&wF2f9qU7yYcNsdd<>d&b`1Ip12L#L!d0rdWjV#@l^}5fC zn@N@>5bq@IFa|_D5O?8V`J{s_;am(ptHG|Cl< z4QUgbRd8&oNXXdguRRberWlY4{_MfvQo7Kvn4IF8K3JHxk5|j&xleZGv)J0AqU9sS z#=h>P|9=aii&LG`o{lmPn7Joxr6W36f$0eP3iq-Au3g)KX7IkRrTkyKHQq z+r|lHJteyES0xx6jm+X}iVH4H_`>0Tg+ZUEtyjou@LPt}wlRE2R!0MXzLx7vHvAt7 zwn73~4rDfc$wSY4R*|vC_ILOf+n@56F9QlSm%}}PSJUoowy$E=vs?$)SFhyyiRWBj z^~ZIs=_dqc#L>Oadux-nQH0YQ(439$PjeD0Y@IstD{^u|_N1|9lrV#QpS`K@M}7HS zjXzg+mTZMB?Cb(CCL2>}ku~G}VJ0_utM8`p*IS*{tII4I&pFwGc4k73TVQXj}HDkipnp6A>gII^66EL85MKh16FnfX$+Yy?{B z$L~{$3#A$NFZNvVS;#yLnS(Pr3<}~<=3RALHsr6!mG!09;^hvA?IHOGT!2mA zh#S}XXp+spDW+;iS2=9wjxs>mI@4g@Z))myk zBQ`g(1ax?vhK*W6QDv81&=gwAO;SBkWYU}tQ{`hkQdksX>eRiSk)FqMYi41b^^Lvt@Nv^%iBd;JgSYlGtzgsLTh= zb8)v*$BkO8r`@?0bTdy)T61JF?_6yO5+F7&y^8aOnuF&T2#73w9#gjkUc+`zwsFTPf3WgL9I+|*m;)MDX;DLM!HSB41v&hy&VfKS zfxFCf1m8-eY0A9U%HuN<=k`|=Wv;5Gr@d$~tzA>1A30`kIvN+FW}yn3 z(NQF{F9o=Nw~as~v1Tl^r=ho>%DpL9Ow?6ael+~kvu25wTR&donkk@kDTeb1_yq<}o|=B}cXv#r*c;*Px=xXblU&=+Tf59(}2~=hUgXXgOl&kj|Ab z+U9c18_Sizaa|QoS$4duPX;AudB>{gzg6T~TdXyuI{1A-C!OsaNN7*NBB~Gb*VfeY z!EpurSEhY746+CaQPM_wQY^W})-5ypa9Y0}HdvO85VGnor2}G4Y*clW8V)6`j-96B zcavd}g?rGVNO%+bme#AfPl}u}2*3OoOUtf#SpUV+eP3VXzQvy!{er0z)#2s5fht1S4{O3m))iCp7b!2H)x6{<&V=cmEo0?A)EJ$y<`Vk0 zBO3vAUdiR1ePlq{>c4H_u|dO>=(^jI=L?vZ=UOvMep01bq3Kbnx62v>2q|oACYSUG z$17uL=K%K6fcR`&K#C9-=&Ox`D@=D&++eOc0eQ3Y%q_><=@p)8o8;iX(4ZQt^6qTJ zx$F4~AYux$R;Odqw^EUBv;OTIM24w0f)}nB+bYenNt9)q)s})ru)*TD%x1%Hdd{)= z$M?iK3MRHC!x9i?9k?Z5}4`;AbT?wQB?MU`$-ib;CQ0g_m&EqiKwbsAzc}GE~ zax?muPbGC07bZmg5rcQW6$IWbSG_%`Io zaCt@tM}Pg9*ryCXMG|9Pq)HN7dFetTkSSVqvm$s8!_92dR)0XR$F$3$(LpR>Ebd31 zEd9!x`G=#ei6y$7Ya@#mEt-&-M1tH3ae2KYS1(LMvf)IS{MB|blD&ZKwSj{gt8y*^ zDYEJTWFyL;yl&;$-SBh9G2u!??o(_K6oAo{09$`}WHec%-8zYmAdni|b6 z@88LuX3MS5ewriL@dug;S)8A9$8%!m+E+8{0LS#~qQ%F`|ECl>7rUT|#ZqjHc%nPW zPfI{nd)@;2Qi&$RhJ|9H-m-y7cnSk{Xov%&E~9{+;lW$MN=-tz+IJ;;=Z1gpq@}adh^+3&G7q5W;pYn$7?+zJ{ zGkBfJc6_IVM1Lmx8C@nOG(`g-9^J^Ft0s3QFbukcc)bJWSUdZ}Qn95gk> zr4SW5U&BDjf;n{H4i!l^zQalU#Q$Mw%BzVqg`#jUmgvg9>Fad`;(c4>b4Gh9wW${4{~O6XO)g+ot{P^Wv; zrp=-&v3{fR?VPZURZ!~`*bEYbfuCxYJ8!Wh!Dyp%bo^_Bi-8IHg9}7=c}9bE)h=#! z7{DO=oIP-j94-rtJczzIK0lcoyL5@|*_A4Y(q$A#<)+=I9oAuTuQD+m(HagLeEMy6 z4)9?kPsG=rv-zL|2A2Q;YC2~M?xmR@yx1Ig^PJsb|1fO#B`7LU?o4YH0>75Z*GV=| z)XMlyWZ00G^R zC;m`S4qQ`v`_;c1?_@`$f)nA<{Cjk8wKQY+A~|=}DfZb?CP88kjBQCb6z*^nA&td_ zqrFLmMZe%Nh(51>l8HolBwXvgnh3sOI4ZkMbXhcyENUxh@ZHf0a7xvZwqn50q~@$) zer8&HGB#&=nEcjnHv1o)TNBj4{xlCG=-X3wJBK@Oh(0FJ;KZ|?keuG|rorpB`eFt|GNQ|P`3%RI*FIev-?_fPSS2b*~{Y=wTWIq&WYo$wwm42GhN%yxW9RU;J*p6Gf|`0<9eJX7 z6iH$v^0zu_Xs+1O`ifY8It?${2`$7d?7J>ufr6fbyS*(b3$iI@I{&Q&6t)U(9;sp{ zv8N9{i#|LPmJ#;Vv7%>*Qa?|in)=>l*VG)P93{u>1noA>p&ogsqBiE9$u1$?~R61)vfW1FHsrfKzF;nlVdcFWaByS>lx6o+$=)12!dft-3?}9x# ziCj;(kJzD5_FDrQ!w>Px{Xw2OBSsy&Z%3h6-!VRsyCZ$tx&V=G=)*Zvj_!VDuxgyJ zCf`-b3AnQaTc2VGDN(oVn?picUKj1BAcK*CnS3?xr0Fw{1UpJ3m{IOLZ`koFB`5^?}YylTJzD zp46Wn!It2S=E90k`hSvl)&>V2P2XDMk%=dS$_w4A(i-{@thWy~&0))9&};>Ns5el6 zA5Jc>$S%0!26 zlag&3X zE>`gb6HmiRu;hZcUYU;G;3|UpBl|~FZq?y!J5Hs47P)RMZ`69F3tBtr$1PZ>?_Dd2+)p3A7L221I?=^)9`AK~J_2NS;r4PxE>M2EMJXBR*1Los;RUhysC2=&I zccXs$2w3R6jM~gz<&)jr=!3+m-%r*U6$KAs(qn~MU;Vv5uJJ^=!-bQXyvf4MyOf^o zrX`Sw?=(*9N5h2BHP0ua-11Rhmh*~t@_G*-LfSy7`^3EK^83+nlH>ORCC}OW@zWe| z6Zkd0^63zQd+?dDd@4>Y!1>;l{PEa1px&6~v4 z{XV=_><79j57F1>@q4t8GT!7u-s^td?79byM_du!Iu!!WH6~ANy9m52*(=pLd$Ovj|37t_F%#@r2YL z-oXC%A5Ycy*+|7lZIFbZa@qMWQJBNY)5q~uFw+d-shsP%2IS%z;oMTZHjb);gt4Lk zMVob_NP5^&50DgWKKiXTUF!{nk4*K9rp7x9Fo@>v34$EQ>eL>YpV=`3{#APV_fC$o zrlRYSw`XM?YG>lJC79Q^{57}PI)9CsrT@wQWKGTvW=w;QnwAg|(2cYauQnkjyZx!*n zm>PvVMz0FO)qnLKPmG!b@7(et4pV1dV71m!bk;qG}|5!iuW;S`2` zO(*AEWS4(cykoxk7Z3cyl=z>HmDml<&z=eup78;+y8C}((k+8YElv_>-EOWH{%gfg z|0L3wKlQq#Z@%Elq}x~f^>$4L8Kz52XEI4c#!6tHv^K0|y)w6WT-Vo|Ye1jkyNv*y z`C;F}X3msRylW{v-m@dmFApiJwy0I-QaJ%vgV!uEWmT1*aK$DSI6{Y-*(+$ zcMHTr^Mr}95x8>v?^hdNA=yVPj@bf)1R zy-~zRCEVr`u)iexoi)*Ieh z+(os;)Ww+*A`Jp?ogEfVOJPmLVdY@N`kRT~`m$6I5vKAD4qGuJ^-S}6MGL$v=q*8A z%EsCR!4LUMNo+j=2|Ff%jE1HG-0l;$hsmuU(U3&|s?wB4U_KO4`KrQWV# zgU{U#PQdcAr=Qd%D63puvD3TNGALf0zc5zBuI|_^D$f35cPKI?*k&)}O9vqz|H5A{j|cy0a!Af9ACLh01@LVN?5z7hZM_HhI{XKL zahcNd|9Rci|Bl_zl73HrLS9VC&V0p!RA@PT1gQT%S7iwek`VlSW!ZOa#C@|CNMJIG zB{2xy_Z9xwNbQdL+kr;z@b(o)*Z!?XLmveypqo}NE?$v)8`Q*r#mo!^n!#Nz3FuL4D zs^*=|7{zCC9gCd|&q!N}SM|J5h zg#7qvSAe!QA$nid48>Slxqk0vecV9*X(DeY^GY6pTTN9t-zttgu40&e3Fjg`GV1UA zON6|wHWMzhlxx0{H5hgz{(*Qn#$`Y=tRw>~RN3@18kTBL7HOyDM^D3-gVY(t``J(~ zrWCmARJHo$h}=_aJ>Q4UAdZB&enXpb8`d0jNaxV)rB@WW7S(?4hn8y6I4``&mlv<6 zmltJ|Kz_7YyT+9?qAcd)C+NcB=1_s47IY||i>rgrLn|sO6%F~C-LC^G%LRx-;+XZk zUwoNWy5g^2tGNWS({+On>J7$|=cuO|vk*2rDD?ucrIgz&MUEk`!6Gn+CQA|b(>@0# z=U^&8J`!tY0AFyuk~yM(1clxz~C3@zN6vrKhhy9TRFba`$K09_TQosBbQG9 znks$DoS0Ip{yyzb+*V{odA9dU6!?Tfra)#di=A1B{H{=(Dx zYmx#KYf>dIxss|5EY5~1Eo-aDO^Dn#6fU3?HE5{!a@KB~*)o6^AIWGlwWDLSBXl5G zRa4s#2W-VX|9kwR2^WCq!E-827b!M86{4 zDQRVD=-!b`w~Me>PA>nf>D2k(S^%x!v2#AQHl$(}6wd66s6QI01X@R@sr8~%!8|4~3DnpIFw62FVF zquuJ_(3iZY@;M$;sTPK1Pw%jvSeaz_n8b;1y`01VRh>$J-?XRuGE0t<3denGFEz=S zXis{&nF*CdlT|dz*A@3x@iP@n3aPtjIc-nHQtRHMYX#Ry>d@KB+A=_XONDV9MJ7@o zhFJLFU=5`sXa^e7%$IvD`3?s^AS$0yl3!10Y<`t!j^=~P= znyK&iqrXJs#d32J#9{kad2UJb=3n7hKK&%+L|4m`I>A>-_O}ZYuf^$^g)d97=ZeDz zZP)O=;hRS<{N(fQ3K+sL@z0Sw7qNp*Kybmew_vd6MweN#CDQFgPqT-TcO>nJzBnzz4&vF7V4RvLk6#~Uin(HcYRA}oFc?g;d3=~NQX;C6G%+-sWc_>zuLsVnsW5bXK z#NxuX4S$@(oNCjH6Qela?Y~#adiMaWOUUm_;<&RgTDF$xrv{U?A4%>QP6`Ts#F z<^R4*_^*b{zp=FvNEm7EQk9(>YOQaMaQC|V?Pe4&SF+S6s+o-;Cn6+)0#SeO%vBcW zSIR>%AN@}bSRWDX7p$wxlP&}ZAJ>UV|3+OVtM`7Ra0GEy>XVR=Z84u62u8QH);-FY zlHRWe|NKR57ti(|Zu&FPk~;>~_iw2(d_6rR*iA03;-9B=HT8;#`=QW%9wd4n5UV8- zMH>7z!#g}ay)D~uYI}D4rQVC`8kxD?#=gIp2rz@M8lLdfv+Bp5$AX)3ANbdEA3Q0E zW`qvetY|J+6Xu^aj-;0s?u82Y^T)z$eY))Z9QxhMO`Wrn{9w@dh-G^km?!$taXe7* zR=g5LZkd|bPmXi%VB*5eu9RT^9f0ZND6Tk0Oq*dDGaR=Un;J zb-R7^3eNkTVc=%)N*}seZe>%bP8pq@h}+d9-S3vvWTX5}N(Y!Oqy=OSYrk=O2bI45 zJLQT!i(_SBxO{p5DGpmG_GL{-7E^{s2suJ#TJKYmn8Wl*nO0|oBEu*80O#e`;aXY6 z3CjLBLVbVkcgLs5P=&}uFsmraq-}P~mJvd4_Bda-QXVM?8;n*M`hr#s7su9O1-_a| zFP{(=w0-Y-#@dL|aX{QM)Z~ANyz3GDcoUIhis_N>;_B^%yFfwH{?yd1U7LqRQ#N$s@hWXdR3cdK=g0 zDdc!&R4n>5_yo*qBBYk2b6<(vZ0N{|qvH0sHa7@m4a{tfjkA9ckYws9Fc=hHJpLO` zlKIljh3=f(vxi{pPGr8Cy{T5=N=%;IeSbg8-zKKU__TSWPRw(bd$t=%kq4olX3?2w zxsDWxH_>Pjbf2i?7@Um4gWz;R`(3ED$AABn`9fy!?vD~E`Pc2Pecgr`P3y&_pdH8r zpria$pC8kg`dQ`RQ|LCNWhg&5Y@A28Meh)DoUnu~D-+AH22wOT_cycb51Jx z2L6}IeHX1~K8Y_`b>_`-AjJw-8AHwk#L(byxrQ)Hh57Q+U+h|SW?+_2Y{tMV@V+cz zb{y8uFYc?MbvizMTvG8^ZdKJnx41B0U#sBt-c&0JDD++tNabXcLVvMD)z3;+k9$m1 z)#Xw0d^MAdnJHs+Y$gn5eBq~O3-*-L<38FybpwB{+K*Us0so0H@GbVMcT_$YobTo> zXvnvGJSF(^Vx3y`x?cj>;Y#y}6OJWP{5y!%iAduQt$Vz?Ehf)rJ@F&htv%aqPa)0h zkp9 z)}ExbVsD5kiMkZ@qZsaJCDGTSf)7HUW=!`>t!8$p$~F)bz&!Phw!LliS6JOx0(baD zJPL6%EKW_JOEewXiiyl0WjYFbxzxcjiD6N6tp;Q)-LF8`HbDP9xLIQBGk zNR$u=%CxI_?k#66qGvvC(g-e7jLZn=>1wjRDnrg{`eK3`HBQ@SGPhx7Lq z2X+S5VebWJQK4N>sP;FSlp}&5NKh1HX7Qq9>M{}3^U3L;zGvmFG|xM(sxA6lD{5Sv zcz{Cma~rSHdWEnlPZV89Ixd0mHzHd=X@$+XGxykv8+7J$ne9woB!Z>I?6j-_S!wo& z%_J_%qv4VFyTZMcK&MCR`_mtoUQHrhKxx^tN{EiYv{UO|_$4>QxHxVqD?Sv30@sB~ z;uW}yPa(zr6~RRl3lX2LaKjd!Cv!PxK6tz`bncfEK*2uN3=(c+zKCsCW|WQu;Kb`dF~)1?B>3 zA$=pA^gLfOdk3$LQ&bBKTqn#R=pb}Ab+x8K$^2i};F&W_lZc`s=@6J~0jyw&g=Luu z6ejP#!B>}h!#P!ovibo$+kE$X6k)%a%21jvT+~B;1}{jW!vu;-dv~H_(I|<=66H^L zSVJ-DPfM5?G$b?7S0&7%I&c4SKXC?Lq zzU?l8JOWq8L(=7j9CXi%`V8mqrhEHuahP8ctBg)SNml5e5M;i^!|{>_##0}TGPg0> zHEigw!VgBC1&l0X@qgN^vL&%n2WhREyDUj%1~O@X=^AScGrlB^jh}k6Cq?`)P;QuB zr)WU8csXQ6luC`eC-qT042l6yeM+gF;mPEcH-xa)P&l_y^p;dfi3K%O^zu2Q*ruk{ zKmL4GW{v=Fj0NKxMT}2HM)lBxBJr6SRj=i%3}+@!Vb~fy zHTTS&1XG0so;!B4iIMMxP_~yt8CaT#SIkn@>yhB+W0kWjEgUvWzOHI{U71V>Q>y9( zJ*HyXD7ufv%MO${%*@c(_q;Qs1GY@l+WKjg6X08iN#uG-vn6 zp9)1;*8d4rzFCqoAU!3|1QsUll8O<+jmyGnUdYY2{j^oBB|A3=`sQVRG=w-#fLU2U zA#81V{kwV1%V0;=evty7*rvEg%~cjlD+l9Y{@xK;JhZm9hPF2+kik>^f08t*lzDbwSCQ=Yib;Qhw@T0wR^nrKaKqZt_hO zRdv0}aA}-KE?j?1FkJ$g=$u~xyAgiicw*_;{TDZ}^U6Rs`?Q(4j=n(Os4t$kk6^2R z_ag*n>9S-|=OyTX_B9YKDv3+Uw2~^rw=6-2-_0wMl6@h@&kN&{9{Fkg0r35V^6k8K zi5HND13PD3@zjPG12a`nLXXd8Qe6I#!r_bj_(i zy43MY?~hO7R@mlEuXM$jvNCn@t6cxEBm_FC9C@_ZLGu^+h42xM_#pIGI%9dqHx6Q| zm>6BA1O(=Mann?TU-=g|eG+BtYUUjx_+d7g6C;F4?VMd6mwnf z$-1O;sBTp9->|>PcyDr;7n3^*X~>159ByW^1gC{6{3|jBabD0Xe`6Fkt(&&3tLuo8 zl~}oO=K(L<#PU?(kkTvXVu|p7mrukRtRq)#a|D|y0UPTR-ge;kJisWKh=_EwL`G_ zC^yU1ILCVrd=CIGRTB?O`ab|$Dw$EVo8Hc5K@>8&me18liCaQGLjgkI7p9-0=RP+{ z`+uy#lq72D>cAME2PoPnv>t;T?@6*x#P8peiXTX` zb}Xw%aSxO25wu|QfCP?4|E&eoc<+X;(#K}x$!jMCN zjIj!NYhK_Vww+mwopmE8ZadS`*p9KF9{GBa0bjQmCMJLblVIZKUd)Wdc2_;`J6Xq9 zCE9FAX3e$|;+Sla!q^76cVqVuju>=gWUCj{JBHLsmG2pnL7eN=5TMU#e0oP{Erj9a zOQ5Hihf%FXNZ!jQUH;LXT0s2<=4Ruq!UL1ESt09XWi+T@o`QolZ;ye6=2jfIU_%JO zM>U+D92f3}`9^&NSqw~Y_rToTC^N|*^mLz*T2()<;W87UAn0i*-$UK}MvXn+r=610 zDe26AQSn?=PEV26s{J_;_62aIM{tbwOev6HGqnYD_Uqoc=X;{&jf>^f&iDgrCsIBg z6)JB90%GcJ^bAz@N!xhUc&9&8U(AaQ9y2Y(|Eu-2cGdz)sudJf#EpTC4)i#52k_oF z7fXkAJ&TJz>9<`=`ICmqRtxH@7)DD1!}WfQwa#wp>AE#lt?pEW;C$)gr2Ve4Gue1o zrfoG!&nwG`0%bNP?>F+kY;EXQ1qkJpS$gYxEey)cS-t!19s1|$?jAqZp%l}XhqgLr z4lC`gy2fjN)&@HJWyj4!M!tHoRxVKagnF~y%Z)#QcNFf32;*#b|ILt%VShv_^YZS; zC3w=hjEPfk_QPZvh!y!9`>b|+m8#HG)wnBkMAnLoxj5=KcePEO>GtlBy|mWY@KE+l zJ%i68GP}m}qxN-op&G%g-p@$6x$Hx__F#xojCn5Gr>Mz~?OycFr)Q?v@2VTDrLfth zyYLL&?s`KX<D%eXPVQ^7>J`PEa!JSh-oa=wP9v;HiCGGTm^*PZG zo6@JM(4y+dXLxGH%GRkDXmU}7X>Si%P&@CP+qBc+R&YLegy$f*7pm6m7Jf{Lamzm= zgq)e*TzXgnXz=se+EGM7f!P94PE%v`ST*qBX;=IE!3KL)YJN{%>hY7O2oQhPj+^{)9e?M3R% zNS6V!8DFq)I@qZUEY6OZ-StWiPpRSAQ8`e>k7K%u8uI?reMN6I&uQlX0(MQS>RHt*l_7A1N8unFT79WuyDM0Sf`6$ZSAe?$MZWYio4RPUNf4YkxJ)h=hOr zS8$7x5*6eslei$@xdAR#IWalttft_%Py1{B5=vpEW#2W+0c#D?VaL>K*j+RzK+xh{ zb#j_>u`*VsA7wa^!92%k4BWzDC)RU=-fR{}0{O>9j0_a1EkWO5S=^JGYk~@q2VUt_ zYT~SCOQTb{cNNZ^z-}1sUHq*#8uV|&x|=%;LjM*VI zgD4?LMS(87w}#!yvyHiWabQRjYKu;g1-`rU^i=sJnZzs~)z#axaF{k=d<8i}l7QOB z$!C^yFKxB<15!oTK`Hy<=nlh1P&C(}*=G{cIx3z;N`>|INsf*KL&=;2nnU_?$@?D_ z69au-Ms|H|^w5`+}q^fBrM?!JiB(RJn>_q=UGXp~5= zYiNkcACWVTONt6tS|Lh*aM-?!YJ2lM(uT;`zJzngjs)|`(ScfA8?iNP1wOayyh@MK*!J?&oR7npS!yk`(x6NchvGn+1&A=@v9jFBFB``Sq>r0zLv9N>j z7!%MZRq&ZnrDVZu!kA%0^U;+6t=>fd+M17FpCOcSPZ9|SalM_7x*hP`QWV?(7tfZ; zA%-@X6Vhm^AXY3S9FfG7NPK`q6Pw5JR-Q~MSY;8_ZpOUXu(J-r7p8k0OjZMjy(*hb zR|ke%iSL_+wn4pH+f=N&JqLtNMHq7S`BS~FbdQ%HD3<#LAoc+J0)YK*1%s8QGL9`tdakkzP_(Z*Z2K{cb(S0qRj=VIMy0 zb8G@`#`*FYJcQ1JjP9!tY47`0wN99IH#&7@5Hs7Ek5>?YjlV}^=}9&KZlhCXjI$*r zH^FqFy&n_BNRZu?WsTVlUq|wFZ|tRmf9Lz*7t5d5PYP5PD(gRe7rOkGgVlmDt+&&)dxn%mI!YCr<>&j8C(`#T2JcLA<5 z(LpHxr?U3{lZU&jQ)&=+eFp>C4x4n@M@OJ0ErSGpD?U$M@Bi}G`tA*&10SD25Fp%1 z{~v6fuZ-?Kqk9h{xffPX3qO_Btf;rRUF~0~DDxP13zi#}Dvr)_inB4+ZWiFGvR zj_+^HbjP-k{+S;;Bh2GR-~ak}h|d0rphFG`(lrvd@iDdsHQ2Dsn)Q(8rfI$@F$!7JVE1pt{vu;ZY;9X>c*eN&iqE?9B75QZg6+Fd` zByzJCWLsf-irN7dtLdEe@?x{0bbYT6aAab*pFg}CH^T_QWO4M z9l=s%KpLT5Pwkmy*28RHOgnx@Dni)q^MGhW@fyE#&5R{^E#=qi;p8b{kNUWAJs#yZ zsERU~=TZCyqv19krDX=6Tg?Ar*BuB2OZ2wVwOb5r%um(R+;X#bI=Ta}eYJC6SW$h} zT+dvZqTpJHAy-%BB{#_zQdwJ&y)m*)-lb8C>jsWlz;bvKLx;Jp+B*O`98p+XQYLL8o|itWo)IsI zaQWZgdy@bO9|@J5S?M&A)G(+pa6Byv1oYqdO=+kbL9qX$HG1^oGHwut4HP+p!_w3s zxw|xM=cAcuO6W(7I}w%f2nd@V8B7uXQ6|3%f;2;atUXzx4JAhcE;UiQbo3~am<^wH zJgLLjuX#422Ni^tiqZGo30Bw=ejP`0-i=SkR z0>LJER9xvebbBkTVn#ovcfh!K#Xjaw#)YSbD1SBQALXps3V zh#AJ0R!bN-7*|1u!%Dc$7_6F!AH91~&(_g@xA1r}zMUn`bD{GxJyJ5$Rt-ch62K<; z;24>RAnQtt!YIQ??TTPB)o`Bd+y_^gMj`Hz)DqL`k{&E7JxPy`rL0`J$r?ergnt(M z+9sW$)b#QBJhy-55(-c|`CpZsif1Ezkw*-5cC!f~qtIrUWtWsoiFjA~k-qzQDH@tE zIvkfaPpwczrcRs9(M=|^NpE-$d3$JPI~58Kly<6y(8ogiz1)iptG5QzM$0AV<4@dk z=fx(|Mx^i}rRi*z%&b6Z5Ddqpwgn?(oK{j#Y2YH0jaRYRm(+ckqdd)7rHU7g3Vjjo zke134pt4$>L_XveM)|zCRh-vZ9Pd<2^1DAxhFcqRE(xc4I`1w+|V{yRvPnAGo^)f4E1x)teY+&1ZxJ0e({do0faE@zpA zrrJK(h};L+I3od-xif9L5Ro58gVqW~(YYS3GB(|&@r22tFcwGzDbR<%oiS4ITj!2L z5*%kRavCb5;s6`-hfQM!))hdcWd@ns2!V{-m$+j-dbRbRPLKHrK1LPIYY@1ywS?A64=u*4Nlc4p8*K*} z_gS0fWTojb9Vf)(Crwi*N^AN{L<+8!l21&8g52wL znhO$Kxo}+#X7*1nMUNj+Bi3P7Bq+W(7tT!?m{%xb8Rnru8apD*7`qG6?fjNfEKRp` zUz5Z=?$|b3{PMk>m1iFG<+G%vZ;4OkHiC>T?3ijpACGFowuBlQM|YRQnbnMH7e;Xq zC!fcK{v~V$F7sgMW_C`Zj^{^2vz!$Mm3@cOwz+N9@l>MJZz2)A5rApDh-7vw*v$-C z9?raWf4NWgdZcCVzh(htL1}9UvAiN*p&>gfs@LiyNeF~Mzj=qjHuqQ9C{q`QqA-3! zA?fMI|AuRP2uK8>D@p$T;GN+_WBBj|cQ(=Bu(zUItt`x}@D#mxCRPTb98kqlC8$Xh z=_6zg8|?@$s$x?(6<|P2_^q?1-qzb7h$?4;QIWl)X>B+~V`cu)+3}}9i!_OKTtRK0 z>)wZX3DzcX-Z~AnzA69}3UD{~g=E8p-#zrmz^p)LFDdj#!c`Qkk62Fbk|?*wK2LXV z7`mvF`;LXc#^G$!Ow}ne1e+`x0@*xnEJ4aBY*d|@-A~h*uP|eau~z-SSB=`JsnABE z;BUxC6aiLka%NfFp|yL|ScCL+jKokFGER0GHsE{jFBHXo$Y?1aO=&sD&I;9XCv~Xr zF%m2q5DWOY%WFIqEwadTeRW?{Y#I$FJ-^5i#l;~*=12vq^4`feAz*=@VMNlxTk`pG z*SgNKCJVaLG?wV5btzCl`f=sHe~oip;cJ17|&zJV_2o(sIO*g!uDk-5e;H zoCE_o32{%Aw~-d9k9aCAWZbwXtyxSuWvm`4wQVJ%tX;E<{fUyma29=K6%%G8*)S>O zfX9@cWfRy%){jnOoC(`5Z$rK3JwBt~JBj-|LNsx|;-gRIsRn+2v07Xy%tfS4LcY@> z%J~jT+KXswL<;M=y$iJ#hl*c9>}}pB|6rT57y$Q|-q-{Ahu&!X&-6y;1%WI~`Fj@< zZ40AN3s>qQMP*0HBx?W;kOP_W;3ngG*TCHA(7Jm$ew;!%FcXo%48xpXRGV$h07XtT z`qspePdXhGb75hfUh=FOq$ob@&$!cI%<1En@oyXm+Vu9g-mCC_G1;$cvmCcY2sj&; z6dRvMlRtN*y$04*3R=fnCiPYY=I^3N<}q4%g9gg1oR?4bB$USqphNK?zb@Z*u3Qf{ zOorv#L@-dPi-L%3xKQ+%%5ruR%l4f)(BDmi_u;k;v(@?|Hf!5j{JaAJa*r2u){eT1 zBL_<4Cx!5XH|`3ZV<(NULFrb_Hh5J+LbuXX-daChG+qpE_Try%A8rcNgOt7m{!e!OZ*s#8QYkr$RDRUXT0I z8fBY0Z(#X#gv5ujU$t?$mzU;w@3i!?LV*{RVKC17N$AWAp7?47`Op<8Fh3MbbmGsu zZUXWkR^Fd_OugIUrKMurcA$QmoA6w~X(BRUTCv}k=heM$E0= zcytjE2k##&TRW`g1}>$&?k+#(X$Vq!$pTUc4L0P+yf2h$@#!bv4`rWL>_%H!AFSc` z^+C3MvK9NGWvQNaGYYz|1&zXJ+;$FHWB0~@4GEMH5Hz3Xn@z7-Ji9+_vF%Rofv@J^ z(IE|R>38g25?Qy{;)(62tqx9~cz0+|vJ9pH1MW18GMg&)*ClYT*i7ScL@-zFA-o)q z(vbzo7z|KJf)9XKf!MtI3!h&=S)+NigzG~GNC8|)?pA_aDUA9X`OvA}YO2#5y z`_!$mH{rrHqoZ!@1yl3?n;S&2*n`HvBz+4fY_hC|OUr?pg4CpV*$!Upp-CAv#e3#* zo=Wv9X;hM0=+_4y?ATk%jp1nf3my%HiPu!cPfm6Dsm=rpF{;fG?n*$ElW;-OpM6CI zkT_Y7{3Gqc84X;k&9Sp4^N2hI)0xVw@uu*tpqpeU zCEB)2Z?GdeS%NWczip$|)}kd`?Ft(>$lJhLqaiZ=Vf$aY$n;RrYTPXxXB8#u@NJJ5 zn)Ik@&6zo|_lK0{Gw?d$GcvOx)pE`uJ;^8)rP9Y9P`nKv;{Kx>WT@U3j9enOO%))) zlV{SIZ=<5DEf0IlN4-6|*EIMGCY%#~en^)0=9o+e(Y|ND{jbJ&Kj(?cc>E%b$Q>g! z*N+X?Nt=F(*Lgvtoj%+44&j2q3VVDE>k8F^lIiX~_Imuqi4>g+oT39L z)nFf_M}xRNkauQ2r5eNjXh|d8UfUz*H~EAe@B6zn z)}G9x!sn~KYXrJ`c|y73TAe4ooUtsu&-tA4E$JDT5oM1v{vYVP5B%Mqhe;K3J#D6M z$FpbIdbDpZW7k0U8rCa7gWz-j^PzdmTGja@BvW+I_{>}Ox8L=9tDN8L^*#UlP5Zrp zy4tCqx}Q*RPnTEY8TlA9#HQ!xr}L+qz+EJHcpkzZcbiZ2;(?jql>P^YaF1( zHOX%fbUe(1Gwefs<>%)Mc3j9O3ag(+dazI739tm^bM7^1J6(^jQD}&qKF8zLx_Fg| zpUI(J&rd1r7<+3Zt>L@xs`tUj@gu*=!wR7OU3BTkD&uxozxguiaM|XxRK?4)c?Ppx z|97mz|6rU*4cH8IB85g~w{B0jS1JA`4?x(-W8VDjDqj{O36})Gam_7>rm2D%XCm*kP7Kwu9xo z7cCS4{-tvpujaK)atH(}g(oNXN%ig$RTZ6@o1`sna zr;gaykTKDghe$`!)(E=NlY&s_NcE$z=|57$zxa%euAf!A7Yt8o=qpvXhAt4|LOS7- za2Hf!c!7iWb3>aYn~8#Qfdcm^78h1kYMCbr;Y*p2#sH-w%H~vqq2q=?H;+_tZs4Vnt=E{`pI;;Yk1Fe>P=j2JTm9mc=YZMoV z0S}tPr)<%1VzrurCCDz$nlEeo!!oe;Xzc#_8;dS?(s*gkq64H`!mI{Q)~Q+Fc}NB{ zzQ&1Sdc_RLG~`da8N*J%&F6f$PF@+pAA)~HbUGkE^Iql1jtIy5%U<&nC<88PlPo1g6nVAtnMxQTWYq2~Grq8-)Q#zvlwa;kh zjtgou$d^JrN;ZG;pe|#gP>*sxMIy6YD+ZPIPuk+Sk5zfa{+L4ohSede9A1;6#X{&=~`kPv5x9wMfr6zm+;mT=#jH( zhleQ}ICvtNXm~v z#;+y>tODn&-61Cte~SGuy&$J_W<7w9`_vdTa%xBEc-a`h3)ay70P06|dE7*+@#_>C z)p_(%U)K^Jk6?(bY#FB4re_t{W~~RF1>bf&HTXVw7GW=96w#1#6i_B|63QPJT-auf z``Ucp5(a7T;<(L@mi>>}#J<8nc_#{rB^plNW0#|ogL;=(MJU0E&AHRO`L>%KTf7$4 z{rs7E7p0E%w(WKoaEW_?r%*W3a>Im6&nL*lLqOS}l;7~JT1o?(D{lNAE^;NFj9eOS z0hi=KV7(E;E}s*bCoh+oq9&v3jN7$3)UdYfWSlb(|BCpH0Ip{8Ga=DIUIc+=# zn?wU+@IJCiofWz`0y`u7!5FcRa8Rzqlm$-nI(fn{KQbe_IB&^+ z-Ebqn=^XR38BDxU=MOu!+2oD(Q*@t?u-cG#RE`{BefTyAo$Ii37(fm^?=_6LojT+v zmEWa%dW!LQyQAdA18LjneGZm6uZDhWvR>}|x&eD}Ue;Z3qrrvDSbuC4chOoUqnY_;37YqSNyFPL>T>6|kzv`M zS5s)~>-t8Kk{T){VW95@=up;uC`}pHfr<1Y6E*laN445TZs}>Cfxz@E?a3FBG$ZQa zaDn_WC9&?v5D@T3M{PRAuikKfo;Kgv#Y8PoPqY60Dw2CC>h|}fK0?SePB#8?f2d2w z)HKDS@52+RbEZ0qrU&xBW&!EU$xIMnADcja;nUX$#jh4o?&Dz_Tuf9~a07&g0tV!9 z^zqKl9`B1bAUsZ~brR(W5@em9f4c{E=pBu>c`!}c7gzNb4Du#v+AQrp^0r*%&BEX&Itj!8%1ci-OH;Fni<$0XaxrW zx*o?9WTUOB`DVTtN&S?hXLUJwWz9=EpR37fd_3KUoS2_|t1?(G9jux`3jyysMB!U% z9ygpwko#_Uo%LFk=ZguhXLi%el6|S&s+ujlXWT#Sbxn-}j9RJqxzyK94-r8X(s@1> zg!obc_h9GmRDnhU*F$XI9^9aV=u^U=ID9a!a?IpkXQrx`-2&^A0^>OXFQ*>JA+p^G4VtBhIom3ZNyRA528jzXmf{5ZK8JOG383BtEOtGPl>H;yQj zxlx!c$4U|Kn0LubW?9o>!EHqycUXl~Z((-Hn|!k(pa!~U(OS`r z%E^=_g`CiY-9_#gXv!Quob-r8-8mneT{0ADbi4zAl@!yk@-g7?&M7vuWspuomOLU0IbSk*oK32M%nyL0m zTozOJ`N@efFn}}-m{}|dB*&}`zMgPTT1lLnEQW}>BcQQMhS z1v&I(O|34olyKfjLbvLlHt}}+s*-AgYu@f&+634}b2^N+%|zk+MMqV<7^hJkq|hdx z#zj|V#YwfHXU|ZzxRph(s?{BuE?C8cWJUU4hX9H|7g?S~q8S}kb{0y;&f^-x8d-GJsBhBRUm|EdiW{0g7_cI9J+l~DC=aIfQXe<# zQ3%Deb>~B8y%UK|I_+j26Uq%qr2m~6a(d!?>7@gPSag_)#EL4t`!nG+5r zj30{Rz#xN-f;Q$Jk3gxHR4H^t#8#7Cye{aj6b8@Q+-x^`>`U~lHbBe*Z{hH8)8$cm zz5vkncyuNdF+@v_=<=oG#zK#7)?fOhY{+!FihUnA*GPJ&=`{7bxluXx%3^$O=|UfX z8jcy$!FFRyR9&cFkdqLJQj8jYwT1_>pe@hNwiR#3hc_td&JO@w1;D_;SH5miq*wRH zlYNI{@%g}oSGh|}T~0X%b0O*~k^X?zI1=QFLi*uh5?ijBNv-b74c{iZGD*kH;LBUy&)2p`) z=`+;IJ-^7gZDN%xCs7*<`Y= z$D6sGu5p=j;~S=ocpYJ;y!bHv7<^x<@RZR5=J}1O##tZFu0+J0(>$~lYy?2p{s4BY z0(E`4f#GWZ)?V^syW?;21UogSGkkNR#hlC7sk`<-S5d#@sje4W0%DWg9*T%86gJr@WYo!8T5;WX&&gKZjdl_cS6;C* zk|IqTX6)dpB@@L8{|w}B-ZD6XQd?n4m)4sO`1j3dm)n#mF)!P~V1j9NkSSQ(O>rw$ z*kdWZ4KZG~2bQXZp|<*5Nb9Sh;d}Mu;^~CCMHSr`%`^yv1lUL7^!n2Dq?7=FCu(A)D>DmYHnG|XUzc$-UG zYv#>3&9&-^>6B0h&NFaecdtDm4#K%1L+Gp}+(CZP$eI9g?qo)dl$gG;)Qgi<0g2F; zVOBF|>4R|21HRqgzcv}mY)%@-z6k|JB;+DslC=a%euYjC!m4&~H|K)&@gZSW9W(>f zR;j`PatZSEN-oIycZ(*lORYMOjn5E098Yd5iHn^hWQCQ_u5VZJU!T-9o&Z&km5VIC zx*EEgEAuk4N9N!i@TIm=aVe3Y9yJq-B?&@j#(A+gEhBv4Vjy9Y`V8LRyxC6d7wWEE z8`kujVaS~`2Hc+qF`om{w%(mH8cJ%a-U`PpJz+htIHen&-;rGkmZ14!rMk7XRCIMq z&#>?0M>t-;dFw@2;p3_;0<5^a0ud)~rzKb0z6fvC(d*+kt<9{Icv(b$<$$GX-uY!R zL)0=`FFnSl>Z>gto;tcm7#x60YJ4=d;4EBvc`n&zGxL~~@C_ciGxcJ^lk`=;dMeg< zwhS9r-c@*iTww$s`?=LvE9Sa~rCEimghX%%U1GN&B49|9l{4R(jH>Cpcg~=I(PIhc z-SPM8Mwttw$oB;66=c}+{T5rBq}9H-rLYs{kD1W&Yt0QxWjShk&g2?-X1Q}=_CiJd&9Ae@b91DV_fkL4;eU)Tal6sGuD=s z%J9$ye}2979({!>sjw9JA%nud>U1`U7Lo7n_A)n4A~$%h+;&}wWBi&h@l>|ICTq0< zY$~eo_(Wadh>JsN^V~WfnEt-X%*{3!!jIJQqMzlWO{wIP;v+qjN^%qi(Dd@rS#tJ?yR^P_Ie{2Fu3WR9hk&$by}s^ zx5%%{K;{`a3*(qJhuu3zls9!MB-?D2m5k4rqQR#-gJ z*x*P+q-RT!TfRLX<9tQ^)5lkt*pVgoGhX-l5$rDF=%lVKqgF+a)(Z_``NXP^HvwW0ut7csY z5WN*DmJr`|9H73ll~*mn!5^X6lnp2?1|vI@m|Ouoy-aCeav3?#xPI~|Tmje^_M6iR z<(If$EF-fdxn>`B}+rBU$2yCjh-Ji zxK;V-2)spi9FEtR_nf8SbM8&S&S&agQO?bO7uKZ;Me!nu=B1QK!sD%|-j~Npj7&+! zWzuhljTEGgg08?fXo)k4nrjT2)`1NqHtPB4PGlcv6vjNR*OWWLVZoZm4;POd662@y zy0#^<%df|f!xarPyj~}t!D5%oES8*<7+hLK3hz>}v!pFx zhg7zntK*_cToqrhxpq@vDkm>;1fdmG)XZ#W)sd>~3E)M}6*nQ2MF&X>eV5qsy-6Xp z`++2PL5SiyIiia%3Bw9@tPF{b%@TJT3@%wcGX;+V{=x;8On<3Pn=Z(YU~a{?(kCX~{Z~q!m%WO!W2(|2* zhy%P;Et8q^eaMFp8Y%7*0R&JoK2}@4y1F?yG#VV#lp4v;&D)?4(V-{C$gs=DC9Rq+ z5b$$F#u=V#p2B*09bu7$OKH|(-t#u~63?ci>^gaWHe&8JGX@&2}Rg0dIzd9pu~#bLbmdg)8I zrX=qL_bW>RC6!ufNK*VJf8WT5``XZA?hBbID+Ryrdv5|)5Ou@e4kFw0>>h^v&*jFQ z{YiQ>Z*TwJ37@gm4K3MuxXCa|p91RubHTz|9?qGb>v1pr0AM+SI!Y!Ew9E#c zjD&w9;&qo}VuaCZGwh@{OX>9Daj~WswUv&-N;@@1GIv}nyCZ}}*lsqS3n>d# zG?U!nnR4Ols7#9xSo6MhCa3y~CHI}1Yh>H%a1{pwT3b{~TT9t47)>@l>QxiJH}{}9 zv=(A2`dFc0Nf@WF#h6cR#P#+g;*7l%5mHGHET)Rg*gLI|9(?xxsa@rf0tRPgyV3@{ z-FBl$k-xb`3WHp|gMMTq{GCWB{uQ@8DeT>tQB9>8fPi?jFSnoGkHuBiUt%Nv(-rie zq7(ld_`h>x@V{x?@msM$;DH2ap}G30t>*h>Ee;4oe$x*C`#;WK{n!P+=eFUy&@)kS`iF#5G*zmfw>Y^H|#Kk^O1=k222&ksf^X_?AP zI^PykyEAq)0~ntiLe#?2)Xoz2i23=*Z+v(<``bypFF7@=hjtz-l5=cg^0GP8(ZuQw z|Kg0U`+q5C3A`npusLD{$ld z!1(2LUq*ZlLV?8i9xw-C=;16?xdOcV1T?S$#ijjjA5^6YES4^+8S%S3=D#tcxlVr? z30+6WC%+kW7I40t06Z5C=4g z$bj88<2t$OW9IK&0=XQ%mktNpwT_lAI7wJ2i5~|U`B(~3XE=CrS0eo_ZpDc?Sx!{1 zt5jLA<4cg~1P7xWU2E7IzF~@`8Ey^!zZgdWJiOCzq5c7D%WpAj_Cjha)X0x5t<&P0Iy4twc?Un_09bg zFytJo;UZumihLXqEJ7)$S8Gx^@SCQQ8lhTDJWV8IxXZgjp^|t-5ov2x`Rb2dWDO?- z;9_@UR1zCu5kZk>R)I4OMMWa(D9WDv-W(VI+#$fWg9LlT-p?(l;{+Nzlw&gOUwf!c#Y2+ zA+72_9ax%V4&`5LF&V_^D6=BP)U!hT$M7a96N?HPw+>=hO||Y}c2xgWZ$xWx{KF`z z?D1?W>eBs8lB2wqR)zhPN-m4yV4sA_f(cxX4h@2^qCZl=j4aHA&4ScGI-ABC6%e?M z6_k-_Y~*5DsH9Q>i|xC>5^Zht)g$a13Xi8SYnrdAwwv!bsjSU!4i%=J$GCOA$Dc2Q zIL0fhp5=fc`3g$5-tYx*J7FIX^ao|yw7ZQ@3T0$B`hA0Q1eYD<(zi*h;~FZKfa_W1 z4xup#%f!j#IiukvLjo#^^2()yAc%ys>A?XO9^-d3c1@HpV$E+5TpLOJG&O=hP7J%h z*WC>bm#3;Qt!k|-}ggmzf(Jb~I8F^$a zpE!fgLI1mu8u;w8c*ml|e>5vK#j`DxOPqbNUxt{JMIVr@h}v@h!CL;so*VoLz?`HW zvBAgjFdK!&T|V7;;lpr@jef*;QL*;}=@3F8|Q6U_yv#3Udg-1E*Sc3a!V(m6w?;z8^2|x|_5@U>R$| z6a!^qqX?r;hR)KWP3|@UheV2y41`8TBCAFH)SdP52`p!KaG$eHo|U%J2Jp>32E&kE;l4h)At6s?&PbdZ7~I;ZdTUtjnX7+iyIv1g~JC zj`)rY=^{UG0mb?CU6`V1ICn01UPAuY-=V2QH)JjWfWut}#$;ghy4Z069y5$`Lw)^< zCi1vp1;g>xgV-t5n$M+x4|>DyK=hoda$3j%MXhOoM_uw2hxoWEouu?>E+z;=i@;v@ z7|!LFt?f5%*huEm6aye3xsw^V8oYG;x>Umi+Z^)ChFCTzzq(@-u}7>o|8#bK$sGT# zB?34txyxYPIWn!=J_cJDZ^f>{31}SR(|K+KcOT^YdERJvl-ptNvlC)(GgqGj!OuS7 ztOE%WmyNm6_uO%Q-qnpy<#Gq7AKG1euuZ*UheJ(MN`EFDOMjbtQMlFR{WI}n4CyO$ zh=h0*m&w9TRoaYYINrIjmxe>qEhiG0+z2^F64{jg<7>}~Yg_%a$6ToJ@*1>XlG1U< zmjQd7MqD5K!y>aBi+RqxgP$&gw(`E|IqhEA^2>J07aMxzSjG*<&5Ek}+%>OyP71I4<0|{r?W=K`P)vzDf>sl| z{uMl>AxL+yUf==4>`#SOHSpiRLN9M;fNatJlZV;z0!B}yqUOM6x)%qgOLJAdt_{zz zn@f*2jFlbN0^sP;t@_|VKpuO4fR_Z)MtJe@0WEs$K1II+VnO`H1*F#d5eF~1b8uzb zh?o{z{>)7xri1uXf>1gjfQ4vjBVnq%zlv{CDGfNCfH0u?;s1l)4gHUdivQPGyx!;s zSYLh9Z^dVv&#IxpA-ouETQ^;E;w>Wf4BLW06*};~H+}HCCydWnk|{QKGYQ9A54|@G zH@J7gUJ8nuqkKEgEb?IpBIexoqJPp3cQobo55_hg5F67~zr&h-x z0hzK^JBslrx)mZt#p7DG+xvqX=0mM8M;Vi~5KZV6vV;0~8{7qx;v+;pb~bKu;Ygmu zG}*Un(zB^NXg{Zd1*XO2l@G?#S=G_NCz`SKN(G&E^=r{ulMx={g7O z2GWXtg8HoKnbwnvl27bJPT#n;;@NP8l$Fz&?o9}Jaftpq#B06v`$=d+BkBg)n9?+d z7q{)trXsz>WQ=bAV$P(;TDW8mI$G9`YY_ai*mkvQEN%*(y9JgYM0b(5z0$KhgQscq z!NIqx%Q$?OYqhH6)rG$mdl(OMpQs(yGuanI2Y}A4ZD9M8n{BEl)^zq;YvT00-lvlE z*h}$1Al&M|W&!-4QY)PL_*z(ekL6njZ{`BfXpiIM_=mtG^ZS4odZ+alxglO$d`#<8iuZxz8??m7y)o3d zgufJ?A9IXNVjlKMWaMVG`TH%-apZ!*nQie4ed}v-1vjvy;Dwpn_x7PZswu!IeZS`> zW&QBrv@%Mz>vpHumg=c|-fZAQyzuOqI^@%6IZ}_0@_qQTf}T(JZkW1*vtiK8kMq;j z%WG?CYx_(6nx4$vjJNyE6GWP4S$yg265Sj9s?eDkOkN^Ad{XG!{IY%hOVTvA*N5S- z-v{&CbTxV;f7NNh$0I$m+5|J@+C8{S6nNU0@21OHO1E8_4&Wip%jh+pRCeY0L`wOi ze3x`TZ)*@w&fQmKWIOMuLZPAfny~?~1l*-RLyqg?6yN090fw>z8Gy;Hdt{81KZ}^F zq2qR)^aD;WgW=Qf`Mfsmqp0xCc;=bf^9}t}*L~=6`aFkh4Jaw7b!ruPPUp7d``Ekr zJbxe8gMBAE$>Vu|9TMusB~E$k-&(xaP`NjY$?mV8FKU`Vv5LCQNTWiiL zhKyNVw4aN9?b+Yy($t*%Mu^Wmh)R3C_8)$(vao3^qn=u_vL#&mS2Hd5qzSOjrjaCE zF^|6Xlu4ti=)ls#p^;R-oX7n^K9E~amjY15ra}8M1tV(y5nE$3^wPBR2!qs|FWR03TT;=4oJN@!T`>a1FPmM00mY(Qibj~g3 zF52YLZYkEQZlyhV%@^b`{l$=lEwJ5^#e1-GLJ_5F*EjIy@nixtt*@zSi1em>;PG&f z1)C_h{Fdd$%1It@9Oqz(r+^SuNc}SVhiV7Uq!CCqqVaXx6f*uu4(r$8b1o^bv9D@( z7T50PH0+sNwkk39=0jK0IwGQNooGoA*6|!yWX$Vzpds*2XXZD!?1VBJJ28U`F`~Ft zlX{nk;2@_UG9cAV$fv3KdXP0o0X0SBPBXS_g~PbvS~(mY@eH7mHgjm1s%nj9yN3_G zRks~0vVM6;Xg;U;Q}5Bm`C$)SZ_B`JB?=4s}6@C1q z&Y4-cq z43GihzxEEL%CxF~9kRX!??Gq+p=`Y)&A98M@z%BQd&m zU*{Aa2A6JSBvO}XQ^NO#zu^q+wob*5^iecTGFgA_F`G$ZTU^)Qtb3YSBBhI5+YqZT zIJGfvwJ&C>cTT{dl#Dw52%So9k@=!C?yiENwrzAPc7fb@v=H{oyonQT0CT^la0WiPlJ;Lely%+$u>_la6E0ov zK;z}^;t|=9F|;Ns6PqI*x+QiBQ{l{P23BRa_+4hKPEAh+2gNa^#CGiq?a{I1X5u*Z zB04#TbbY*DQz5oUj$&0Xw<&eJe&@44cp#*C2o

zeHx9|lHdM2YJD4ADF*Q-ru|WZ3mV(2r1Ff2zT}pCT-HZhxs4QE?bH{=zl~cVFPzSj zH)A0eU#CppB8$8aZY&IPL`AB8_$G3nEzCG&{Rtf$f^2`Px;{m=`Zs$%r^!F|e66SX zA(#B+G<2(1e6m!53c6OTh@%+H>ENmT1`c1KbW;9#oiPXT@GRILF~XhBYBd*gD~9%* zER}>(g^r{R@PQOjek~|m$vOS^*D~lW%>0NB$Uu_P_iw$vS|?a>tkF*F^ntQ<^eF|` zltWwovsk?z?}VZ=Z~N$`oJUhTCxa6>KRF$XUpzazXnI)6P{*yOLq-2OH?~hF*=DAQ za=^W{Dk#u6u+bC3eiM6&+y_r%P- zA~>lHPI0jC!2WHc)=>KHEx5Aqv9tf0gFOj1ADP|CKII@L5DA~-v@zQ9Um&D_L`_f2 zsFQWf^{s!elV*AdZ2-K;sV~bP7gf9nd6`;zUPL%2{fT)~yftUjk~^OfPEC=dX2X|0 zB;-9&$mMkgBzU)p%UW;jhe1b6RvOMaDY@SjvFh>rMPeNX4>s4x3OK!;jCxO2Q+k;i zB0Q?1;B_V?W+r-B6Nhem+!+2M6`SqPx8%KFP!2+*4cDMSgJ%!J|ME|ZR1dAlM9aTb z1c%|ndN7zim+l>mjy7`3u_P|LVZ5{_(tc69d7OV3fW~`z-I@7-n`(s|=NA%ESJo@* znfsg8s(r(ifZ~HWoQjhD5w)Lz;-aSs6yXDW0FU&T>HGL;Nv(w8OuibQvZH3aS7~G* zSL{37f7dkIuLxZ)kpI5W`EPd<|JU9d|CiP}{;7%I8ps(zK>Q*0J8q)zpRYnNKtOs2 zzQEuBb7+1Kn-Db-%v+Cnt}doa7lRK>&o)`@Yne?tiJNTott2*SgEhgq-`1gTOEQZ?yb6)1KKU5DRO{823u?`=j}0-v7IXEivLJewKRMZRc>Xf`u^WLj5Z z?!3q`_DFPC_n14^K0Jwk@2uS>w zzkcKYLmvlpGGgW@Rw6(Mnfb(`%5AIZ3^sGdg!z0JO^SMc?{xS+L9NwY$}`n$!4kWn zx?R!KVPyEIke$7a9A7(!qlq|3i) zl6sCHPNdcyGdGm|r?}lTNK^8Pk?hYSp5(Q_K zp~l*~_F8++pH0W8ub5atFa^K)ES>knV1lEHF(?<^oFzgSG{@aJPK5@HTQD+N&2hAy z9G(m`#eQv(QWFTUWvdvyV`fqHG@zzqV8I*KNPyPY9$M3oM#~bkFfHVn^^HJGg5sBS zSU)(uqRRUMDzSTm-46uXFgv7!0L#ZmYs+v#-)Kn-g%<#VtfS)9<`eWXsRZ1*`>0;z7G?NQQ|W`VTb79u!r+t{bf8= zx-4!Y7ZH|hNFh#BASsqYMhX|vP43m}z+`E&1lG??Fm)GgfB#}b%Huu(Y?Km)cd2t$w&gEdjiI ze}7KHjt`O<_E;?b7PYLZm!#KTmpAb^^O}JErIX{CRq5|| z;<1^5+jF0R=KMY8*RQinxEI)@2@(C8h+mz8*XnI?hG|s z#S5W1D(r-_nm9XC`djD{v?p@N>g@L0^bbq0h+W-sr8#+|`%R2?Qva1sl{jq#jioB! zMWXU>A9brQ65I)1W~L8WQ=x8hq2>InXI$n|X{TeXr)NA^X#KHFJl^btZwejq+&+nH zfuZ6cWopo*3|>hML5EMs9`^2HOje>wqkn87VAW9CGuhKDTbNre)DcTDw9wazmh2z@ zB!(qzM#zPA2v0bptx`M6Ry@h@(kNUNCMnE6UQk44_a7{PSuZO54iUzT(P1sKhSbEk zW<=haA>?N4)Y^MBUWX}8c~L1tc;rc3SJryuP|JAJJy@lZ4FfYPv(5)xWNvxb7aU8b zkqqjOVzw?VO}r`|);nUVC2gRICKWf8T2c;{^~l7xl}+i;2}Ko9Fm=C8*ZS78U5{b*;xTex*4E@t3PY1G=JQb9)u(Gay zeC$6JcDl5RhFq!^o;;bkL{EP!(R9BLcVXcfa4cfn+P3vsv&~j-m8}eAEg~GLLTHm3 z&{@E1Vpp)Pw91dU@Ne(Lnr9nr_9hXU+7$V#ba^c?aQ!GgE`y_fhIO2qK%Z`$hAP-- zb7{LbZza1fzNltBi_uSGr&C@^&f?(~bBSNjmdnF!0GrT=AK7j_e$1c$bEs1nw<|U6 zWei?X&a7@yym?TMFaif!e^``R^a&o-I%JYtv}l?+g14W0tV%H>x@>$lsX~c5wc2fn z=O8W3QayHAmGQz}e19ufOK`VzV){WFpe=d+adp^2%hpOt^1H3!5%m8)~;>v;xOkKdy{rMIz<>UArZlSMiXXstpw z_CY)9J0CbbIr=)3=H0L-@20Y_kxv(?lR7#Uk9|7}&ecN!Ogt4(QA{rsaiP|i)|+j@ zxRluAd!?i>+Ubg>zs^mJ{bxxU(Mz2bP;_o|9I}s3zbTWON=A3*S=OgzM3SosSUZaN z9RJS`N$S0!Xgtp^0qCe+7+#TXgGoQ@Tg>jj!&xN{E3Ci zWy2u)?+ah?#BEB)TN(^3xCoFLF}N8%!R6(KXGqt>dZ23xYSuq(0-wF6B4Y)wK0?~6 zUm1i`nfIu~x`+rniBQ2eR}9C_9LW?zOW8%)4;+FxdkD)X3}XZ_n`G0mN}mjNM$|A8 z1*t1^ER~H|fFSo0rXc)CI#$USQnT#6T`t8FcfD9u=goozkbS9Lte$;7?h|8sKJa-lvy~bu`7?pyP*@ z9c!0#^_qBhve}G}GVAPKrWTz@P0~>@j##XvLr4(ifSoJ$!lrN&G0`dJBXKCq&_&&` zR^=DmBizV&??5bEnniDKdO}1MhSSr^jGsFML)G?5ONUNtk7V76!!yC7LW1`id2#Wn{xj3ngI3)GF4bzCRVr5 zH=ZAfg3{LCgl%SNXMv`1%zT~1=y47-V8*ij%P7smAsvs*3K;_vmLO?Q7VLug)OWlt#msl z<_9a5ygq>gL9Pt`#Z>i1yV((%LxreWEnYRje<1vX5gsU9^k91-bav3OE3)pVVip+T z$V+mPleOb$la;L7S-u@Qqni?aS&dGJ6*vDM2AI9u;30fz8+Iv46JZZY^}hIaLfM6T zuQk2OLh`!WjyBg-jq@byhunsN*s{2o3%~c0w^tD_plX|OOc9?7+Kf%hJ&EFYWldR4 z{v?($B~s@`C)t)%k%iO-wwu%vhg#`LWWu&!MTw1p6@c*&+Eu9Cp?3WM>Xrsc`O^)NyeZbAm7l-Z?FIl}jUxI|o#_ zw4Bv&V)*G7U2E_ZkW&Q6RBttK#2y672RFA&{r<~>)`WMhKB+rL{74D;6Sx<`aw=P| zao>xCU{A6A*APU0dwpCnS10vB9ECd0m?eBQRS-&QB?OiDNu@={iCwXZE>padxR%L} z8SLo$H;nEBFU*EX_ozyL!z^>kuavx=Od+FQISbpQ9&tN)*CVMgerkzUrD?J!GW8PV|2_N(g>NMK{NxoyK- z4fR#shriA;*7azIkkWagTxxzyp{}?m1=8B!%v>m7j6gKCQmgZ zirC6kEDt&N-D~s$42|`p+!C>T;<95%((}{$LI%wiIpqS4Zc7gq1+g=%t!=eB#z@Q4 z?E=SQlxDzbOV5iP%^JnNfjIju2zqwYcc7_y96V%}-oCdo4F@0lu%!E^ zsYk`YKJs`Ertg!%!-hx9;Z;vZ7d{3l^`kJh;*LgQe zl<4O-n>k0A4_#tEIUe4?*Wl5)Ef-OlRcdX!&yYiMLUk+VEe1Eg=w;12%ecy9sW-@L z???Nz4_;BVr)x|UGx3Rd23Yk$ng@uXMk|Xt8Bc>rS;(4cnw_WoA!JNBHrwlq+Tq{) z{q}z;CZ1tV-gT>5PF7ioR*_WKMFjrryy+kI(>EwneKpNuP`4T&a{snkWVx|R`Zv6~ zIr>_DfJpEv{G{BxjL8y3pxW>FbT}xFP!gV76#lElmhDv8*3uu0oZykRwi&TJB+N>i z4w&;UbBxe{F?;?u>{W+-?TBj9reiQ9hGS(8#C3KZq~NB3H)@l{5)PtxUA%8wJa_%% zew{etcX5hzqc?1Kzq7vnYGrFP(jvwlB2(}1B{OKsj>G+>%JZuCgD88NL3Nes_2HFfrAYQ(*D(8<+YQax zvRJOe@P0!Pfc^+V6a4ckGIN02fO>BXG#1)`ZuPvrG7Sbc0JAR-1y&kEv$O2H4r@Tu zP{7#BjoGtYfh&*dwyI=B-yCxJkKjpLD}ZH zRiw&?$u_E5Y1_jce!?E?c@ls^e1>9))sF#JQLMfT(F9_ZPMm=c0 zcn}Y;WUMuD&h0{D1LHI(MeJpAbx!XoKc+o2wSVM+CgSM~xR7c0Lb}I847p;$OHylZ zI^sAEyx(-8IlN_2C4P>RuwfCJ^Vq$B{(xR(Jz;>J`QcC(infXDp&&&@QrH9Yd<&NS z*Ac+g1~B_>pK5~QD(~sML-9K5Ew7LvrNya(%h&$K%WX zmdu;B8oM1T-*?VcEEHXyeN_a6`Vb2X<>4=T8%W8i(&{ubHt@UVEKIH#o+(qG`8XDH z%m?K_cEoPxv`({MQh*s(P-;nCzWq@|df~K=N5U43;<>p5=(WcgSK;f8wDlVM%@tFR z(B<(<$x6-PDC{1&H-$)R$>dN(P2zAsdsjB_$olG3qIoW*ObPnNY(zkRz9x=2gMG5Svc-x@;i>oUb<%vhair6lXI2LTX*b_#FS>$ylD<(v}m(2uv;dTCcd!G9C! z!x1#edSkuQQlb9tQJrz3)zgYMx*f}wAw9+L#<{y|4RhdQmYsBSw%FEuW|1ttOc~u< zIsbcMXtKm>)`d<#U1YD7(@)y}y&HvlA?xzYaf-Lf=m?Qk zu5A^5qNm`;k#2XYXO>$v<3wu982)F8ZF~}+fF;DuW2Cv8ShFR_gH4)KG$>ekIDbwr zTI;R)?X%ruGdBgt>+h0NnXkzXI^h*;;*OO^*3{!IsD{SVe^exYvno)~%hbwosG$q$xAng~aeJuAT8KjxoVi-kP!x;0kiNBrW#}Gx zXb;>i{b)S>-hqT*LM&1^ST)?d3kxA6mX{y&VRoU)3{Ex~hpEOAZOAdHFO%Ax9mjD< zG6I!bkMzcHEzui}-CIcqh1l;tGK*tQn-#>YYlD!XCs>Bw)3qmPi(i{a5fLR(>~~p$PZuH|s??Qg82EuG`+VXpCw<3LE0565!t-=~e8!5uC4QmCE~IR~t- ze$;Ci0j|Iv_&aoFig}>9&5gsE+^RramRCTO!qcENm=M@GTKn{ii?>sKisS%+^5t_R zcOj*6NnYJ?uv*;S=@tL9J_lBvQ41Y2nIaLUd`@N+|DJo+j}Xl3`OeC_bvFUg9#~c`$Ns&7*@CM!13POGya+Aw?5upk#W6_@vUiX zmAQi~9r8{8!2%vYnGYA(g_PZFPA@EtS-(|MDPlrKHn@a`pJjzBEwy{$X1 zRg3zw%D3?nP9378F>1i)uW@2qW8CJB*^5VOiz}L9D0aW*ihsl1RqJlDLDUlR#Smg7 z1k}kj6=n2(g;e(2!iKX@dGLs!5MrJm6i7BNG#Vx?_b%==%BZl<@i>_dOVgeeT(Ts0 z2$zA{q|;fZt}#6SR$btMcWS8u5@?gu$c8_xN+VAWChx2*CO%$1la)=E`INL-T@&tpHm7$Tq|;xTXighy za2!ijZFV-!X5fM)>T`~_E~r5A%NEe)*BVk^N^)rFymsG=XbcyGmdTAZ%aDM{FjX6h z6yc4Pdkp0mKL$Q=GBPxelFBVbIc4LHh_`Rf!K$WTHxw0^y#ZWC2`G_pF;OY_ZEI^b zT4c<5Tb@&sKgKg@h+>cHo#oLteXG9Kh<$Y9Xf!+5i^3tbV=U#wjiT-)kIfZuRaLKRheT2oNJ-s+Nm(4AjA7NpSb^Al}g0E@9tv%?W*jjS@QxM@V( z!|=e1;FLdv;^WyS-{yfj4uwF;*>`Jf#ldMp2!FH#QN%}>rKTnZS;JgWD!~lBBl>}a z#uen(Mj#cRKhrcaxM=dp?uEZ>PO9t7rKS0CC=}Z?KR%_fR_)}Z9?reWU=gGBKvay) z*x5Ylx|6xG#ZT74+O%52n0|dxbpe+=@e(lH$>%OpB4sLwZH!_=)fj~A#@p#KE{9~| zX!7w}_)thT|3%Z+tQ|)Sa0<2t4~*YH&4umCdQ%tub~l9DMK#W9qQ_L-PLoM-Fh#>i zQ`;-61n3zqXecV~=aEV^gh9=3PfKIRLrx?oreA@4+N3W$8>}qF+n?>efW_d$^)2h8 z*RirR33(Pewe-}r`DnX6%_p)j-B)8EX?@Rz!3MqEIQ39idq3M&%$z1Xno01?)?7-; zxij0G8QE~qModT%Q_m7ciq+kZQ##Sucj7^o@`EDh%ZxNz&dy}-ztj8~tFlB@onuAr zSi`HF61aT7?nr4d+&YcCAK$UOa%KGmz>@s7HVkB+W?T@{8jG^z zaIL$EZ0nYOP4Yq;dAv-9R&kP5IjklCpU%z5#OidQ`c(lSloRcg2~-R-x{NRD&> z_P}P6r_ZV!&5Jes55^W3vr)M5#I2I8Sg5YA*4wyfp#82~pB>_;m%js3Ssg$hgSm36 z*yokIxogEE;!a$W!>6o!u8wfz`YS#%DL1q^U|&PZ4C@ComXJ?N~L`< z*H4Nj+1CL??4f3DzlLT=t#33dk%HjwjvFnCtkZd3ZoyWyb3Z#(Hb3n{c8Zj=RyQ0YBwxuAky z8$$uMy1yPLOcw-Tzly32a6$lmVz9E@pDx|14I0vP?^4=my3$bkaOuh6`;nuYZu)R84 zZyVK-7&3;)1Ht>tvhj5Z=iTZ(Pdg*e=yhO< z;A_bgEcCA+_P#m|oxyk~*Giq;bw{WWzSMKs%g_jPn&un?d^po8esv4T6D`(J&F18P zJTw%10R?8AL+@N~I)rE#fRC3#zO`*FQc6IfBp( zecg;tfSXY0gU7W?LT#+#bIpne^j1nBw&dh&KFEm!CHXVVIE2_W7UJDV4fV$b#O z%ATjyYf5aNRQ>aG7A(7N? zLDKjZh5@rq;>r`7Lx`R)(BbhUXf~@qC7Oex6#L@XuJ+U$thyQM+lS0@*Pz?W&wjJ69UokrHHbTWNk?;CP&S1BYRe`0derW0}c3?I3&{t0u z_wX0~9p2qf!^}IngTIIPE>;ov?gd^UvP{|m(VGLsd+gx8qm`jp!H&$aXSSyfCdQ(d zi#(r_Oe0BgQCQrk?pAg!`<=NWe_2>is68Rx139NUk9>NLDz*LW z)R&#aBQ)jtZWQ3n=;F1Ct!3$fP(gBS*>(n}1nM4E25+>A0$~_{fCxNCadS-~&&>%B z3*-l~a$`-%XT8pdE(ox(YMlSZ)A;+$?rW<`^@X?2o^;LUszC@98(_pc4F7zxf7yZF zMO|8$Q?gy7Dz7TgZZhsNV0~V2CXB2lF8{*%1+V-=#~@&dD|ete)QJvv@7p)D98^l1 zS@KR-=~mag^Re;w?1@t82;Gk7E?~gr^zogptt(g&=Kw8W5 zGH5B2zVu$yG3gVaS9hY{Z#DQR0gQi?TaFD($7s`VQl^C3e{bb3pLplK-}DpJhlTlK z)xT-cE+>}$wJWr)V!iWqucD4_{PJa)fa#s4S7bwz1#$vwusXc>s=w8}kYn3s*@Ec3 zK?PMjfgTN;{r>WJ4ijLPZ>80E;QgJy1@P*3bj~5H#}`vSF}t6)O)bGmNBEFDM4sMk z=SL<1ey;PO5&*ZKcmF`c0Yj|{ZVbso*pPjgkYN=!H3hM!Six7fj<-Zqfw>@FZJ0ZR{;=TRT-6>E;S6w8bMU}Aqt zwu(UoX1=?ec!fT+_r6#mgB7r!U~b*7%2cT9I#=?mSUHxTo#<&VSSSMIdQ{V+4}~BT zC?z@Y?p4a%4G5e#W-VGWfVP zIyf}WIiV9`YzXjve)zM~5FRN1CPwnOO7eX=);+=1bEtk%-`QK@+K%W>d^I`JS?p4+ z6UtugqP!{l0=O>PsUd7J4)(g%5DNViDD+Z#3=8cNN{dR>gH#?@ao`&I9#J(fv#RA! zJ6rzZ%n$Mio~0NG2F^J&kTFP=cLRcfM48v<K)#_&3>=WakzAVSM=K%%@IuL zh4=`C*T2!#4H=0k3Qa)xoRD`v33t|?DPUn!RdUj}G-W;_;TGih&S>2qV{)ZR!T)sN z`Sm9F`VPNS5&Pe)Li%6!1>c{KeOtW&<7M9uZF@WH7zN6DdKqBfBs8Md3vI07gViK@ zNp*FEo5Z}A?#*Bge80e*|8O*R_Mj?%_No&sl8t}Z9l;Z`ikqwAfAmg~*JC{;N_4V* zG{>92I~%f#H}DNdt_K#|CIPcQ&(;z@oN&Uw&VV%0s+$=ZXlg;a3tahW?;IuHy`R$j zR4pyDh=Dh3{PQEok4^RIU_a#pyf1lbqhEIjNq|-v3M&SA&b-l(o zD=Dz_D^Ibx#?GDinlyCgc8U;xjqZuLEK4`i9#=GeDmgkB$2*Er4tc4C=uYY^b4ASV z3{&%LMEwBUL6+m`t?;d$lKIvHk>$ftj|2Hu7C0$ma7?UH+`3e3AGzjS zNXNAQi`4j$?Mczs-b{C~WkWfuM%!^&dN^#j?>>Kox?7uhTCU*L-)`wQF3b)ic;43{ zs~Y09fQc4^CyKpXSG(2Z?!qxbtwh~c6bHWWSM7S6UE8CKTA1o^CU~;yEouCP zs{PCQRxLhewVnpWdc~IeV20qn)9IU%$2*|YH%)FrtFHUhsO3lMNK?(9h}FNCMNi*+ zwrCm;mKyUQXvQeP8RH>j-Wx>1~TJr|o+YT=c{pAhnGlK|s5f4(QBscMaou6UY?oD>|QL$9j zGB*62SO#$^k%j=5?wbp$TFK6G1ty0$T1mw=C-HO%|IN>Gb9AJrcc}735_PJ+l}yD- za}b^)RQ9&Q!>Ro<2L;C1lf&XJDUUQqOq_K zRZWFk6ITN{JpUqHhKePG?j8k5AXR z5vKpV{B0D#n!<%+h0RhXUBubc=)Z%@xVcG}YZa%mrIu0=XZz>PBb%pWJpp$d1%DgA z+v2?{^=(7aDsk7Wo2g%n97U)dm6+5G`IbddGLQG;Lg8>{YfL{9&Jsea4eBR_lI6yQ zqIoAwPZ585`n4Z3)lA;Eo9LlpzOTt125fnuxRIWl^k&S~^vK7@t@%TIFT0DGkjXjq z%h~$W_$6&;P2DKA$hY)S$+t{MTNhD(g?gdkJ7X+6DH`T)8I7_LTl}NDZ1u}_RZS|F z6)XYyZZwg{3lV0)i`jrETyCoeEbzn_5=S)HsG4P?5;_xSWbglJ9#7Tvuk;&R5=YRW zd_q9t%;zK7;qGa&wH6{H>)Z4vk)~lTaf7Qqh_}LggxAT@lXA!0F;Lp3esSSiZ#F}q z^5N&6>atd|_Ov)mpH!ksc~bk$Vg};bzO#iGo*h}5<@O5y3K;>jL7v(i8=H(f-VJu- zIML^vfq_Ev`5?|Ak(LmV-BYAf+d%GXQ-GwRR%?iWWwD*`yj5)#esVrXu`cVZL-wc_ z#b1;J{~rT!^D$g=Ih?xYllq|OJQ`Lu^7-%5aJ%DyO2;c2PnAFBD<4(fqs)Jsp3T?$ zT-|U`BEySeZ)oqgMvrr`H-Hev{ukFGTsGK=ewyyVOj#IEhB?W{>tppQS9n(bO*gra z+%LNvH$i^-n{Fgun{M9HrFaaDTVacH^<^5?g1wfnsBt}IIF0p~mKyDm6n$-%QS&8! z1B=^eb#5!*jW4OcO$ADh&b9c?Y1K|G2IF6AM6>WAC|j0FsT)LCX{2}2d~24qWLU!g z%YhKeijyM4OZG`2;^=6#SqIHp*-Bd_SrB$ay-XH2LZ%M^j00LX)V9>!a7-V;3h6hY zb0(3J_b&Eqs_8IMs6T4MnxAN#b2nt3P^D~w8KD;aM~)R$;Mx2q4QrfDT01GLBv3hO zY5rTpW|VmZa;T`JH7FxTnq`_sXpTw&wt~Tn8Ax(op%U$rfjcR^B-k^?ak=dQ^3KwK zUw5AV!^T&a%0esJWczW3)Z#~D{I8Hqek=9sSMw)&HgHE5MLC{e9rfOB(Of%fGtT5V zlQ++{K{R~m%xdzFk(U@@Gb+#R5OKI3dH~Oe0i6JoS^Z|Zr+PMa8_IZO5e`0x@Tg%T z*&@E7yhHplUsWchc*e7*r$RE5RE&&Ow_TvmXi-)M!|@@%fADUHtrB_e8w;~j%ynUT%$I5yxH8kbFQnMw`qetF-;+$~FM-;k7&BHRX50Y-7iF04+pg`DH^OEfgBqU?d3 z9EIXZF$JYZJ8wy4&_B0fH7*8*E_gV-hu}pN;88oWxwRIwfMkdChA4wh)=&r@pCgxx zm?Wy0-`7+WSdEYhsSvOPHW#iW+s@38`}!->{fZ7+z4Wb{{rHDCl*_!H+I9E`>gsrg z%D0l~`)-i6N6c4v$q-X*Mu7SbB<*1$@;;8>%a>@sl4-$R1Dy&LquD-9UtlT`MmZi; z3^{>^)l$qarWDk>u2>toK839J`A!}S>J8&YR%879UKAIq5s~!6}v7@lqd6Y`E+n{=vXvHq68SXRX9^ZfGL`2a3hjRT*AbQ z=z~$mmpm$kSXNg&nfT@8{#aApLwvSb(cE|$c4XMiKzP=2OIlRst5^5c;EcgG(<8N> zBUGa!XrnrEp^rCUdpF$Z5Z5-2p$}b`= zRegPAmF(O|*1zFh%j>BTr2QtgSO4xqz9EEKz%n@4U&Z48sI~awkjy;@tAiy|j9o)N zz4W>LrmYw!2h+?g7~n8M;1Ify%**3r<)s~>V>~tr02}t>t56Ca*i=?pNh$g!~%dD_7Hja zXRR{?Z0|Ap0XbN?DmXqd(ypJB@}HuMJ<o}A6F%2nO$9Vj3Sw8dp{?dclb*DoWiBP3K3-Vn(RBel&RxI5-C!nD3do-QBKUT z7-uJSh(Yy)*`=p+wQ^^frp?aAv9q^dUHY1>Rp2t2v@2X=;}YYlFq&s}a-sie%E@kt zkO84%6JdEos62AbnHN^H`eh-`9?PnpA*$os;qS=Sw9tWUu}_JX&Kfb&G?0D;t8X>L z_>>EDlgPoN$vmpA`qKIuXO13e$hU8r1=q1>&+McH(83!X+W3gt;q(wHY31TxzcBPc zRKonZm-ghCsLBxL*aL3x8(sP+vP$7qA!)@7ZCkJL_Uw+4FUc*h4@9itz&vtEECvO2 zjpHPoWy|ro)1;ZXhB zkac4~%qSA`bKW=ZTx3PK3e6oNW$p}l%$m(^xOv=3-+~24NqW1N6nUJwPIY-z30*i2 z*_|{MKl8Sz4i{{udW=jx7|Wx0G43i9{8zrQJGMgK#u3I&w_Rn{xOT1+(sfpk_nAav zYg?xWiV|TN!_HwrIP1hDV6o}HfGsB{EIWXM*i!Hzu=fgy#W7E_iG=BglHHF037P7k z4m8gKV6PU+X`2jv*Q(07PSG(w(EDVTzYHh#Bfs?&C@=A^xgK|Gb?2{V3E)=lwM^j15(_yHB zPc;7Zlf9wHLR0&U!8-OE>L4W>EO1&%qjOj7$~oR=)wmRQz_(M!%r?Fx4;xuC33kNnsHuIb-TsV&OUIoSC zZH9Z>P)H)zVE}5s_$MAwrxdE)Go&b=fQj2P@20{jqPXJtfL*`nnqm5d!BArxMfUqkkMr{F1)Nq`zj8tt=ePOIzu)~c#rgNY9C@t&N_s^3XY|BZ z*niA`{})~E-CWW9r#a?UeCYHUo5X^DlPkyT{z=*qgDLuor3T~wHiT~SZ?%s)0m|Fp zj01GoFPSp+nq8;dCzm;>K?<%s&5KxR>aCuneZSPVT1)1AXwi8WPgnHtfP@8api)7+ z!}^W%T~k%Hs-VT~b9tcu%<4}YJ7@l|zD1rl5iwtPGF&W_x<$M4aRn0faQYYJI6uAE zPk+nPB_l<5QN;$lDsB^pFX)`(Ex!VPA>6_N*oiWyc4> zkUpvC-{5rd&}7+C)y5qn%&>uC6b_UMs! z?^>(As{|wzHjc7&-!Twwq1~8Lmwgt63h_-BQ!#Jp8rEBS)O@E@?`&xJo|0@~FE~fk zlcHqg-wdwmX!rZ9(c6>-=vWe?vN%+?N3$CJesAKJ4Qn9L8!C-x^}8{W7sF+lk$!V? zJa366z`|)VAXANCbw>Mgj211jqJguqMwVrj6;<++a$&0J(X|I>)z+~YO9Yuo<3-W>Zft1D;GeK^JdJw(6f_}}~KL8TVJe~M8nIaxCrB8-p= z<&qwf(UFTgM_XG`yvooc9Zg_Zn_gKjBn>*yt#0X7v|`8O!WzYE1E}JW)~~GN&nStcMxTOt6-h;}~|SqJD}iq~zhm!iEdVr{P3*7~khPLc7iX zEgfYE6ACsoOyzLsI=n{*r@>#yz1z7Hk?Tm;XkN;I>tX7>K%|ULl|X$WuS4##W5|v| zV#}&4*3USKzDU%mC?GpNV6Y-llw0{ZfIZq=!O@uN=;rNRqhRHj#~3P9GLc)W*5bZe zfET<=WVqN#@o8BhR6eI-FicZ68kolf!@Mn~Ty=hSiSdsr#b>GE018umiVSc)dy!xiT$aw~ds~ z-;NVm4AmqNy-s(r*wylY5ub)X-IslTelnp`G1seuVeF%5bxlYKA(ci<(rb1S_0RU{ zVSRJdwgt}OI#i~)JXWM$eh{^npI>8#K%jRYs?+uL(u>=kCnYdNDG%&yso5^iYqE1* zO{7)B#;UDL<$8uS6CC~JS7VQwM4?ek)%SNS0L4;8^o(hPw!Deo&58S3s>n87YFq6b zaNex;_<8}djz`6e(PD`hGJNYIBO4Bj?C$rhc?HoGT&-|jlj&iyQ(?z9v&m#h_f_)t z<~Ng4G&16b!?+$~GV_wWpFIPLEW^WjW^kgSJqg>033c@Y$(D68_zsVD3S|cFdBI%l z?pfhM-a1=rU~`&4X>3hq6}}d4-3^DU)~&L-Bh2bEln^TO8m-b#ruzEsKT`R{_k7-Im)to#It!q_a_7I$bpl)V<(m8_xC{Zvs#aoZa`^DKuZI zgfg3PaPYK7VnjceArC) zeREn5&&8t<=sEB7xjI{kl0F! z&%=qaU0nzeV{{&_gTwwhiSNBHHSVbHyt;;gxjcUm_rsrWUL)C|V&e!&oV;6f-2=f> zrh;&hkagBvaoDfw^!1^o^AEQ)2LFgN;%bIKUPD$9Jbj33cp=t@e?3r2rf8fGM;}5@ z%X}J=3_2V~E`#Up6N#=qm}@1>+Fr(q%#6iv(c$dm)!#v3D&2lViQ9LD1} z8}&7)v|tKSK;E-wKtDh7Apmez&QugE>xjs>`L~E}5G@dHsbTbWqqy(>cVUEpm2V+5 zq-WlbxlwzG2HdM2gFek>H-1$iUSd^QTyiw^w6*8X z6GhNg60Vi2k=qZ@sD{KvHS&sRc7HM4;m*#bj<@l;P9d(FKp~VS3&s}ZY5kD0GbgW4 z(B)i&`9;rcY-|)y=7&m1qgwj44@@0mPbANb37_1+{W2eBzR{n)iN69N@?QMNEzuu5W`<9M@k6>uQG-lJ=v8%d{( zvphV^WkkVZ=L;3Ob)a`5Fj&>QBKyaS|DWv^U_MFoBpF>@q^HF5Id8uDu?0-vEKdtxg;Yhom4q6{pH!O=h!v#VOw-{b*0e) z$a%ji`QaepI_e@YDxRZ=W+nVSGY?9{b7#MoTonjSZ-2CyCw|G{Z~?uB*DPTHUHmIqTx>oh7+DBPY;Je zFAAPF=gBG4%g~Xh+U*w?5CRB_#l$TCw7Z)lg!ek4|0L9wPXP?Sy^bxO1d=LqLxq5` z0$20q_oz!d!>;mKw#3Mg|M}h;_i|0a{`0}%_>{wcCqO{&F`YpN06?W{*VjL`k(2T=U>p5=jV0`J~! zoA1PG{?kkR|Fic0_m1SN%&2KG$r}&E3b=0$j3vx`}g3X4m=Pf3i50aMK$oRcwtr&GRP1HB>$KjX16{+qipt8`(JvnIk6*7{QA&7( zCiZD(9a}qG^wT>lgLR5-U?u5#sz!U;qFX;mC?vnIO($ZZIECx%NF*=e{$jm|CU7jM z!bN9yR&3E$HCd}S(q3qGvC)K6pxQkTy|~uNFh#R{_TS`kggM54e1r@|^@On@3Qagx zL&$}^RVIY>+Vmv6v!@%)8~k$k(x)41eeQg^oH#m%my(gb`MW#Pn)(s{qq?^Yiu2LB zd`SWXclSUbxVr`k1P$))?ykYzz0t<4A-KB}++7pAaSJ+4{%78M>fU$mhp9SKHQ&0P zPt8+M?7i1ozlHHs!Ms4igU_qZ7^34I@WHmtJYmACsoqwBm7^{A3KE{=T}wsQ<=f!b zeGLAz%;_fW+JJ-K@6fL@d{fp)94g>z(WT=>KwFay2?C0FqvYtW1G!fuz8sa=Y@*!ovU6%0_ae#9x`BVp^hJ*pX zFj^WO<&-jkDd)^SG7^RP<#yS{*Mij#_5Bnea%NdiStC!sAyRm@Ss6gXYX?gTR=Z0A zp$&88c-c}lBQeM=8@>sDReu^_&2i1Ic4hiKTCB$>Nt|YR@b*iZdC=ArZ+7SJtOc2p z!0NDtF-6$?ftnv3F?5=3GZVnW>9R&pdVbO&6-M2Bf+yS0*uD8P5pFC)a|U8N|8Q2| zS0u=?`l`PNvs3%9VBsC^C2>v~uli1H7P%zc>sNOOVsVwpE>)$E4n(BBm(W+=!x$~( zNLED%Z!vQD!$=w$FKLoNh~TW~#nOP9Z!Rk3|6!>>NAaqNdlSk&<9i!MfMWSaN`qeD z^OUsOKIt@i-3(!*6eV#uOatyHr=SsM5#tgzR5a;)*H9-VVOz>x++h8HD?{M0#oxxdK~>+ zaQPBHDi0YS6eRE@&aHddXdv=v4K3fd_xX6B;?6Q{G^kZL!|$Th+qDU}+`a*M1-wfi zpA;qqzgb0Ak;*!MIJB}#O<{~T=RnrOs~DUjMu*0tY^G%BzL#s@@!|n)oF1bGzg3cF z=ke~pc$O>f)NA2z?qhi0B=(9v6j{`BrJf$jx zX2B@ofT7@)EZVa!aCdI+$*uHi`QLQ7Mdv;=e4743#ego zAzds{zpUcmr7j{gF+v_|rXhXN5&B+-dc$Mx7O(TNfgrpv!G%@%baGw^sLY%PbHOef zx1iPHa{6Mr!k8mC_@-*ai-yfBeP8iS7dY^)8AA{!CmC8&F6oD|JgwBdze(DMVf3bw z{W|J6BIH!qY_L(GNkAT10lWFRlR`*h4}MIvBziR7Jrc!$_)>Q;C7kx-Cq)Mpsn5a+ z4ut>Y-sqq~UpiEbl|3?ZlM)8M;7Q%&FybpDYf7WHN4TR<53pAMY>C|8a7I*5^X-NUmG)4miE%!Xape?xE|N&x5SMFbwRS)(X0Hgz>^d&27-2w{9p znY?O(nx)cuZHzuF5R{<14zVmB${&q4@4l~u&`d9f4={t?_HSuoFJTi`uf(&-M+BZ< ziE-C8Upy8Z{oeLs5n^Af+L@O`M|5p{dweEkv`+2GQ@OCQ=e2>k6#^2>J`d019Pn)G z81kRpTd-kFrxE|q(Lfy9hE!Fb%*3gKjy?a1LC7s}-XiiLkG|lBF03^^q?c@|30BdW z+7i&ehf~(y%}YR0CeRetN6^f%T*#SLa8PSL_s+JTBKHY`oI}VFeMa(m5A|^R!Qpg%*Dwh@{;~E7VkCg=P zUu>o|O>J|3>BnWm-4SQH!W*o~!whopZ$DDK)y_&P6z{-suS9JolqyK|PvPQ<*7a(N zFrg%c@7F3(w9vsEI!`Z4bLQPHfdiyd6tDFbWaVQ-G>z zjy1?$Pb8BVo36s6$#61mT59}+0L9kgdfP8C8s&M{wP3;w!pH*AZS_84!v~7B6Z^+N zsy|Djdr2^y2fxU1xVIzMqk3iWSlN)a-)~LQC^TqbKvVsSj?$&oF8P%G6{xg3h}m8s zSbUPTTQp|3SU52SA7~p=B4Oy=4a@(ArC4Nt`q(fVTjwk7^6F|0NSMDD=T$*lq!h`q zk;aM5ITtIpX}BO57)T^NUkuyhF-Qx^gUD@N7_p^&cygANb1oIFCR3Ps&D2`I4xJ^o zIigB1(GiKvtxnZLW6A##FRPvAMjz4Ic;L%NCm>EJ%5> z@U+Dm{ntD6Kw{%n#XL7-(ba<#5Xw=JH6XjTZgNWt(~w095NBqfddhXqeCJ#X)MRGD}EUYo9xaAry-QVs^r3du%c+u&u z;U_5m=v(Xwn=PJ0PZIwsY(;%BQQW9nb#fNc|4u(MmfI>dwPR^=kpwL^84JEIr`K!h zPqnCbY3>{W@^rlV;);e^TS1Ezl8{{3jkVoH7}$#SCl2NLU&W>$0%|p%B>?oKb;YM} z9sg_)$%AeJH67_D!#>=1{gY>V8z5-raZ?_;$)*NnANp306+-U&cWt3-A@dbVnDOTS z4gumHg~XoAmF_EZmGyP@1@}VH6gO-XeC3!0|M4ZM72|Xw9@MHum@a&!RQX!1L5$;m zJ02m*A0h@VHvUJT}8)^j* zcRbszQM{=Ph3HEbm0u;J47daG3WrBS8<=#}Pj`N3!KSO%wbzfLt*ON6dC{%F3m{cH?W0u(s$|3J>r%D-eh&xk3_GbslTB=ghh3^GPB)l5cI! zM42r?nu{|DKQ62k(e*mF2f`wZa=dbIahw`ZedV`QOte=u+kOE6#O%gxc+ugqz_v2D zV|k+-jLKk$#saoRSjz|KxFNM)v6~3o=dYS}-aS9&)dwaw-BdIZVulb=W^(Q-+li+3 zU8pf`Iqb9^P3Qbz)I8lDpXQlWmy^487&#{{&GlFpx$2V+;j?+b*ZKrIGFHPVpq9Ga0sM^u ztbrP^c<|e`?i|zR(~MN*6w_(EiM!VJwq^aVoRedZL*OWa^3e^fZ~{+xUPGCn-_6Vv zXT2vPxzh>SbS$;#otR zK~AEz?{6r!0R((b6oyqcqlKHreu^yLsrg?>?fXXRtdonc`qsJvM-12THzTvEs@h)s z+8s9f2KE99?;kEx5U1&kMP8{n)#8Qh;_&?fqZTJ5z>5YYDJHDEz>R;dkhQLmeO;^l za3~sO*!Jga^0w+}-`|dv;+s8Iow%rx%+S%Xx|-@b-ln}&A&!n&k8(9`a}&^SNh23i zPZ2;j+O(h`#m7g0R>P=7Jx{ijOe{Ti+z3?SF<%dEm1#_fwKXOsp}K zOo1`&>txxNSWyUYjyKvc2m+kzf6^5$Wn39}&{~)y{S1n2oYKV)J-2$WE!gv6s>xtA z*|)G!48fa(C~X}IR(_8LIpex&W}$4ZgER~bfDu^F$05fLhi))pK%n`UdiOANhy&M! zy1yMt?>nM(t&C3dTYEhyLV)b!K?;EC^tXWcCb_xd;ZCYM4H`ik8jtdg;ZS^bj+wOd z9Dc2Iw5g4Uu8Y2VqETuoKUObQ20>jyGS^fW$2HjdobS z>2y#+PfvFKIfZ7X6qzAW5neuj42sVmr|cMx>?bV{Heci7tdwYtCVHA!x~Q)VV3Ar$ zRJ)zjQHDC=(LG?E(0rA&Cu!LjWd50*2#m`;4mLtyubYo6=~|>%hr<-aKmJ*7p4i7~ zkR$DE%(%cazrFSFgpM;KFQwTZs;P6X{4V{o0X1K4Pi1PjWiQ^w8`Q*X*-s`~@hBI? zw{BS-v`A^@i@`%5dHEVNi~s}?eaz7T)1QFWkvYjQV5MCl9zB9VN z=M5F7w(9^xZzA;y?Jo2s=V?8&c8;0od4_=Kr951#W|pfFr%lDG6Qtz*KzNEN)`M^5 zF^~Ae?XVyrNNh@DlwyF)8ZI4Rnb&{Uy@A=WYmJk4k}^KC8_dShtaY=$=1(nNimW-U zm9BK$Q<(v8d2}dI9rRanjIXBNI=s3g1bEiIN z(jQxuGt!p{(ZrNb9+lW}lkb{hv4c_)Xm{{$LM8bTk73{%Fb@O zLeg$I!{3L4T0Pf}#|R$3|5axm>(zCoT$X~6NpREe`z}@R0NHyscK$hhr#pP)46C)K0;3{DcCfQ}E59w4b3*-wHwBNtWQX;zlQ|;5}Z403K9_ zu=ca9+Mxp^i7&{v?)2DuspK+oK#Io{9zafD4MmvNG;cwR%whlGZYkHwiH0BoMIN=Z zNVdO7w$-d<`YaMcPq2L&9J2%I>U-=%3Xq`O_yZ$Ye)|^~6XV<`zg%9v6Ub>Z&Pl%d z{=k(JixyQH@t#D|djt@l*K#-2af$n>!ugQBfS_7B^_27u^In6;h9Ja;gnzOI{40m) z*#V(&`Y?yB{X)EFmLqyC=LsLuqKZPrNU21c$#K*zZRQ5sUy{E7?yS4xnF*|%2u-M( zaYp2Nv;A~V$z$?-?&+^y z3yt&_m@~;zn(B^Xwh#Z!>~oB9({7;@%J}Vz5bERkw2Gs{dE~baB6suqD~rWF%oZMq znPL}m!p5kKRq|P1vk}03rf&C`c%nh{t?W$j#-6F{)_)O;>HQqlZr`txPVkhJb>DUZ zJZ;uch$2`ZBWK9GrK z=EzCm;#%ew8FcaHW@jhI_*duErP-p~IkUUJh3D*_4^m+=GwQpRuyUS4_tyIN6yG}H zxW2{uv_1X8MO%aFA|6koQC7U5px(OQIsk4_cJE27oo1DC~? zAbW2;huV~xRADVVLu0^ZFkiM|^dKsD|7 zcaB3|FjCj)=6}xd6{dKRb};y=B1JpSKWN?IaJ&-H-$Iz}{~<2=|8FAke?@42p8Nz6 zjSwG!-=Ls=z4<@-?M?=45&w91|g3B+P7+= zH8OI8gef27Vh9FpzlYOy>U67ExD3XPb)>A~bIl$c?Hm!2dxwCDQ9V0OqA_Kp_2XJG zF-bv{o<{dq2Z^0#=B&uim!wCIZ8P%iWPb)i2?ya=g@vi9BQkSQGQZ~biNHg{LM6he zjWtx!H{xS<5LxzhvWPj3ryI%Hk)3UQb?D>S5*pYf`OEc^#*w60N(B6>I&0JVmuzVx7 ziroLoNbi7n{6w>V{NuYzU4szIjjKFc!9OG}%y|K2yOx;Z;s{M6EhR-Jda1Y`0>%85 z0?2dCl2|e2P8m=8Sw=@}$_d*E;rMg}M1$Vj(V={R`EjnnWOwG1eOqnz&igDmCdb%s zE_|F!jVLngtH!SXeA{MK*OBPtm$N*1kG7@iANp!|P)hkGLh&{9Myg9foex-SiY z^5`TIsaiH&*y!Pg3u^`sFnXeG#o|0ow7MI_9wX)%*9|^=9o)HGJRy z$*^iycAiD}8yns%t2WNM?d|p)Cxf2Pdm_SVn=OO7# zkclg+rEIpAdpK!w%Vj0gesmV_`gpAIWu0Ie{x}d^TyAna!4#5WTw9bk{jA&iN{~l9SkNWO7>)Z^FH-u9@tWzD$&C>;+g)XYo0_HVq>Twb~>hzNni7Tk;uQ<+W%QADF z=Y!|mKeio1amoE&2xu_Q%`oH_r)H-y$&l>GEudu2gRachdKm{O7tYq>Rn6k>?MG2D zSm-ry1&j^pHrA>F=)FmM^$Wz0Z_XIQeJ!2_yotNtYw6OhIVSXD+7LEYiaRwk{c@Es z51caGm=sC8VO7ou>QQf4E0NFj=*=;GNoXo*nGTsp@>*VFkoOFf{C3*Wq7`%}IDg~< zp@wJgvfz91&jbR8fhXeX*@$dC8?#19zdGOASvfCXSGJyvj029mU%C?X z=m*XvL$ySNFyq@Kq`t&&ET}svPVMOP(&=ibLl~E!8om%3nFg76!B!Q2@F5`guA6|R zqP-bVtzr!wDhovszZ^A2-}xrRNlHjWDjTSFI>I#*ADc6Nd6Zv4Vv%BwXTWT9pzV4c z5719@6J_8fK)PJ--f(_UR|wudRI*HTnZ5m_^d~K7B{eOFGXs%8F49UY$ABQ&Sxo0= zJ#~NO+;7T^Y)QGQ;}cAQ`bYkEk_4>aZTi?|E2q(kLaiKWmWh>3(<&rV;7G-qdH^q$ zYXxoPsX_mrY*n(8@OX&sNAGE3F#q$Yoxg~E&sc9s9`4_69hV!CX8L6H5If;Nw}5m! zy@wRQ7p6kI9HxrPn47)eA>j62bjaRiWJ)r9*($ymG4PeIUs}u}j#OM{QM%0<*4z)| z1Rw$e7t+H1-ZUoRkT``Ye=l7@Ul<=rA44Fdr~Q9aR>-q4&(Ti4jqSg1 z{8h}V!&T&0@ek0UUGPx_^|Vv&yiS7-zm&3$HB-rXQ46FbsgQIL9<)DLe|fc`M8Qqu@xXwZ{LLX_5{ndP)u`{sL+HUoNtMu_;r$riLQT@ zsf#8LA66EmirOF+q02=4)3$v@o@6P-k_RPO{=RiLn#=+N&+pq;YYyT34k3TlNVcR? zf$ea6BWEoW5H-39^{T>?sq$ODU$Jp| z2p>5er@P{c6^z|KP_E;oNXOekthkR3SC{;C<)f!zZ=G8ddatAuC5l4DZ5aEkb;;m_ z47YOi`mIiTCi~lMHenhUj6s^@S<=^Y4h+HHb50_`qbJU&1fM5}GD?cGA&&m>&q*2k9&2pe-?z1+O%U($3tm+T;E>3IU;skYB55I+pBKKQC!pC6c-aW>f<32xuS-{}>`a z@Q8$mI2WwYymSECw-PE}&meL^T0q@)#du+F`$xfP)z-N&-O6;Mx2a4QIs2DvxqF)XPn%7yRJ*lvU3H5{2Fc5B*cYjw^ zxIgi_h?z8`;^s?6oC_}i$p))|CEV&?NyON4pK0pbc<5+VBXUbA}Hocq2XW;fH=#;{BwqSIc{N~R?t z9((QL&y8jd9xS7dinSaXA6`rtt<1+su|7i5-V+i#G7Qtfbt_3{aT0+8_sjaW9Bf-$ zV)Nkcq!J>|AJF)>`RZ;;qE9F9PKLAVfM5Yy9F<%k4x4WQfs6gN)_^+ATQ9lgO?b-S zV1e@!BA53f!gZIg#Rf^Q#RlFf1ycF=D@sJ23BIn6=p@R{7t1O7)g@Q*rRk>a=J17_ z7N7p07r|#Qkix!?!}EeFEWq&O622#cmb1DBNFos|x6uS3x^!xtOUbG7$F;Q;_1&M* z@f|`_nwU(|{kizbRQ1A~#>`-|Uz3O>&{TZWOi1gRDVC4p3U*$+@*sHH*71@(fr@l= zUl!{q?`vrJbbNTlIH{F%U!#-dffPGaN4~00$IV`$d7f_$#?~A-65aWlp+Jjn67_bp zB>3ob_4kOjO`+m~i!;Na#J4Q*qk^j9wcwjAc#@Zt*%kJ+woDLUe|R*Ao>{s0+^{j|psWVN3l_5^PYv zx5{M-cc^Et*tkbKhf7gbIcPbc%<*MgWnA7Yk`9@Yf&m-!z~&i1Pa{{OUT(8qG>JDK zUz7~GHtjw|WcCh_l<_+=@cYPFLBaniJm>9iRVzynk&RT&uZ=7-(a@M6Cq#Dh60IqD ziYhISccjM#KK~2LeJph5+9(mBteKsC8stBIuEll>9elJv4|<46T^tJvgDX}inU`J`P<68!1qIgcho=PKc{gWV(pHA`IKILL>P(t zRe__{s~th6-s|#0KuL@-V-eqwy>K`>n0t?mTTQ&miCW-ny1J06q>Sy!^U=n01lF(O z1EymOtIiJ8WHKyvhC{dw+^gfV%$#=XGf_&BIA6=mSvkKGZ$tw$-zjXL$^h*SA0Fw*lp{K7a3 z=H_{*CayohKV2hWn%P=Mqz<)#f>&`Iij*H_Z48uSiLMFP&HB70u1AO%9L5!z?G4n`B*TuYN*&j~JJAQZcAC=J+LO73{c~N!B7hbrf~-v`7qn(2 zv7eIa5D>vGcSZ|3!|oLHSGkzA@G&KIx#lb?C^#P6nOWN9A7Qyu-J&v%S1rIAJ1$|H553&i{#zZ7#R# z{%xzgJ~sBGQf2um(m(-wv^WFfxPz3Tqtk1Lg{|C%`5FEV*o#iKBA>f6@8Mo(2zm%E zS-aiLiX{4 zXQ>aJ@0N+94&7%#Lvz2nVE^Ni@po#<*mU2hH;^+CwOM;-Fq1JfT#+mX%Xx*xy6jUQP1=5~Oc;5CYZ+04gsK6O~eRrD=K@Q)Yd?XOis zX+HKodl1a1V6KULlt&N|rbL6BUN$}#t@SPr~APE!&pa!gEf z{g@akP)q4hB1j_so0!~EY-Z}~Vy%cIN@c26-y#=&5(U{UO^jy)A*NA~&x*rh=*cyS zZRNOUKvXA#%C;t7r@2k}QIPu_WR+b@W5&YXO7gYs-&NgXIZt@dce1H;rbth#HuU+~ zZR=e1qHZ=b!p^~?YBz*G|Aj;Lp`~qT98)J|>?g=l8)+=+(h34{iZ3m0$@*)`%(XWj zUNNjz8gsR2bC}kiqwzEg-9V#gI@c_CjsA^nG#n%zmmn5e79~s{`bZPT?%XFCoE=#4&(m^Z0T9 zmCgH@t*^5rpCKD5bctueT#V9L0&xj+7@$O7!}!>Su%|_N95q;$P)fU94IoZJ)Lr0% z;ZWj-e>OUr5{EA^Bihq-JbItR<*NlldiTWuXzu(|4Gu?^1u8xt8u>&Bqi`NVZ~%3w z;ApsBt*rQVTQiV~cK$z0skA}n=fbwB*)+N*AlWQgAxnWd(lBqCeemV?#)-_bS+le` z8=$WLbTMGAM1Im`h*Y}HhNQUPUF%=bL!{r|YKfR)pvr26_ECOBh(&%damb2LZ!4G| zEUmR9pc_RfbrDI_&=6SN9KMFUAng2^O1Cu^HP?CVy@&THkA^HU-!fq+Ww5>8oof%5 znq~k{U0v>{-a;&!SG09bTC7**%~u{M?&;{YgvC)yrVx9hS@X381e()K;r%;r0FW5L z$eIPg9?(MwGOA0&y;xLQMOzVj7?<-CTtbPNx=U-Jx>J%k$o^PEhSHcWA6KiXIr=ZW zTTE0U>S+^rmA5lXf7VeIF( z3*_T`Hy7?4gxbIeNVfLmwJ8lm_pkHT!B533aO!15g%~^!HAZEOH#uUe{owOea<=zWv4q;8+~iwppR_vrCQ9`Z zK)5PvzD#T|V0_hq|8j%@vN~;#>gTtgwtDi&Y(5E~3x1JA`xtH{7NE*7-*p#rpP%69 z@I;UB&HO+|rrlj_Vw-MGft^E}60+ss)JOl_f42GFhTeanva}eU{2Z%yY&v_2aAh(h z6)`Wbz4DyDb?l9df$rkQEdakpa_L44e;yHat`&vS0e5Owa>PB{&$RCp5jichECx_T z`dy%qI{8A4%#r6(#fMcpkcBn1DR~9?bs)|!gf7(KLnX3ayt++hLkQtL0ewTi}l&waohe|=R?ZGRKm9}b)oC)Iv{dW}Y$YtE_&7b8% z)IS>2EqNbRJen7WOoNY3aG_RN#lK2YFFtIjzTAG^L~;#;i#q)tDmhVI=XYgAD%Quqy=aQjU)oTjtc{^O6UN?8dQwW!Y`k*e^Zwp$_fiPyTPS6pY3T;uEf} zx!Z<_F$^*5+^@|n#TG=dy6qR{TZljL%voQWw7SQjhr-uqS5SDh5lJCQ3MLraTPr@j z{jN2bo8z^1L?W8!Coc;EW-hH#}kz_|jCM;k6p3baXfZ3k5F& zRU6144B`xIP_8?f*oUv7{yuGG2x{=i5b32CWen-fQ8JZRFVkv0R)`WgkEya;$54Cl z&*jl8)>?FAX3#B8Yu$&mh)ETB^0u?&2dhT-Pr_*qvL}8d;}dCo3hY=t|!h z0)8obs>im--DoH#wLBID)+!uf$s)74|NWT<*8HAU^x^{^*`I<1Y`_b*K#ZV(dGpzA zA)UOCs8BrZ$tC=x$B@5SQ7c_Ug}n?)?3mA_BUsax^mX-h>zJ*M)&DiNri~{gSWeHV zv+tW#UQc2AA1wcXRJi>-gH)DIVs%T%J!t51@0Dmbl{EB52fP=RDw`|G|D&JlR#)Da zwWOk|xQB2JRQeE20Di8PC+mU#N5#!Rq#-Cvj0*O#nG61M9cY2#&vS6rrv!|r81K?o zbWUJE!8vi6ZS+*QQ3uM7$<6ekfUMxpQoVjvl$M;L3^LDB(9X~7!s*}#9a+Lab$I@T z&VOHlfEE=NH0=uS^lP7f%lb|tZF0Wz%{py>Q8SyHWg_bZQhy?`#%E}8cb_AMC4OTo z^jXSQ3pyseCgPE2DHXNYuvkkqxjyp^2qmz$P*~ZFLGj) IBKiUU1IPWVSO5S3 literal 0 HcmV?d00001 diff --git a/reports/design-system/torch-dashboard-pilot-conclusion.png b/reports/design-system/torch-dashboard-pilot-conclusion.png new file mode 100644 index 0000000000000000000000000000000000000000..e73c45cb1aec4495ffc386f436e907ad3d1dfd4a GIT binary patch literal 63066 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 adx9c&J`Slw8InbA1VuSD*;47Jf&U9GvntsD literal 0 HcmV?d00001 diff --git a/reports/design-system/torch-dashboard-pilot-review.png b/reports/design-system/torch-dashboard-pilot-review.png new file mode 100644 index 0000000000000000000000000000000000000000..5d4fcac944695cf2579be22b9a48a0ae6188950e GIT binary patch literal 2934 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 literal 0 HcmV?d00001 diff --git a/schemas/domain-proposal.schema.json b/schemas/domain-proposal.schema.json index 47a8e04..6196878 100644 --- a/schemas/domain-proposal.schema.json +++ b/schemas/domain-proposal.schema.json @@ -1,17 +1,18 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", - "required": ["schema", "generatedAt", "repository", "review", "specifications", "domains", "collisions", "checks", "resources", "schedules", "architecture"], + "required": ["schema", "generatedAt", "repository", "review", "specifications", "domains", "collisions", "checks", "resources", "schedules", "architecture", "organization_assessment"], "properties": { "schema": { "const": "torch.dev/domain-proposal/v1alpha1" }, "generatedAt": { "type": "string", "minLength": 1 }, "repository": { "type": "object", - "required": ["root", "initialCommit", "head"], + "required": ["root", "initialCommit", "head", "workingTreeFingerprint"], "properties": { "root": { "type": "string" }, "initialCommit": { "type": "string" }, - "head": { "type": "string" } + "head": { "type": "string" }, + "workingTreeFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" } }, "additionalProperties": false }, @@ -55,6 +56,67 @@ "checks": { "type": "array", "items": { "type": "object" } }, "resources": { "type": "array", "items": { "type": "object" } }, "schedules": { "type": "array", "items": { "type": "object" } }, + "runtime_wake_budget": { + "type": "object", + "required": ["max_invocations_per_day"], + "properties": { + "max_invocations_per_day": { "type": "integer", "minimum": 1, "maximum": 24 } + }, + "additionalProperties": false + }, + "organization_assessment": { + "type": "object", + "required": ["shape", "rationale", "evidence", "authority_boundaries", "proposed_coordination_roles", "direct_peer_communication"], + "properties": { + "shape": { "enum": ["flat", "coordination_roles"] }, + "rationale": { "type": "string", "minLength": 1 }, + "evidence": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }, + "authority_boundaries": { + "type": "object", + "required": ["implementation_ownership", "coordination_responsibility", "fleet_operations_authority", "project_priority_authority", "independent_review_authority", "owner_only_decisions"], + "properties": { + "implementation_ownership": { "type": "string", "minLength": 1 }, + "coordination_responsibility": { "type": "string", "minLength": 1 }, + "fleet_operations_authority": { "type": "string", "minLength": 1 }, + "project_priority_authority": { "type": "string", "minLength": 1 }, + "independent_review_authority": { "type": "string", "minLength": 1 }, + "owner_only_decisions": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } + }, + "additionalProperties": false + }, + "proposed_coordination_roles": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "title", "coordinates_domains", "integrated_outcome", "decision_scope", "owner_escalations", "evidence", "owns_implementation_paths", "has_owner_approval_authority"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }, + "title": { "type": "string", "minLength": 1 }, + "coordinates_domains": { "type": "array", "minItems": 2, "uniqueItems": true, "items": { "type": "string" } }, + "integrated_outcome": { "type": "string", "minLength": 1 }, + "decision_scope": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }, + "owner_escalations": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }, + "evidence": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }, + "owns_implementation_paths": { "const": false }, + "has_owner_approval_authority": { "const": false } + }, + "additionalProperties": false + } + }, + "direct_peer_communication": { "const": "preserved" } + }, + "allOf": [ + { + "if": { "properties": { "shape": { "const": "flat" } } }, + "then": { "properties": { "proposed_coordination_roles": { "maxItems": 0 } } } + }, + { + "if": { "properties": { "shape": { "const": "coordination_roles" } } }, + "then": { "properties": { "proposed_coordination_roles": { "minItems": 1 } } } + } + ], + "additionalProperties": false + }, "architecture": { "type": "object" } }, "additionalProperties": false diff --git a/scripts/condition-browser-command.mjs b/scripts/condition-browser-command.mjs new file mode 100644 index 0000000..bb8fe15 --- /dev/null +++ b/scripts/condition-browser-command.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; + +// Condition and measurement subprocesses both contact the same browser service. +const [base, operation] = process.argv.slice(2); +assert.ok(['probe', 'measure'].includes(operation)); +const response = await fetch(`${base}/${operation}`, { + method: operation === 'measure' ? 'POST' : 'GET', signal: AbortSignal.timeout(10_000), +}); +assert.equal(response.status, 200); +const report = await response.json(); +if (operation === 'probe') { + assert.equal(report.subject.commit, process.env.TORCH_CHECK_COMMIT); + assert.equal(report.subject.inputDigest, process.env.TORCH_CHECK_INPUT_DIGEST); +} +console.log(JSON.stringify(report)); diff --git a/scripts/condition-browser-runtime.mjs b/scripts/condition-browser-runtime.mjs new file mode 100644 index 0000000..6b19ab5 --- /dev/null +++ b/scripts/condition-browser-runtime.mjs @@ -0,0 +1,86 @@ +import { once } from 'node:events'; +import { readFileSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { join } from 'node:path'; +import { chromium } from '@playwright/test'; + +// Explicit qualification service: one page survives all probe/measurement +// commands. It is not a provider, launcher, or installed TORCH service. +let subject; +let page; +let browser; +let measurements = 0; +let mode; +const server = createServer(async (request, response) => { + try { + if (request.url === '/page' && subject) { + response.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-store' }); + response.end(readFileSync(join(subject.root, 'dist', 'index.html'))); + return; + } + if (!page) { response.writeHead(503).end(); return; } + let result; + if (request.url === '/probe' && request.method === 'GET') { + const observed = await page.evaluate(async () => { + const before = window.simulationClock; + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + return { + commit: document.querySelector('[data-commit]').dataset.commit, + runtimeId: window.runtimeId, + conditions: { 'clock-moving': window.simulationClock > before, + visible: document.visibilityState === 'visible', + build: document.querySelector('[data-build]').dataset.build }, + }; + }); + result = { schema: 'torch.dev/check-conditions/v1alpha1', + subject: { commit: observed.commit, inputDigest: subject.inputDigest }, + runtimeId: observed.runtimeId, conditions: observed.conditions }; + } else if (request.url === '/measure' && request.method === 'POST') { + measurements += 1; + result = await page.evaluate((freeze) => { + if (freeze) window.clockFrozen = true; + return { measurement: 'green', runtimeId: window.runtimeId }; + }, mode === 'drift'); + if (mode === 'replaced') await page.reload(); + } else if (request.url === '/status' && request.method === 'GET') { + result = { measurements, runtimeId: await page.evaluate(() => window.runtimeId) }; + } else { response.writeHead(404).end(); return; } + response.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); + response.end(JSON.stringify(result)); + } catch (error) { + response.writeHead(500).end(JSON.stringify({ error: error.message })); + } +}); + +async function close() { + await browser?.close(); + await new Promise((resolve) => server.close(resolve)); +} + +process.on('message', async (message) => { + try { + if (message.action === 'load') { + subject = message.subject; + mode = message.mode; + browser = await chromium.launch({ headless: true }); + page = await browser.newPage(); + const base = `http://127.0.0.1:${server.address().port}`; + await page.route('**/*', (route) => route.request().url().startsWith(`${base}/`) + ? route.continue() : route.abort()); + await page.goto(`${base}/page`); + if (mode === 'frozen') await page.evaluate(() => { window.clockFrozen = true; }); + process.send({ event: 'loaded', runtimeId: await page.evaluate(() => window.runtimeId) }); + } else if (message.action === 'close') { + await close(); + process.disconnect(); + } + } catch (error) { + process.send({ event: 'error', message: error.message }); + await close(); + process.exitCode = 1; + process.disconnect(); + } +}); +server.listen(0, '127.0.0.1'); +await once(server, 'listening'); +process.send({ event: 'listening', base: `http://127.0.0.1:${server.address().port}` }); diff --git a/scripts/frozen-browser-harness.mjs b/scripts/frozen-browser-harness.mjs new file mode 100644 index 0000000..af72d86 --- /dev/null +++ b/scripts/frozen-browser-harness.mjs @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import { once } from 'node:events'; +import { readFileSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { join } from 'node:path'; +import { chromium } from '@playwright/test'; + +// Qualification harness, not a bundled product dependency. Run from the captured +// input root; module dependencies resolve from this explicitly configured harness. +const expectedBuild = process.argv[2]; +assert.ok(expectedBuild, 'Expected build must be declared by the scenario'); +const root = process.cwd(); +const server = createServer((request, response) => { + if (request.url !== '/') { + response.writeHead(404).end(); + return; + } + response.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-store' }); + response.end(readFileSync(join(root, 'dist', 'index.html'))); +}); +let browser; +try { + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + const base = `http://127.0.0.1:${server.address().port}`; + browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + const errors = []; + page.on('pageerror', (error) => errors.push(error.message)); + await page.route('**/*', (route) => route.request().url().startsWith(`${base}/`) + ? route.continue() : route.abort()); + await page.goto(base); + const observation = await page.evaluate(async () => { + const before = performance.now(); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + return { + build: document.querySelector('[data-build]').dataset.build, + commit: document.querySelector('[data-commit]').dataset.commit, + visible: document.visibilityState === 'visible', + browserClockMoving: performance.now() > before, + }; + }); + assert.deepEqual(errors, []); + assert.equal(observation.build, expectedBuild); + assert.equal(observation.commit, process.env.TORCH_CHECK_COMMIT); + assert.equal(observation.visible, true); + assert.equal(observation.browserClockMoving, true); + assert.match(process.env.TORCH_CHECK_INPUT_DIGEST, /^[a-f0-9]{64}$/); + console.log(JSON.stringify({ ...observation, inputDigest: process.env.TORCH_CHECK_INPUT_DIGEST })); +} finally { + await browser?.close(); + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); +} diff --git a/scripts/interrupted-browser-harness.mjs b/scripts/interrupted-browser-harness.mjs new file mode 100644 index 0000000..1fa5b4a --- /dev/null +++ b/scripts/interrupted-browser-harness.mjs @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; +import { once } from 'node:events'; +import { readFileSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { join } from 'node:path'; +import { chromium } from '@playwright/test'; + +const token = randomUUID(); +let browserServer; +let page; +let finish; +const stopped = new Promise((resolve) => { finish = resolve; }); +const server = createServer(async (request, response) => { + try { + if (request.url === '/' && request.method === 'GET') { + response.writeHead(200, { 'Content-Type': 'text/html' }); + response.end(readFileSync(join(process.cwd(), 'dist', 'index.html'))); + } else if (request.url === '/status' && page) { + const observed = await page.evaluate(() => ({ build: document.querySelector('[data-build]').dataset.build, + commit: document.querySelector('[data-commit]').dataset.commit, visible: document.visibilityState === 'visible' })); + response.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify(observed)); + } else if (request.url === '/stop' && request.method === 'POST' && request.headers['x-fixture-token'] === token) { + const processHandle = browserServer.process(); + const exited = once(processHandle, 'exit'); + await browserServer.close(); + await exited; + response.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ stopped: true })); + server.close(finish); + } else response.writeHead(404).end(); + } catch (error) { response.writeHead(500).end(JSON.stringify({ error: error.message })); } +}); +try { + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + const base = `http://127.0.0.1:${server.address().port}`; + browserServer = await chromium.launchServer({ headless: true }); + const browser = await chromium.connect(browserServer.wsEndpoint()); + page = await browser.newPage(); + await page.route('**/*', (route) => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort()); + await page.goto(base); + const commit = await page.locator('[data-commit]').getAttribute('data-commit'); + assert.equal(commit, process.env.TORCH_CHECK_COMMIT); + const browserPid = browserServer.process().pid; + const stat = readFileSync(`/proc/${browserPid}/stat`, 'utf8'); + const browserStart = stat.slice(stat.lastIndexOf(')') + 2).trim().split(/\s+/)[19]; + const executorStat = readFileSync(`/proc/${process.pid}/stat`, 'utf8'); + const executorStart = executorStat.slice(executorStat.lastIndexOf(')') + 2).trim().split(/\s+/)[19]; + console.log(JSON.stringify({ event: 'browser-ready', executorPid: process.pid, executorStart, browserPid, browserStart, base, token })); + await stopped; +} finally { + await browserServer?.close(); + if (server.listening) await new Promise((resolve) => server.close(resolve)); +} diff --git a/scripts/interrupted-check-runner.mjs b/scripts/interrupted-check-runner.mjs new file mode 100644 index 0000000..617d439 --- /dev/null +++ b/scripts/interrupted-check-runner.mjs @@ -0,0 +1,15 @@ +import { spawnSync } from 'node:child_process'; +import { openControlPlane } from '../src/control-plane/service.mjs'; +import { CheckService } from '../src/checks/service.mjs'; +import { ResourceService } from '../src/resources/service.mjs'; + +const [repositoryRoot, preparedId, areaId] = process.argv.slice(2); +const control = openControlPlane({ repositoryRoot }); +try { + const resources = new ResourceService({ repositoryRoot, controlPlane: control }); + const checks = new CheckService({ repositoryRoot, controlPlane: control, resourceService: resources, + executor: (command, args, options) => spawnSync(command, args, { + ...options, stdio: ['ignore', 'inherit', 'pipe'], + }) }); + checks.runPrepared({ preparedId, areaId }); +} finally { control.close(); } diff --git a/scripts/test-dashboard-demo.mjs b/scripts/test-dashboard-demo.mjs new file mode 100644 index 0000000..770593a --- /dev/null +++ b/scripts/test-dashboard-demo.mjs @@ -0,0 +1,247 @@ +import assert from 'node:assert/strict'; +import { mkdirSync } from 'node:fs'; +import { once } from 'node:events'; +import { chromium } from '@playwright/test'; +import { createConsoleServer } from '../src/console/server.mjs'; + +const server = createConsoleServer(); +server.listen(0, '127.0.0.1'); +await once(server, 'listening'); +const base = 'http://127.0.0.1:' + server.address().port; +let browser; +try { + browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, reducedMotion: 'reduce' }); + const apiRequests = []; + const errors = []; + await context.route('**/api/**', (route) => { + apiRequests.push(route.request().url()); + return route.abort(); + }); + context.on('page', (page) => page.on('pageerror', (error) => errors.push(error.message))); + const landing = await context.newPage(); + await landing.goto(base); + const popup = context.waitForEvent('page'); + await landing.getByRole('link', { name: 'View dashboard demo', exact: true }).click(); + const page = await popup; + await page.waitForLoadState(); + await page.locator('#project-name').filter({ hasText: 'Northstar' }).waitFor(); + assert.equal(await page.title(), 'TORCH Dashboard'); + assert.equal(await page.locator('.agent-row').count(), 6); + assert.equal(await page.locator('.task-ticket').count(), 5); + assert.equal(await page.getByRole('region', { name: 'Queue', exact: true }).locator('.task-ticket').count(), 2); + assert.equal(await page.locator('#demo-banner').isVisible(), true); + assert.match(await page.locator('#operation-outcomes').innerText(), /Outcome unknown — review required/); + assert.equal(await page.locator('#operation-outcomes button').count(), 0); + await page.locator('#operation-outcomes .check-evidence').last().locator('summary').click(); + assert.match(await page.locator('#operation-outcomes').innerText(), /transient failure.*effects unknown/s); + mkdirSync('reports/design-system', { recursive: true }); + await page.locator('#operation-outcomes').screenshot({ path: 'reports/design-system/torch-dashboard-operation-outcomes.png' }); + await page.locator('#activity-review > summary').click(); + assert.match(await page.locator('#activity-review').innerText(), /4 days without a named commit/); + assert.match(await page.locator('#activity-review').innerText(), /waiting may be expected/); + mkdirSync('reports/design-system', { recursive: true }); + await page.locator('#activity-review').screenshot({ path: 'reports/design-system/torch-dashboard-activity-review.png' }); + assert.match(await page.locator('#check-list').innerText(), /Running — completion unconfirmed/); + await page.locator('#check-list .check-evidence').filter({ hasText: 'interrupted-browser' }).locator('summary').click(); + assert.match(await page.locator('#check-list').innerText(), /Abandoned — not qualified/); + assert.match(await page.locator('#check-list').innerText(), /Captured-input cleanup pending/); + assert.match(await page.locator('#check-list').innerText(), /owner-attested, not independently verified/); + assert.equal(await page.locator('#check-list button').count(), 0); + await page.locator('#check-list .check-evidence').filter({ hasText: 'recovery-scenario' }).locator('summary').click(); + assert.match(await page.locator('#check-list').innerText(), /clockMoving.*true/s); + assert.match(await page.locator('#check-list').innerText(), /not independent sensor truth/); + mkdirSync('reports/design-system', { recursive: true }); + await page.locator('#check-list').screenshot({ path: 'reports/design-system/torch-dashboard-check-evidence.png' }); + assert.match(await page.locator('#owner-briefing').innerText(), /Choose the project home layout/); + assert.match(await page.locator('#owner-briefing').innerText(), /not independent live verification/); + await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click(); + assert.match(await page.locator('#owner-briefing').innerText(), /Landed — not necessarily deployed/); + mkdirSync('reports/design-system', { recursive: true }); + await page.locator('#owner-briefing').screenshot({ path: 'reports/design-system/torch-dashboard-owner-briefing-desktop.png' }); + assert.match(await page.locator('#attention-list').innerText(), /manager launch needs inspection/); + await page.locator('#attention-list a[href="#manager-wakes"]').click(); + assert.equal(await page.locator('#manager-wakes').isVisible(), true); + assert.match(await page.locator('#manager-wake-list').innerText(), /Launch outcome unknown/); + assert.match(await page.locator('#manager-wake-list').innerText(), /sample-wake-01/); + assert.equal(await page.locator('#manager-wake-list button').count(), 0, + 'uncertain launches must not offer automatic restart or unlocking'); + const pilotPanel = page.locator('.organization-proposal').filter({ hasText: 'Coordination pilot' }).locator('.pilot-review-history'); + await pilotPanel.locator(':scope > summary').click(); + assert.match(await pilotPanel.innerText(), /Recommendation: inconclusive/); + assert.match(await pilotPanel.innerText(), /reviewer-reported, not independently verified/); + const comparison = pilotPanel.getByRole('table', { name: 'Baseline compared with pilot observations' }); + assert.equal(await comparison.locator('tbody td').nth(0).innerText(), '3'); + assert.equal(await comparison.locator('tbody td').nth(1).innerText(), 'About 2'); + assert.equal(await comparison.locator('tbody td').nth(2).innerText(), 'qualitative'); + await pilotPanel.getByText('Criterion outcomes and evidence references', { exact: true }).click(); + assert.match(await pilotPanel.innerText(), /Reduce cross-domain waiting.*unknown/); + assert.match(await pilotPanel.innerText(), /Sample peer handoffs/); + assert.equal(await pilotPanel.locator('button').count(), 0, 'review recommendations cannot change the organization'); + const conclusion = page.locator('[data-organization-review][data-type="hierarchy-conclusion"]'); + await conclusion.locator('..').locator(':scope > summary').click(); + await conclusion.locator('[name="action"]').selectOption('adopt'); + await conclusion.locator('[name="reason"]').fill('Keep the coordination role if the pilot supports it.'); + await conclusion.locator('[data-review-preview]').click(); + await conclusion.locator('[data-review-preview-panel]').waitFor(); + assert.equal(await conclusion.locator('[data-review-confirm]').isVisible(), false, + 'qualitative inconclusive evidence does not permit adoption'); + await conclusion.locator('[data-review-edit]').click(); + await conclusion.locator('[name="action"]').selectOption('reverse'); + await conclusion.locator('[name="reason"]').fill('Restore direct reporting while collecting better evidence.'); + await conclusion.locator('[data-review-preview]').click(); + await conclusion.locator('[data-review-confirm]').waitFor(); + await conclusion.getByText('Exact proposal and evidence in this decision', { exact: true }).click(); + assert.match(await conclusion.locator('[data-review-scope]').innerText(), /schedulesRetained/); + mkdirSync('reports/design-system', { recursive: true }); + await conclusion.screenshot({ path: 'reports/design-system/torch-dashboard-pilot-conclusion.png' }); + await conclusion.locator('[data-review-confirm]').click(); + await page.locator('#organization-action-status').filter({ hasText: 'recorded as reversed' }).waitFor(); + assert.equal(await page.locator('.agent-row').count(), 6); + + const ticket = page.locator('.task-ticket').filter({ hasText: 'API-08' }); + await ticket.locator('.priority-change > summary').click(); + await ticket.locator('[name="priority"]').selectOption('urgent'); + await ticket.locator('[name="reason"]').fill('Needed for the beta'); + await ticket.getByRole('button', { name: 'Review change', exact: true }).click(); + await ticket.locator('[data-priority-confirm]').waitFor(); + assert.equal(await ticket.locator('.priority').textContent(), 'high'); + await ticket.locator('[data-priority-confirm]').click(); + await ticket.locator('.priority-urgent').waitFor(); + + await page.locator('.owner-request-composer > summary').click(); + await page.locator('#owner-request-recipient').selectOption('qa'); + await page.locator('#owner-request-body').fill('Please review the home layout.'); + await page.locator('[data-owner-request-preview]').click(); + await page.locator('[data-owner-request-confirm]').waitFor(); + await page.locator('[data-owner-request-confirm]').click(); + await page.locator('#message-list .conversation-item').filter({ hasText: 'Please review the home layout.' }).waitFor(); + + const feedback = page.locator('.artifact-feedback-form').first(); + await feedback.locator('textarea').fill('Give active work more room.'); + await feedback.getByRole('button', { name: 'Review feedback' }).click(); + await feedback.locator('[data-feedback-confirm]').waitFor(); + await feedback.locator('[data-feedback-confirm]').click(); + await page.locator('.artifact-feedback blockquote').filter({ hasText: 'Give active work more room.' }).waitFor(); + + await page.locator('.runtime-profile-tools > summary').click(); + await page.locator('#runtime-profile-area').selectOption('interface'); + await page.locator('#runtime-profile-runtime').selectOption('claude'); + assert.equal(await page.locator('#runtime-profile-model').inputValue(), 'sonnet'); + await page.locator('[data-runtime-profile-preview]').click(); + await page.locator('[data-runtime-profile-confirm]').waitFor(); + await page.locator('[data-runtime-profile-confirm]').click(); + await page.locator('.agent-row').filter({ hasText: 'interface · claude / sonnet' }).waitFor(); + + await page.locator('.approval-decision-form [name="decision"]').selectOption('approved'); + await page.locator('[data-approval-preview]').click(); + await page.locator('[data-approval-confirm]').waitFor(); + await page.locator('[data-approval-confirm]').click(); + await page.getByText('No open approval requests.', { exact: true }).waitFor(); + assert.match(await page.locator('#owner-briefing').innerText(), /Choose the project home layout/, + 'published briefing is historical even when live approval state changes'); + + await page.locator('.organization-review > summary').click(); + await page.locator('[data-organization-review] [name="action"]').selectOption('approve'); + await page.locator('[data-review-preview]').click(); + await page.locator('[data-review-confirm]').waitFor(); + assert.doesNotMatch(await page.locator('[data-review-preview-panel]').innerText(), /undefined/); + await page.locator('[data-review-confirm]').click(); + await page.locator('#organization-action-status').filter({ hasText: 'recorded as approved' }).waitFor(); + + mkdirSync('reports/design-system', { recursive: true }); + await pilotPanel.locator(':scope > summary').click(); + await pilotPanel.screenshot({ path: 'reports/design-system/torch-dashboard-pilot-review.png' }); + await page.locator('#manager-wakes').screenshot({ path: 'reports/design-system/torch-dashboard-manager-wakes.png' }); + await page.evaluate(() => scrollTo(0, 0)); + await page.screenshot({ path: 'reports/design-system/torch-dashboard-demo-desktop.png' }); + await page.setViewportSize({ width: 390, height: 844 }); + // Let responsive styles and layout finish before measuring the new viewport. + await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + await page.evaluate(() => scrollTo(0, 0)); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, + JSON.stringify(await page.evaluate(() => ({ viewport: innerWidth, documentWidth: document.documentElement.scrollWidth, + bodyWidth: document.body.scrollWidth, overflow: [...document.querySelectorAll('body *')].map((element) => { + const bounds = element.getBoundingClientRect(); + return { tag: element.tagName, id: element.id, className: element.className, right: bounds.right, width: bounds.width }; + }).filter((element) => element.right > innerWidth + 1).slice(-30) })))); + await page.screenshot({ path: 'reports/design-system/torch-dashboard-demo-mobile.png' }); + await page.locator('.console-rail a[href="#owner-briefing"]').click(); + const briefingHeading = await page.locator('#owner-briefing-title').boundingBox(); + const stickyRail = await page.locator('.console-rail').boundingBox(); + assert.ok(briefingHeading.y >= stickyRail.y + stickyRail.height, 'briefing anchor must clear sticky mobile navigation'); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false); + await page.locator('#owner-briefing').screenshot({ path: 'reports/design-system/torch-dashboard-owner-briefing-mobile.png' }); + await page.locator('#owner-briefing .briefing-detail').first().locator('summary').click(); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, 'expanded briefing evidence must fit mobile width'); + await page.locator('#reset-demo').click(); + await page.locator('#approval-request-count').filter({ hasText: '1 open' }).waitFor(); + assert.equal(await page.locator('.priority-urgent').count(), 0); + assert.equal(await page.locator('.agent-row').count(), 6); + assert.deepEqual(errors, []); + const livePage = await context.newPage(); + await livePage.clock.install({ time: new Date('2026-09-30T12:00:00Z') }); + await livePage.goto(base + '/console.html?demo=1'); + await livePage.locator('#live-refresh-status[data-generation="1"]').waitFor(); + for (const model of ['external-model-1', 'external-model-2']) { + await livePage.evaluate(async (model) => { + const payload = { areaId: document.querySelector('#runtime-profile-area').value, runtime: 'codex', model, reasoning: 'high' }; + const preview = await globalThis.TorchConsoleDemo.request('/api/runtime-profiles/preview', payload); + await globalThis.TorchConsoleDemo.request('/api/runtime-profiles', { ...payload, token: preview.token, planHash: preview.planHash }); + }, model); + await livePage.locator('#refresh-console').click(); + assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), model, 'clean editor must track successive external model changes'); + } + await livePage.reload(); + await livePage.locator('#live-refresh-status[data-generation="1"]').waitFor(); + await livePage.locator('.runtime-profile-tools > summary').click(); + await livePage.locator('#runtime-profile-model').fill('owner-draft-model'); + await livePage.locator('#task-create-form').locator('..').locator(':scope > summary').click(); + await livePage.locator('#task-create-form [name="title"]').fill('My unfinished task'); + await livePage.locator('.approval-decision-form [name="decision"]').selectOption('approved'); + await livePage.locator('[data-approval-preview]').click(); + await livePage.locator('[data-approval-confirm]').waitFor(); + const previewToken = await livePage.locator('.approval-decision-form').getAttribute('data-preview-token'); + await livePage.evaluate(() => { globalThis.oldMessageNode = document.querySelector('#message-list p'); }); + await livePage.clock.runFor(15_000); + await livePage.locator('#live-refresh-status[data-generation="2"]').waitFor(); + assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), 'owner-draft-model'); + assert.equal(await livePage.locator('#task-create-form [name="title"]').inputValue(), 'My unfinished task'); + assert.equal(await livePage.locator('.approval-decision-form').getAttribute('data-preview-token'), previewToken); + assert.equal(await livePage.evaluate(() => globalThis.oldMessageNode.isConnected), false, 'unprotected read-only panels must keep updating'); + assert.match(await livePage.locator('#live-refresh-status').innerText(), /Edited forms and active previews retained/); + await livePage.locator('#toggle-live-refresh').click(); + await livePage.clock.runFor(30_000); + assert.equal(await livePage.locator('#live-refresh-status').getAttribute('data-generation'), '2'); + await livePage.locator('#toggle-live-refresh').click(); + await livePage.locator('#live-refresh-status[data-generation="3"]').waitFor(); + await livePage.evaluate(() => Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'hidden' })); + await livePage.clock.runFor(30_000); + assert.equal(await livePage.locator('#live-refresh-status').getAttribute('data-generation'), '3', 'hidden tabs must not poll'); + await livePage.evaluate(() => { + delete document.visibilityState; + document.dispatchEvent(new Event('visibilitychange')); + }); + await livePage.locator('#live-refresh-status[data-generation="4"]').waitFor(); + await livePage.evaluate(() => dispatchEvent(new PageTransitionEvent('pagehide'))); + await livePage.clock.runFor(30_000); + assert.equal(await livePage.locator('#live-refresh-status').getAttribute('data-generation'), '4'); + await livePage.evaluate(() => dispatchEvent(new PageTransitionEvent('pageshow', { persisted: true }))); + await livePage.locator('#live-refresh-status[data-generation="5"]').waitFor(); + await livePage.clock.runFor(15_000); + await livePage.locator('#live-refresh-status[data-generation="6"]').waitFor(); + await livePage.locator('[data-approval-confirm]').click(); + await livePage.locator('#approval-request-count').filter({ hasText: '0 open' }).waitFor(); + assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), 'owner-draft-model', 'completing an approval must not clear another form'); + assert.equal(await livePage.locator('#task-create-form [name="title"]').inputValue(), 'My unfinished task'); + await livePage.locator('#refresh-console').click(); + assert.equal(await livePage.locator('#runtime-profile-model').inputValue(), 'owner-draft-model'); + await livePage.screenshot({ path: 'reports/design-system/torch-dashboard-live-refresh.png' }); + await livePage.close(); + assert.deepEqual(errors, []); + assert.deepEqual(apiRequests, [], 'demo must never contact live project APIs'); + console.log('PASS: real Console demo; separate tab; priority, messages, feedback, profiles, approval and organization review; reset; mobile layout; zero live API calls.'); +} finally { + await browser?.close(); + await new Promise((resolve) => server.close(resolve)); +} diff --git a/scripts/wait-owned-browser-group.py b/scripts/wait-owned-browser-group.py new file mode 100644 index 0000000..1cd4c17 --- /dev/null +++ b/scripts/wait-owned-browser-group.py @@ -0,0 +1,57 @@ +"""Explicit Linux qualification only; observe, never signal, one owned group.""" +import json +import os +import select +import sys +import time + +root = int(sys.argv[1]) +expected_start = sys.argv[2] + + +def record(pid): + with open(f"/proc/{pid}/stat", encoding="utf8") as stream: + value = stream.read() + fields = value[value.rfind(")") + 2:].split() + return fields + + +assert record(root)[19] == expected_start, "Browser PID identity changed" +assert os.getpgid(root) == root, "Browser must own an isolated process group" +handles = [] +executor = int(sys.argv[3]) +assert record(executor)[19] == sys.argv[4], "Harness PID identity changed" +handles.append(os.pidfd_open(executor)) +observed = 0 +for entry in os.scandir("/proc"): + if not entry.name.isdigit(): + continue + try: + fields = record(entry.name) + if int(fields[2]) != root or fields[0] in ("Z", "X"): + continue + observed += 1 + handles.append(os.pidfd_open(int(entry.name))) + except ProcessLookupError: + pass + except FileNotFoundError: + pass +assert observed > 1, "Qualification must include real browser descendants" +print(json.dumps({"event": "watching", "observed": observed}), flush=True) +deadline = time.monotonic() + 30 +remaining = set(handles) +while remaining: + ready, _, _ = select.select(list(remaining), [], [], max(0, deadline - time.monotonic())) + assert ready, "Owned browser group did not reach terminal state" + remaining.difference_update(ready) +for handle in handles: + os.close(handle) +for entry in os.scandir("/proc"): + if not entry.name.isdigit(): + continue + try: + fields = record(entry.name) + assert int(fields[2]) != root or fields[0] in ("Z", "X"), "Live group member survived shutdown" + except FileNotFoundError: + pass +print(json.dumps({"event": "terminal", "observed": observed}), flush=True) diff --git a/site/app.js b/site/app.js index c3eaab8..c387a1a 100644 --- a/site/app.js +++ b/site/app.js @@ -2,9 +2,10 @@ 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.', + 'Project evidence collected; no provider launched.', 'A fleet proposal is ready for owner review.', + 'Each session can use its selected runtime; peers keep direct links.', + 'Messages, tasks, status, and evidence share one coordination layer.', + 'Changes land only through authorized checks and serialized integration.', ]; let routeTimer; @@ -12,7 +13,7 @@ routeButton?.addEventListener('click', () => { clearInterval(routeTimer); steps.forEach((step) => step.classList.remove('is-active', 'is-complete')); routeButton.setAttribute('aria-pressed', 'true'); - routeButton.textContent = 'Routing…'; + routeButton.textContent = 'Following the flow…'; let index = 0; const advance = () => { steps.forEach((step, stepIndex) => { @@ -24,7 +25,7 @@ routeButton?.addEventListener('click', () => { if (index === steps.length) { clearInterval(routeTimer); routeButton.setAttribute('aria-pressed', 'false'); - routeButton.textContent = 'Run again'; + routeButton.textContent = 'Show it again'; } }; advance(); diff --git a/site/check-evidence.js b/site/check-evidence.js new file mode 100644 index 0000000..dc28afc --- /dev/null +++ b/site/check-evidence.js @@ -0,0 +1,40 @@ +(function () { + const text = (value) => String(value ?? '').slice(0, 4000).replace(/[&<>"']/g, + (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]); + const value = (input) => text(typeof input === 'object' ? JSON.stringify(input) : input); + function frozen(snapshot) { + if (!snapshot) return '

Frozen-input evidence not recorded.

'; + if (snapshot.unavailable) return '

Frozen-input evidence unreadable.

'; + return `

Frozen input commit ${text(snapshot.commit)}

+

Input digest ${text(snapshot.digest ?? snapshot.inputDigest)}

+

Copy strategy: ${text(snapshot.copyStrategy ?? 'not recorded')}. Recorded capture, not a new integrity check.

`; + } + function phase(label, observation) { + if (!observation) return `

${label}: not recorded.

`; + const entries = Object.entries(observation.report?.conditions ?? {}).slice(0, 100); + return `

${label}: ${observation.valid === true ? 'contract valid' : 'invalid or unknown'}

+ ${observation.reason ? `

${text(observation.reason)}

` : ''} +
${entries.map(([key, observed]) => `
${text(key)}
${value(observed)}
`).join('')}
`; + } + function render(checks = {}) { + const pending = (checks.prepared ?? []).filter((item) => ['prepared', 'running', 'abandoned'].includes(item.state)).slice(0, 40); + const receipts = (checks.receipts ?? []).slice(0, 40); + return `

Recorded check evidence. Running records do not prove a live executor; interrupted runs need review, not automatic rerun.

+ ${pending.map((item) => `
${text(item.checkId)} · ${text(item.areaId)} · ${item.state === 'running' ? 'Running — completion unconfirmed' : item.state === 'abandoned' ? 'Abandoned — not qualified' : 'Prepared — awaiting execution'} +

Preparation ${text(item.id)} · ${text(item.createdAt)}

${frozen(item.snapshot)} + ${item.state === 'abandoned' ? `

${item.recovery?.cleanupStatus === 'completed' ? 'Captured-input cleanup completed (recorded).' : item.recovery?.cleanupStatus === 'pending' ? 'Captured-input cleanup pending. Owner review is required.' : 'Captured-input cleanup unconfirmed. Inspect the recovery record before acting.'}

+ ${item.recovery?.recordedAt ? `

Recovery recorded ${text(item.recovery.recordedAt)}

` : ''} + ${item.recovery?.cleanupReason ? `

Cleanup reason: ${text(item.recovery.cleanupReason)}

` : ''} +

Stopped executors: ${item.recovery?.stoppedExecutors === 'owner-attested' ? 'owner-attested, not independently verified descendant termination' : 'unconfirmed'}. + Recovery does not release resource leases or create a check pass. Use the owner-confirmed CLI recovery workflow; this view does not recover or rerun work.

` : ''}
`).join('')} + ${receipts.length ? receipts.map((receipt) => `
${text(receipt.checkId)} · ${text(receipt.areaId)} · ${text(receipt.result)} +

Commit ${text(receipt.commit)} · ${text(receipt.finishedAt)}

+ ${receipt.invalidReason ? `

Qualification invalid: ${text(receipt.invalidReason)}

` : ''}${frozen(receipt.snapshot)} + ${receipt.conditions ? (receipt.conditions.unavailable ? '

Condition evidence unreadable.

' : `

Conditions are project-reported pre/post observations, not independent sensor truth or continuous monitoring.

+ ${phase('Before measurement', receipt.conditions.before)}${phase('After measurement', receipt.conditions.after)}`) + : '

No condition observations recorded; this is not proof that runtime conditions were verified.

'}
`).join('') + : '

No exact-commit check receipts.

'} + ${checks.coverage ? `

${text(checks.coverage)}

` : ''}`; + } + globalThis.TorchCheckEvidence = Object.freeze({ render }); +})(); diff --git a/site/console.html b/site/console.html index cde20b2..bba7b05 100644 --- a/site/console.html +++ b/site/console.html @@ -4,41 +4,295 @@ - TORCH Fleet Console + + TORCH Local Fleet Console + + + + + + + + + -
+ +
- T TORCH -

Local Fleet Console

- + TORCH +
Loading project…Reading repository
+
Waiting for snapshot
-
-
-

Project

Loading…
-

Commit

—
-

Fleet health

Observing
-

Context locality

Unmeasured
-
- -
-

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
-
+
+ + +
+ +

Waiting for the first snapshot.

+ +
+
+

Project pulse

Work in motion

Observing
+

TORCH is gathering the latest repository and fleet evidence.

+
+
Repository—
+
Fleet0 identities
+
ContextUnmeasured
+
RecoverabilityUnknown
+
+
+ +
+ +
+

Owner briefing

Published evidence
+

Reading the latest local publication…

+
+ +
+

Approvals and reporting lines

Flow watch

Owner decisions gated
+

See which teams need attention and who owns each decision. A configured cadence does not confirm that a system timer is installed.

+
+
+

Manager check-ins

Not available
+
+
+
+

Open approvals

Not available
+
+

Only the named approver can decide. Owner-addressed requests have a review-and-confirm flow; requests assigned to an AI stay with that AI.

+
+
+
+ +
+

Authoritative backlog

Work board

0 active
+
+
+ + + + + + + + + +
+
+ + + + + Saved in this browser for this project; filters never change tasks. +
+

+

New work remains proposed and unassigned until the Session Manager triages it. Priority changes never reassign a task or advance its state.

+

Loading backlog views…

+
+
+ Create a task proposal +

Describe the outcome and any domain suggestions. The Session Manager still owns triage and assignment; this form never starts an agent.

+
+
+ + +
+ + + +
+
+ + +
+

These are routing hints, not assignments. Leave empty for Session Manager triage.

+ + +
+

+ +
+
+
+
Activity review
+ + +
+ +
+

Derived from the single backlog

Feature and milestone progress

Task states only · gates remain separate
+

Progress is calculated from each linked task's current state. It does not mean checks passed, integration landed, or a release is ready.

+
+
+ +
+

Persistent identities

Fleet and ownership

Session state from latest observation
+
+

Sessions

One context per identity
+
+ Change a next-launch profile +

Choose which local agent and model an identity will use next time it starts. Running sessions are not changed or restarted.

+
+
+ + + + + +
+

Install a reviewed Fleet before changing profiles.

+ +
+
+ +
+

Ownership

Implementation stays with specialists
+
+
+ +
+

Current approved structure

Organization map

No active graph
+
+
+
+ +
+

Owner review

Organization proposals

0 pending
+ +
+
+ +
+

Durable coordination

Conversations

0 unacknowledged
+
+ Request work from an agent +

This adds one durable inbox message. It will not create a task, change ownership, or wake/start the agent.

+
+
+ + +
+ +
+ +

Install a reviewed Fleet before sending owner requests.

+
+ +
+
+ +
+
+ +
+

Provenance first

Evidence and review

Exact commit receipts
+
+

Checks

Passes are commit-bound
+ +
+
+ +
+

Separate authority gates

Integration and release

Integration is not deployment
+
+

Integration

FIFO queue · source commit
+

Delivery

Release readiness
+
+
+
+ +
+ Operations, resources, and decisions +
+

Worktrees

Branches and recoverability
+

Resources

Shared capacity
+

Schedules

Declared lifetimes
+
+

Manager launches

Not available
+

Unknown launches block another scheduled wake. Offline presence does not prove that the agent process stopped.

+
+
+
+ Manage the persistent timer +

This timer runs configured system schedules, not AI sessions by itself. Review the schedules and exact unit files before setup. A schedule with provider wake enabled may invoke its selected local agent.

+
Loading installed timer state…
+ +

+ + +
+
+

Providers

Configured capabilities
+

Decisions

Tracked project truth
+

Context locality

Measured, never assumed
+
+
+
TORCH observes local project state. It does not expose private model reasoning.Read-only snapshot
+
diff --git a/site/console.js b/site/console.js index f9c80fe..89e3f96 100644 --- a/site/console.js +++ b/site/console.js @@ -1,120 +1,1909 @@ const $ = (selector) => document.querySelector(selector); +const demoWorkspace = globalThis.TorchConsoleDemo; +const workViews = globalThis.TorchWorkViews; +const workProgress = globalThis.TorchWorkProgress; +const emptyWorkFilters = workViews?.emptyFilters() ?? { query: '', owner: '', state: '', priority: '', domain: '' }; +const workViewState = { initialized: false, key: null, storage: null, views: [], selectedId: '', warning: null }; +let runtimeProfileAgents = []; +let runtimeProfileAdapters = []; +let currentBacklog = []; +let currentBacklogHealth = []; +const refreshProtection = globalThis.TorchLiveRefresh.createProtection(document); +refreshProtection.remember(); +let renderingSnapshot = false; +let heldPanels = 0; -function empty(message) { - return `

${message}

`; -} - -function safe(value) { +const escapeHtml = (value) => { const element = document.createElement('span'); element.textContent = value ?? ''; return element.innerHTML; +}; + +function localPath(value) { + const path = String(value ?? ''); + return path.startsWith('/') && !path.startsWith('//') ? escapeHtml(path) : ''; +} + +function empty(message) { + return `

${escapeHtml(message)}

`; +} + +function setHtml(selector, content) { + const element = $(selector); + if (renderingSnapshot && refreshProtection.protects(element)) { heldPanels += 1; return; } + if (element) { + const open = new Set([...element.querySelectorAll('details[open]')].map((details) => details.querySelector(':scope > summary')?.textContent)); + element.innerHTML = content; + element.querySelectorAll('details').forEach((details) => { + if (open.has(details.querySelector(':scope > summary')?.textContent)) details.open = true; + }); + } +} + +function setText(selector, value) { + const element = $(selector); + if (element) element.textContent = value ?? ''; +} + +function shortCommit(value) { + return value ? String(value).slice(0, 9) : '—'; +} + +function ageLabel(value) { + if (!value) return 'time not recorded'; + const time = Date.parse(value); + if (!Number.isFinite(time)) return 'time unavailable'; + const minutes = Math.max(0, Math.floor((Date.now() - time) / 60_000)); + if (minutes < 1) return 'just now'; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} + +function taskCard(task, healthFindings = []) { + const affected = (task.affectedDomains ?? []).join(', '); + const dependencies = (task.dependencies ?? []).join(', '); + const acceptance = (task.acceptanceCriteria ?? []).map((criterion) => `
  • ${escapeHtml(criterion)}
  • `).join(''); + const detail = task.description || acceptance || task.observedAt; + const staleEvidence = healthFindings.find((finding) => finding.taskId === task.id + && ['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING'].includes(finding.code)); + return `
    +
    ${escapeHtml(task.id)}${escapeHtml(task.priority ?? 'normal')}
    +

    ${escapeHtml(task.title ?? 'Untitled task')}

    +

    ${escapeHtml(detail || 'No task description recorded.')}

    + ${staleEvidence ? `
    Evidence needs refresh

    ${escapeHtml(staleEvidence.recommendation)}

    ` : ''} +
    +
    Owner
    ${escapeHtml(task.owner ?? 'Unassigned')}
    + ${affected ? `
    Domain
    ${escapeHtml(affected)}
    ` : ''} + ${task.feature ? `
    Feature
    ${escapeHtml(task.feature)}
    ` : ''} + ${task.milestone ? `
    Milestone
    ${escapeHtml(task.milestone)}
    ` : ''} + ${dependencies ? `
    Depends on
    ${escapeHtml(dependencies)}
    ` : ''} + ${task.observedAt ? `
    Observed at
    ${escapeHtml(shortCommit(task.observedAt))}
    ` : ''} +
    + ${acceptance ? `
    Acceptance criteria
      ${acceptance}
    ` : ''} +
    + Owner: change priority +
    + + + +

    + +
    +
    +
    `; +} + +function organizationProposalCard({ type, proposal, title, state, id }) { + const pending = state === 'proposed'; + const pilotReady = type === 'hierarchy' && state === 'pilot-approved'; + const concluding = type === 'hierarchy' && state === 'piloting'; + const formType = concluding ? 'hierarchy-conclusion' : type; + const details = type === 'fleet' + ? { + changeType: proposal.changeType, + proposer: proposal.proposer, + targetDomain: proposal.domainId, + rationale: proposal.rationale, + expectedBenefit: proposal.expectedBenefit, + baseCommit: proposal.baseCommit, + evidence: proposal.evidence, + } + : { + proposer: proposal.proposer, + proposalRevision: proposal.revision, + rationale: proposal.proposal?.rationale, + observationWindow: proposal.proposal?.observation_window, + signals: proposal.proposal?.signals, + alternatives: proposal.proposal?.alternatives, + impact: proposal.proposal?.impact, + pilot: proposal.proposal?.pilot, + proposedOrganization: proposal.proposal?.graph, + baseCommit: proposal.baseCommit, + }; + const actions = concluding ? [['adopt', 'Adopt the pilot organization'], ['reverse', 'Restore prior relationships']] : type === 'fleet' + ? [['approve', 'Approve for activation review'], ['reject', 'Reject']] + : [['approve-pilot', 'Approve bounded pilot plan'], ['defer', 'Defer'], ['reject', 'Reject']]; + const review = pending || concluding ? ` +
    + ${concluding ? 'Review pilot conclusion' : 'Review owner decision'} +
    + + +
    + +

    +
    + +
    +
    ` : ''; + const activation = pilotReady ? ` +
    + Review and activate bounded pilot +
    + +

    +
    + +
    ` : ''; + const pilotReviews = type === 'hierarchy' ? ` +
    + Inspect pilot comparisons (${(proposal.pilotReviews ?? []).length}) + ${(proposal.pilotReviews ?? []).length ? proposal.pilotReviews.map((report) => ` +
    +

    Recommendation: ${escapeHtml(report.recommendation)}

    +

    ${escapeHtml(report.summary)}

    +

    ${escapeHtml(report.reviewedAt)} · ${report.window?.complete ? 'Full review window' : 'Interim review'} · Evidence is reviewer-reported, not independently verified.

    +
    + + + ${(report.comparisons ?? []).map((metric) => ` + + + `).join('')} +
    Baseline compared with pilot observations
    MetricBaselineObservedEvidence quality
    ${escapeHtml(metric.name)}${escapeHtml(metric.unit)}${escapeHtml(metric.baseline)}${escapeHtml(metric.observed)}${escapeHtml(metric.classification)}
    +
    Criterion outcomes and evidence references +
      ${['success', 'stop'].flatMap((group) => (report[group] ?? []).map((criterion) => ` +
    • ${group === 'success' ? 'Success' : 'Stop'}: ${escapeHtml(criterion.criterion)} — ${escapeHtml(criterion.outcome)} + ${(criterion.evidence ?? []).map(escapeHtml).join(', ')}
    • `)).join('')}
    +
      ${(report.comparisons ?? []).map((metric) => `
    • ${escapeHtml(metric.name)}: + ${(metric.evidence ?? []).map(escapeHtml).join(', ')}
    • `).join('')}
    + Review ${escapeHtml(report.id)} · Activation ${escapeHtml(shortCommit(report.activationCommit))} +
    +

    No organization change is made by this recommendation. Adoption or reversal requires a separate owner-confirmed conclusion plan.

    +
    `).join('') : empty('No pilot comparisons have been recorded.')} +
    ` : ''; + return `
    +
    ${type === 'fleet' ? 'Fleet change' : 'Hierarchy proposal'}

    ${escapeHtml(title ?? id)}

    ${escapeHtml(id)}
    ${escapeHtml(state)}
    +

    ${escapeHtml(type === 'fleet' ? (proposal.expectedBenefit ?? proposal.rationale ?? 'No summary provided.') : (proposal.proposal?.rationale ?? 'No rationale provided.'))}

    +
    Inspect proposal scope and evidence
    ${escapeHtml(JSON.stringify(details, null, 2))}
    + ${review} + ${activation} + ${pilotReviews} +
    `; +} + +function viewFiltersFromControls() { + return workViews?.normalizeFilters({ + query: $('#work-view-search')?.value, + owner: $('#work-view-owner')?.value, + state: $('#work-view-state')?.value, + priority: $('#work-view-priority')?.value, + domain: $('#work-view-domain')?.value, + feature: $('#work-view-feature')?.value, + milestone: $('#work-view-milestone')?.value, + }) ?? emptyWorkFilters; +} + +function setViewFilterOptions(selector, emptyLabel, values, selectedValue) { + const select = $(selector); + if (!select) return; + const options = [...new Set(values.filter((value) => typeof value === 'string' && value))].sort((a, b) => a.localeCompare(b)); + if (selectedValue && !options.includes(selectedValue)) options.push(selectedValue); + select.innerHTML = `${options.map((value) => { + const label = value.includes('_') ? value.replaceAll('_', ' ') : value; + return ``; + }).join('')}`; + select.value = selectedValue ?? ''; +} + +function selectedSavedView() { + return workViewState.views.find((view) => view.id === workViewState.selectedId) ?? null; +} + +function renderSavedViewPicker() { + const select = $('#work-view-select'); + if (!select) return; + select.innerHTML = `${workViewState.views.map((view) => + ``).join('')}`; + select.value = workViewState.selectedId; + const selected = selectedSavedView(); + const update = $('#work-view-update'); + const remove = $('#work-view-delete'); + const name = $('#work-view-name'); + if (update) update.hidden = !selected; + if (remove) remove.hidden = !selected; + if (name && selected && !name.dataset.edited) name.value = selected.name; + if (name && !selected && !name.dataset.edited) name.value = ''; + if (workViewState.warning) setText('#work-view-status', workViewState.warning); +} + +function initializeSavedViews(snapshot) { + const identity = snapshot.project?.id ?? snapshot.project?.root + ?? snapshot.repository?.root ?? snapshot.project?.name ?? snapshot.repository?.name; + let key; + try { key = workViews?.storageKey(identity) ?? null; } catch { key = null; } + if (workViewState.initialized && key === workViewState.key) return; + workViewState.initialized = true; + workViewState.key = key; + workViewState.selectedId = ''; + workViewState.storage = null; + workViewState.views = []; + workViewState.warning = null; + try { workViewState.storage = demoWorkspace?.storage ?? globalThis.localStorage; } catch { + workViewState.warning = 'Saved views are unavailable in this browser; the backlog remains unchanged.'; + } + if (workViewState.storage && key && workViews) { + const result = workViews.readSavedViews(workViewState.storage, key); + workViewState.views = result.views; + workViewState.warning = result.warning; + } + const name = $('#work-view-name'); + if (name) delete name.dataset.edited; + renderSavedViewPicker(); +} + +function renderWorkViewFilters(tasks = [], filters = viewFiltersFromControls()) { + setViewFilterOptions('#work-view-owner', 'Anyone', tasks.map((task) => task.owner ?? 'Unassigned'), filters.owner); + setViewFilterOptions('#work-view-state', 'Any state', tasks.map((task) => task.state), filters.state); + setViewFilterOptions('#work-view-priority', 'Any priority', tasks.map((task) => task.priority ?? 'normal'), filters.priority); + setViewFilterOptions('#work-view-domain', 'Any domain', tasks.flatMap((task) => task.affectedDomains ?? []), filters.domain); + setViewFilterOptions('#work-view-feature', 'Any feature', tasks.map((task) => task.feature).filter(Boolean), filters.feature); + setViewFilterOptions('#work-view-milestone', 'Any milestone', tasks.map((task) => task.milestone).filter(Boolean), filters.milestone); +} + +function renderBacklog(tasks = [], healthFindings = []) { + currentBacklog = tasks; + currentBacklogHealth = healthFindings; + const active = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state)); + renderWorkViewFilters(active); + const filters = viewFiltersFromControls(); + const visible = workViews?.filterTasks(active, filters) ?? active; + const selected = selectedSavedView(); + setText('#task-count', `${visible.length} of ${active.length} active`); + setText('#work-view-summary', `Showing ${visible.length} of ${active.length} active tasks${selected ? ` · ${selected.name}` : ''}.`); + const lanes = [ + { title: 'Queue', states: ['proposed', 'ready'] }, + { title: 'Assigned', states: ['assigned'] }, + { title: 'In progress', states: ['in_progress'] }, + { title: 'Blocked', states: ['blocked'] }, + { title: 'Verification', states: ['verification', 'ready_to_integrate'] }, + ]; + const known = new Set(lanes.flatMap((lane) => lane.states)); + const unknownStates = [...new Set(visible.map((task) => task.state).filter((state) => !known.has(state)))]; + if (unknownStates.length) lanes.push({ title: 'Other', states: unknownStates }); + setHtml('#backlog-board', lanes.map((lane) => { + const items = visible.filter((task) => lane.states.includes(task.state)); + return `
    +

    ${escapeHtml(lane.title)}

    ${items.length}
    +
    ${items.length ? items.map((task) => taskCard(task, healthFindings)).join('') : empty(visible.length ? `No ${lane.title.toLowerCase()} work.` : 'No active tasks match these filters.')}
    +
    `; + }).join('')); +} + +function renderTaskCreateOptions(agents, tasks, installed) { + const form = $('#task-create-form'); + if (renderingSnapshot && refreshProtection.protects(form)) { heldPanels += 1; return; } + const fields = form?.querySelector('[data-task-create-fields]'); + const domainSelect = form?.querySelector('[name="affectedDomains"]'); + const dependencySelect = form?.querySelector('[name="dependencies"]'); + if (!form || !fields || !domainSelect || !dependencySelect) return; + const selectedDomains = new Set([...domainSelect.selectedOptions].map((option) => option.value)); + const selectedDependencies = new Set([...dependencySelect.selectedOptions].map((option) => option.value)); + const domains = agents.filter((agent) => agent.areaId && agent.areaId !== 'session-manager') + .sort((left, right) => left.areaId.localeCompare(right.areaId)); + domainSelect.innerHTML = domains.length ? domains.map((agent) => + ``).join('') + : ''; + const active = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state)); + dependencySelect.innerHTML = active.length ? active.map((task) => + ``).join('') + : ''; + fields.disabled = !installed; + if (!installed) { + taskCreateStatus(form, 'Task proposals are available after this repository is installed as a Fleet. No project state was changed.'); + } +} + +function renderOwnerRequestOptions(agents, tasks, installed) { + const form = $('#owner-request-form'); + if (renderingSnapshot && refreshProtection.protects(form)) { heldPanels += 1; return; } + const recipientSelect = $('#owner-request-recipient'); + const taskSelect = $('#owner-request-task'); + const body = $('#owner-request-body'); + const preview = form?.querySelector('[data-owner-request-preview]'); + if (!form || !recipientSelect || !taskSelect || !body || !preview) return; + const selectedRecipient = recipientSelect.value; + const selectedTask = taskSelect.value; + const recipients = [...agents].filter((agent) => agent.areaId) + .sort((left, right) => (left.areaId === 'session-manager' ? -1 : right.areaId === 'session-manager' ? 1 : left.areaId.localeCompare(right.areaId))); + recipientSelect.innerHTML = recipients.length + ? `${recipients.map((agent) => ``).join('')}` + : ''; + const activeTasks = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state)); + taskSelect.innerHTML = `${activeTasks.map((task) => ``).join('')}`; + const enabled = installed && recipients.length > 0; + recipientSelect.disabled = !enabled; + taskSelect.disabled = !enabled; + body.disabled = !enabled; + preview.disabled = !enabled; + const status = form.querySelector('.owner-request-status'); + if (!enabled && status) { + status.textContent = installed ? 'No persistent identities are available in this Fleet.' : 'Install a reviewed Fleet before sending owner requests.'; + } else if (status && !status.textContent) { + status.textContent = 'A request waits in the selected inbox; it does not start the agent.'; + } +} + +function profileDescription(profile) { + return [profile?.runtime, profile?.model, profile?.reasoning].filter(Boolean).join(' / ') || 'Adapter defaults'; +} + +function runtimeProfileStatus(form, message, error = false) { + const status = form.querySelector('.runtime-profile-status'); + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); +} + +function runtimeProfileInputs(form, agent, runtime) { + const adapter = runtimeProfileAdapters.find((entry) => entry.name === runtime); + const selectedRuntime = runtime || agent?.runtime || ''; + form.querySelector('[name="runtime"]').value = selectedRuntime; + form.querySelector('[name="model"]').value = runtime === agent?.runtime + ? (agent?.model ?? adapter?.model ?? '') : (adapter?.model ?? ''); + form.querySelector('[name="reasoning"]').value = runtime === agent?.runtime + ? (agent?.reasoning ?? adapter?.reasoning ?? '') : (adapter?.reasoning ?? ''); + form.dataset.profileArea = agent?.areaId ?? ''; + form.dataset.profileRuntime = selectedRuntime; +} + +function renderRuntimeProfileEditor(agents, adapters, installed) { + const form = $('#runtime-profile-form'); + if (renderingSnapshot && refreshProtection.protects(form)) { heldPanels += 1; return; } + const areaSelect = form?.querySelector('[name="areaId"]'); + const runtimeSelect = form?.querySelector('[name="runtime"]'); + const previewButton = form?.querySelector('[data-runtime-profile-preview]'); + if (!form || !areaSelect || !runtimeSelect || !previewButton) return; + if (form.dataset.previewToken) return; + form.dataset.installed = String(installed); + runtimeProfileAgents = [...agents].filter((agent) => agent.areaId) + .sort((left, right) => (left.areaId === 'session-manager' ? -1 : right.areaId === 'session-manager' ? 1 : left.areaId.localeCompare(right.areaId))); + runtimeProfileAdapters = [...adapters]; + const priorArea = areaSelect.value; + areaSelect.innerHTML = runtimeProfileAgents.length + ? runtimeProfileAgents.map((agent) => ``).join('') + : ''; + const selectedAgent = runtimeProfileAgents.find((agent) => agent.areaId === priorArea) ?? runtimeProfileAgents[0]; + areaSelect.value = selectedAgent?.areaId ?? ''; + const currentRuntime = selectedAgent?.runtime ?? ''; + const runtimeIsListed = runtimeProfileAdapters.some((adapter) => adapter.name === currentRuntime); + const options = runtimeProfileAdapters.map((adapter) => ``); + if (currentRuntime && !runtimeIsListed) options.unshift(``); + runtimeSelect.innerHTML = options.length ? options.join('') : ''; + const selectedRuntime = currentRuntime; + runtimeSelect.value = selectedRuntime; + const enabled = Boolean(installed && selectedAgent && runtimeProfileAdapters.some((adapter) => adapter.available)); + for (const field of form.querySelectorAll('select, input')) field.disabled = !enabled; + previewButton.disabled = !enabled; + runtimeProfileInputs(form, selectedAgent, selectedRuntime); + runtimeProfileStatus(form, enabled + ? 'Changes apply to future launch plans. Review the exact profile before saving.' + : (installed ? 'No selectable runtime adapter is available for this Fleet.' : 'Install a reviewed Fleet before changing profiles.')); +} + +function clearRuntimeProfilePreview(form) { + delete form.dataset.previewToken; + delete form.dataset.planHash; + delete form.dataset.profilePayload; + for (const field of form.querySelectorAll('select, input')) field.disabled = false; + form.querySelector('[data-runtime-profile-preview]').hidden = false; + form.querySelector('[data-runtime-profile-confirm]').hidden = true; + form.querySelector('[data-runtime-profile-edit]').hidden = true; + form.querySelector('[data-runtime-profile-review]').hidden = true; + const enabled = form.dataset.installed === 'true' && runtimeProfileAgents.length > 0; + for (const field of form.querySelectorAll('select, input')) field.disabled = !enabled; + form.querySelector('[data-runtime-profile-preview]').disabled = !enabled; +} + +function initiativeColumn(title, groups) { + return `
    +

    ${escapeHtml(title)}

    ${groups.length} ${groups.length === 1 ? 'group' : 'groups'}
    + ${groups.length ? `
    ${groups.map((group) => { + const counts = group.counts; + const countCopy = `${counts.completed}/${group.active} active complete · ${counts.blocked} blocked · ${counts.review} in review`; + return `
    + ${escapeHtml(group.name)}${escapeHtml(group.status)}${escapeHtml(countCopy)} +

    ${group.tasks.length} linked ${group.tasks.length === 1 ? 'task' : 'tasks'} · canceled tasks are excluded from the completion denominator.

      ${group.tasks.map((task) => `
    • ${escapeHtml(task.id)} ${escapeHtml(task.title)}${escapeHtml(task.state)}${escapeHtml(task.owner ?? 'Unassigned')}
    • `).join('')}
    +
    `; + }).join('')}
    ` : empty(`No backlog work is linked to a ${title.toLowerCase().replace(/s$/, '')} yet.`)} +
    `; +} + +function renderInitiativeProgress(tasks = []) { + const rollup = workProgress?.summarizeInitiatives(tasks) ?? { features: [], milestones: [] }; + const hasGroups = rollup.features.length + rollup.milestones.length > 0; + const explanation = hasGroups ? '' + : empty('No feature or milestone labels are recorded on backlog items. The Session Manager can classify work; this view will derive progress from those same tasks.'); + setHtml('#initiative-rollup', `${explanation}
    ${initiativeColumn('Features', rollup.features)}${initiativeColumn('Milestones', rollup.milestones)}
    `); +} + +function roleLinks(roleIds, rolesById) { + if (!roleIds?.length) return 'None recorded'; + return roleIds.map((id) => { + const target = rolesById.get(id); + const title = target?.title ?? id; + return `${escapeHtml(title)}`; + }).join(''); +} + +function lifecycleIdentityTitle(identityId, agentsById) { + return agentsById.get(identityId)?.title ?? identityId; +} + +function lifecycleOrderList(order = [], agentsById, managersByIdentity = {}) { + if (!order.length) return empty('No session identities are available in this roster.'); + return `
      ${order.map((identityId, index) => { + const managerIds = managersByIdentity[identityId] ?? []; + const managerText = managerIds.length + ? `Reports to ${managerIds.map((managerId) => escapeHtml(lifecycleIdentityTitle(managerId, agentsById))).join(', ')}` + : 'No fleet manager dependency'; + return `
    1. ${escapeHtml(lifecycleIdentityTitle(identityId, agentsById))}${managerText}
    2. `; + }).join('')}
    `; +} + +function lifecycleBlockerText(blocker, agentsById) { + const area = lifecycleIdentityTitle(blocker.areaId, agentsById); + if (blocker.code === 'FLEET_STARTUP_ROLE_MISSING') return `${area} has no approved organization role.`; + if (blocker.code === 'FLEET_STARTUP_MANAGER_MISSING') { + return `${area} reports to ${lifecycleIdentityTitle(blocker.managerId, agentsById)}, which is not in the Fleet roster.`; + } + if (blocker.code === 'FLEET_STARTUP_IDENTITY_CYCLE') { + const identities = (blocker.areaIds ?? []).map((id) => lifecycleIdentityTitle(id, agentsById)); + return `Reporting cycle prevents a safe order: ${identities.join(', ')}.`; + } + return `Lifecycle order is blocked: ${blocker.code}.`; +} + +function renderLifecycleOrder(lifecycle, agentsById) { + if (!lifecycle) { + setHtml('#organization-lifecycle', empty('Lifecycle order is available after a Fleet is installed.')); + return; + } + const blockers = lifecycle.blockers ?? []; + const startup = lifecycleOrderList(lifecycle.startupOrder, agentsById, lifecycle.managerIdsByIdentity); + const shutdown = lifecycleOrderList(lifecycle.shutdownOrder, agentsById, lifecycle.managerIdsByIdentity); + const blockerMarkup = blockers.length + ? `
      ${blockers.map((blocker) => `
    • ${escapeHtml(lifecycleBlockerText(blocker, agentsById))}
    • `).join('')}
    ` + : '

    No hierarchy blockers. These are planned orders only; no sessions were started or stopped.

    '; + setHtml('#organization-lifecycle', `

    Session lifecycle order

    Derived from approved reporting lines. Independent sessions keep their configured roster order.

    Preview only
    +
    +

    Start · managers first

    ${startup}
    +

    Wind down · reports before managers

    ${shutdown}
    +
    ${blockerMarkup}`); +} + +function renderOrganization(organization, agents) { + const graph = organization?.graph; + if (!graph?.roles?.length) { + setText('#organization-revision', 'No active graph'); + setHtml('#organization-map', empty(organization?.status === 'not-installed' + ? 'This repository is still in intake. Design and approve a Fleet to establish its organization.' + : 'No approved organization graph is available in this snapshot.')); + renderLifecycleOrder(organization?.lifecycle ?? null, new Map(agents.map((agent) => [agent.areaId, agent]))); + return; + } + + setText('#organization-revision', `Revision ${graph.revision} · ${graph.roles.length} ${graph.roles.length === 1 ? 'role' : 'roles'}`); + const rolesById = new Map(graph.roles.map((role) => [role.id, role])); + const agentsById = new Map(agents.map((agent) => [agent.areaId, agent])); + renderLifecycleOrder(organization.lifecycle, agentsById); + const ownersByIdentity = new Map(); + for (const owner of graph.implementation_owners ?? []) { + if (!ownersByIdentity.has(owner.identity_id)) ownersByIdentity.set(owner.identity_id, []); + ownersByIdentity.get(owner.identity_id).push(owner.surface); + } + setHtml('#organization-map', `

    Reporting, coordination, and implementation are separate relationships. A role does not create an identity or grant owner-only approval.

    +
    ${graph.roles.map((role) => { + const agent = role.identity_id === 'owner' ? null : agentsById.get(role.identity_id); + const identityStatus = role.identity_id === 'owner' + ? 'Human owner' + : (agent ? (agent.state ?? 'offline') : 'Identity missing'); + const responsibilities = (role.responsibilities ?? []).map((item) => `
  • ${escapeHtml(item)}
  • `).join(''); + const surfaces = ownersByIdentity.get(role.identity_id) ?? []; + return `
    +

    ${escapeHtml(role.title)}

    ${escapeHtml(role.kind)} · ${escapeHtml(role.identity_id)}

    ${escapeHtml(identityStatus)}
    + ${responsibilities ? `
      ${responsibilities}
    ` : ''} +
    +
    Reports to
    ${roleLinks(role.reports_to, rolesById)}
    +
    Coordinates
    ${roleLinks(role.coordinates, rolesById)}
    +
    Works directly with
    ${roleLinks(role.consults_with, rolesById)}
    +
    Authority
    ${escapeHtml((role.authority ?? []).join(', ') || 'None recorded')}
    + ${surfaces.length ? `
    Implementation
    ${surfaces.map(escapeHtml).join(', ')}
    ` : ''} +
    +
    `; + }).join('')}
    `); +} + +function cadenceCopy(trigger) { + if (trigger?.type === 'interval') { + const seconds = trigger.seconds; + if (seconds % 3600 === 0) return `Configured every ${seconds / 3600} ${seconds === 3600 ? 'hour' : 'hours'}`; + if (seconds % 60 === 0) return `Configured every ${seconds / 60} minutes`; + return `Configured every ${seconds} seconds`; + } + if (trigger?.type === 'cron') return 'Configured cron cadence'; + return 'No check-in cadence configured'; +} + +function renderScheduleLauncher(snapshot) { + const details = $('[data-schedule-launcher]'); + if (renderingSnapshot && refreshProtection.protects(details)) { heldPanels += 1; return; } + const button = details?.querySelector('[data-schedule-launcher-preview]'); + const systemSchedules = (snapshot.schedules ?? []).filter((schedule) => schedule.lifetime === 'system'); + const state = snapshot.scheduleLauncher; + const status = $('#schedule-launcher-state'); + if (!details || !button || !status) return; + details.dataset.action = state?.installed ? 'reconcile' : 'install'; + button.textContent = state?.installed + ? (state.stale ? 'Review timer refresh' : 'Timer files match configuration') + : 'Review timer setup'; + button.disabled = snapshot.mode !== 'installed' || systemSchedules.length === 0 + || Boolean(state?.installed && !state.stale); + if (snapshot.mode !== 'installed') status.textContent = 'Install and review a Fleet before configuring a persistent timer.'; + else if (!systemSchedules.length) status.textContent = 'No persistent system schedules are configured.'; + else if (!state?.installed) status.textContent = 'Timer not installed. Preview lists the schedules and unit files that setup would enable.'; + else if (state.stale) status.textContent = `Installed files or their configuration digest need review. Systemd running state is not queried. Current digest: ${state.currentConfigDigest}.`; + else status.textContent = `TORCH-owned unit files match the current configuration (${state.currentConfigDigest}). Systemd running state is not queried.`; + const launcher = state ?? {}; + const wakes = snapshot.managerWakes; + setText('#manager-wake-count', wakes?.available ? `${wakes.blockingCount} awaiting inspection` : 'Not available'); + setHtml('#manager-wake-list', !wakes?.available + ? empty(wakes?.reason ?? 'Manager launch records are unavailable.') + : wakes.reservations.length ? wakes.reservations.map((record) => ` +
    ${escapeHtml(record.managerId)}: ${record.blocksManagerWake ? 'Launch outcome unknown' : escapeHtml(record.outcome)} + Schedule ${escapeHtml(record.scheduleId)} · Reserved ${escapeHtml(record.reservedAt)} · Presence ${escapeHtml(record.managerState ?? 'unavailable')} + Reservation ${escapeHtml(record.id)} · Message ${escapeHtml(record.messageId)} + ${record.blocksManagerWake ? '

    Inspect the runtime before owner-approved CLI recovery. No automatic retry or budget refund.

    ' : ''} +
    ${record.blocksManagerWake ? 'Wake blocked' : 'Recorded'}
    + `).join('') : empty('No manager launches have been reserved.')); + setHtml('#schedule-list', (snapshot.schedules ?? []).length ? snapshot.schedules.map((schedule) => ` +
    ${escapeHtml(schedule.id)}${escapeHtml(schedule.owner)} · ${escapeHtml(schedule.lifetime)} · ${escapeHtml(cadenceCopy(schedule.trigger))}
    ${escapeHtml(schedule.behavior)}
    + `).join('') : empty('No schedules configured.')); + button.dataset.launcherReady = String(snapshot.mode === 'installed' && systemSchedules.length > 0 + && (!launcher.installed || launcher.stale)); +} + +function clearScheduleLauncherPreview(message = 'Review cleared. Check the current timer plan before confirming.') { + const details = $('[data-schedule-launcher]'); + if (!details) return; + delete details.dataset.previewToken; + delete details.dataset.planHash; + const button = details.querySelector('[data-schedule-launcher-preview]'); + const panel = details.querySelector('[data-schedule-launcher-review]'); + if (button) button.hidden = false; + if (panel) panel.hidden = true; + const confirm = details.querySelector('[data-schedule-launcher-confirm]'); + const edit = details.querySelector('[data-schedule-launcher-edit]'); + if (confirm) { confirm.hidden = true; confirm.disabled = false; } + if (edit) edit.hidden = true; + const status = details.querySelector('[data-schedule-launcher-status]'); + if (status) { status.textContent = message; status.classList.remove('is-error'); } +} + +function scheduleLauncherStatus(message, error = false) { + const status = $('[data-schedule-launcher-status]'); + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); +} + +function showScheduleLauncherPlan(details, result) { + const { plan } = result; + const panel = details.querySelector('[data-schedule-launcher-review]'); + details.dataset.previewToken = result.token; + details.dataset.planHash = result.planHash; + details.dataset.action = plan.action; + details.querySelector('[data-schedule-launcher-preview]').hidden = true; + panel.querySelector('[data-schedule-launcher-title]').textContent = plan.action === 'install' + ? 'Install the persistent TORCH schedule timer' + : 'Refresh the existing TORCH schedule timer'; + const dailyBudget = plan.runtimeWakeBudget?.max_invocations_per_day; + panel.querySelector('[data-schedule-launcher-effect]').textContent = plan.effect + + (dailyBudget ? ` All managers share a limit of ${dailyBudget} wake invocations per UTC day.` : ''); + setHtml('[data-schedule-launcher-schedules]', plan.systemSchedules.length + ? `
      ${plan.systemSchedules.map((schedule) => ` +
    • ${escapeHtml(schedule.title)} · ${escapeHtml(schedule.id)}${escapeHtml(schedule.owner)} · ${escapeHtml(cadenceCopy(schedule.trigger))} · ${escapeHtml(schedule.behavior)}${schedule.action.type === 'manager-check-in' ? `${escapeHtml(!schedule.action.wake?.enabled ? 'Inbox delivery only' : schedule.action.wake.budget_mode === 'invocation-count' ? 'Count-limited wake · No USD spending ceiling' : `Hard spending ceiling: $${schedule.action.wake.max_usd_per_invocation} per invocation`)}` : ''}Action: ${escapeHtml(JSON.stringify(schedule.action))}
    • + `).join('')}
    ` : empty('No system schedules are configured.')); + setHtml('[data-schedule-launcher-files]', plan.files.length + ? `
    Files and current review${plan.files.map((file) => `
    ${escapeHtml(file.name)} · ${escapeHtml(file.path)} · SHA-256 ${escapeHtml(file.sha256)}
    `).join('')}
    Configuration digest ${escapeHtml(plan.digest)}
    ${plan.updates?.length ? `
    ${plan.updates.filter((entry) => entry.changed).length} owned unit file(s) will change.
    ` : ''}
    ` + : empty('No unit files are available in this plan.')); + const confirm = panel.querySelector('[data-schedule-launcher-confirm]'); + confirm.textContent = plan.action === 'install' ? 'Install and start timer' : 'Refresh and start timer'; + confirm.disabled = !plan.canProceed; + confirm.hidden = !plan.canProceed; + const edit = panel.querySelector('[data-schedule-launcher-edit]'); + edit.hidden = false; + panel.hidden = false; + if (plan.blockers?.length) scheduleLauncherStatus(`Cannot proceed: ${plan.blockers.map((blocker) => blocker.code).join(', ')}.`, true); + else scheduleLauncherStatus('Review each schedule and path. Nothing has changed yet.'); +} + +function stateClass(state) { + return ['waiting', 'stale', 'missing', 'offline'].includes(state) ? `is-${state}` : ''; +} + +function renderFlowWatch(snapshot) { + const agents = new Map((snapshot.agents ?? []).map((agent) => [agent.areaId, agent])); + const checkIns = snapshot.managerCheckIns; + const managers = checkIns?.managers ?? []; + const attentionCount = managers.filter((manager) => manager.attentionRequired).length; + setText('#manager-checkin-count', checkIns?.available + ? `${attentionCount} of ${managers.length} need attention` : 'Not available'); + setHtml('#manager-checkin-list', checkIns?.available + ? (managers.length ? managers.map((manager) => { + const reports = manager.directReports ?? []; + const reportsWithApprovals = new Map((manager.approvalWaits ?? []).map((approval) => [approval.requester, approval])); + const findings = manager.findings ?? []; + return `
    +
    ${escapeHtml(manager.title ?? manager.managerId)} · ${escapeHtml(manager.managerId)} · ${escapeHtml(manager.state ?? 'unknown')}
    ${findings.length + reportsWithApprovals.size} ${findings.length + reportsWithApprovals.size === 1 ? 'signal' : 'signals'}
    +

    ${escapeHtml(cadenceCopy(manager.cadence))} · timer installation not verified

    + ${reports.length ? reports.map((report) => { + const approval = reportsWithApprovals.get(report.areaId); + const label = approval ? `waiting on ${approval.approver === 'owner' ? 'owner' : (agents.get(approval.approver)?.title ?? approval.approver)}` + : report.state === 'waiting' ? 'wait reason not recorded' + : report.state === 'offline' ? 'offline' : report.state === 'stale' ? 'stale' + : report.state === 'missing' ? 'identity missing' : report.state; + const task = report.task ? ` · ${report.task}` : ''; + const cls = approval || report.state === 'waiting' ? 'is-waiting' : stateClass(report.state); + return `
    ${escapeHtml(report.title)}${escapeHtml(task)}${escapeHtml(label)}
    `; + }).join('') : empty('No direct reports in the active organization graph.')} + ${findings.some((finding) => finding.type === 'unacknowledged-message') + ? `

    ${findings.filter((finding) => finding.type === 'unacknowledged-message').reduce((sum, finding) => sum + finding.count, 0)} direct-report message(s) still need acknowledgement.

    ` : ''} +
    `; + }).join('') : empty('No manager roles with direct reports are configured.')) + : empty(checkIns?.reason ?? 'Manager check-in observations are not available.')); + + const approvalState = snapshot.approvalRequests; + const pending = (approvalState?.items ?? []).filter((approval) => approval.status === 'pending'); + setText('#approval-request-count', approvalState?.available + ? `${approvalState.pendingCount ?? pending.length} open` : 'Not available'); + setHtml('#approval-request-list', approvalState?.available + ? (pending.length ? pending.map((approval) => { + const requester = agents.get(approval.requester); + const approver = approval.approver === 'owner' ? 'Project owner' + : (agents.get(approval.approver)?.title ?? approval.approver); + const waitClass = approval.approver === 'owner' ? 'wait-owner' + : approval.approver === 'session-manager' ? 'wait-manager' : ''; + const refs = [approval.task, approval.evidence].filter(Boolean).map((reference) => escapeHtml(reference)).join(' · '); + return `
    +

    ${escapeHtml(approval.title)}

    +

    ${escapeHtml(requester?.title ?? approval.requester)} is waiting on ${escapeHtml(approver)}${refs ? ` · ${refs}` : ''}

    +
    Request details · ${escapeHtml(ageLabel(approval.createdAt))}

    ${escapeHtml(approval.summary)}

    + ${approval.approver === 'owner' ? `
    + + +

    + +
    ` : `

    Decision belongs to ${escapeHtml(approver)}. The owner Console will not transfer that authority.

    `} +
    `; + }).join('') : empty('No open approval requests.')) + : empty(approvalState?.reason ?? 'Structured approval requests are not available.')); +} + +function attentionItems(snapshot) { + const items = []; + const unresolvedDelivery = (snapshot.deliveryOperations ?? []).filter((operation) => ['running', 'unknown', 'succeeded'].includes(operation.state)); + if (unresolvedDelivery.length) items.push({ tone: 'urgent', title: `${unresolvedDelivery.length} delivery operation${unresolvedDelivery.length === 1 ? ' needs' : 's need'} review`, + detail: 'Inspect recorded attempts and external state before owner-approved recovery. Do not automatically repeat effects.', href: '#operation-outcomes' }); + const neglected = (snapshot.backlogActivity?.tasks ?? []).filter((task) => task.status === 'stale' && task.ownerRequested); + if (neglected.length) items.push({ tone: 'review', title: `${neglected.length} owner request${neglected.length === 1 ? ' needs' : 's need'} activity review`, + detail: 'No named commit within the configured review window. Blocked work may be waiting as expected.', href: '#activity-review' }); + const unknownWakes = snapshot.managerWakes?.blockingCount ?? 0; + if (unknownWakes) items.push({ + tone: 'urgent', title: `${unknownWakes} manager ${unknownWakes === 1 ? 'launch needs' : 'launches need'} inspection`, + detail: 'An unknown launch blocks another wake. Inspect the runtime before owner-approved recovery; no automatic restart occurs.', + href: '#manager-wakes', + }); + const findings = snapshot.doctor?.findings ?? []; + for (const finding of findings.filter((entry) => entry.severity === 'error' || entry.severity === 'warning').slice(0, 5)) { + items.push({ + tone: finding.severity === 'error' ? 'urgent' : 'review', + title: finding.areaId ? `${finding.areaId}: ${finding.code}` : finding.code, + detail: finding.message ?? finding.recommendation ?? 'Doctor reported a project condition to review.', + }); + } + const awaiting = snapshot.messages?.unacknowledged ?? 0; + if (awaiting) items.push({ + tone: 'review', title: `${awaiting} unacknowledged ${awaiting === 1 ? 'message' : 'messages'}`, + detail: 'Open Conversations to inspect the latest durable inter-session handoffs.', + href: '#communications', + }); + const ownerApprovals = (snapshot.approvalRequests?.items ?? []).filter((approval) => + approval.status === 'pending' && approval.approver === 'owner'); + if (ownerApprovals.length) items.push({ + tone: 'decision', title: `${ownerApprovals.length} approval ${ownerApprovals.length === 1 ? 'request' : 'requests'} need your decision`, + detail: 'The named owner is the approver; review the request evidence in Flow watch.', href: '#flow-watch', + }); + const pendingChanges = (snapshot.fleetChanges ?? []).filter((change) => !['active', 'rejected'].includes(change.state)); + const pendingHierarchy = (snapshot.hierarchyProposals ?? []).filter((proposal) => proposal.state === 'proposed'); + if (pendingChanges.length + pendingHierarchy.length) items.push({ + tone: 'decision', title: `${pendingChanges.length + pendingHierarchy.length} organization ${pendingChanges.length + pendingHierarchy.length === 1 ? 'proposal' : 'proposals'}`, + detail: 'Organization proposals wait for owner review; they do not change the active Fleet.', + href: '#organization-proposals', + }); + const waitingIntegration = (snapshot.integration ?? []).filter((item) => !['landed', 'rejected'].includes(item.state)); + if (waitingIntegration.length) items.push({ + tone: 'decision', title: `${waitingIntegration.length} integration ${waitingIntegration.length === 1 ? 'request' : 'requests'}`, + detail: 'Review exact source commits and required checks before landing.', href: '#delivery', + }); + const staleEvidence = (snapshot.backlogHealth?.findings ?? []).filter((finding) => + ['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING'].includes(finding.code)); + if (staleEvidence.length) items.push({ + tone: 'review', title: `${staleEvidence.length} task ${staleEvidence.length === 1 ? 'evidence record needs' : 'evidence records need'} refresh`, + detail: 'Some backlog items were observed against an old or unreachable commit. Re-check them before dispatch.', + href: '#backlog-board', + }); + return items; } 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 ?? []; + initializeSavedViews(snapshot); + const active = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state)); + const working = active.filter((task) => task.state === 'in_progress').length; + const blocked = active.filter((task) => task.state === 'blocked').length; + const attention = attentionItems(snapshot); + const branch = snapshot.repository?.branch ?? 'branch not recorded'; + + setText('#project-name', project?.name ?? project?.id ?? 'Not installed'); + setText('#project-branch', branch); + setText('#project-commit', shortCommit(snapshot.repository?.head)); + setText('#fleet-health', snapshot.mode === 'installed' + ? (snapshot.doctor?.healthy ? 'Healthy' : 'Review findings') : 'Not installed'); + setText('#project-dirty', snapshot.repository?.dirty ? 'Uncommitted changes' : 'Clean at snapshot'); + setText('#context-state', snapshot.contextLocality?.measured ? 'Measured' : 'Not measured'); + setText('#recoverability-state', snapshot.mode === 'installed' + ? (snapshot.recoverability?.level ?? 'Unknown') : 'Not installed'); + setText('#agent-count', `${agents.length} ${agents.length === 1 ? 'identity' : 'identities'}`); + setText('#snapshot-time', `Updated ${ageLabel(snapshot.generatedAt)}`); + setText('#snapshot-provenance', demoWorkspace ? 'Sample project · Changes stay in this tab' : `Read-only snapshot · ${snapshot.schema ?? 'schema unavailable'}`); + const parts = [`${agents.length} persistent ${agents.length === 1 ? 'identity' : 'identities'}`, + `${working} ${working === 1 ? 'task' : 'tasks'} in progress`]; + if (blocked) parts.push(`${blocked} blocked`); + setText('#project-summary', snapshot.mode === 'installed' + ? `${parts.join(' · ')}. ${attention.length ? `${attention.length} item${attention.length === 1 ? '' : 's'} may need your review.` : 'No owner decision is currently waiting in the observed queues.'}` + : 'This repository has not been installed as a TORCH Fleet. The view is showing repository intake only.'); + renderTaskCreateOptions(agents, tasks, snapshot.mode === 'installed'); + renderOwnerRequestOptions(agents, tasks, snapshot.mode === 'installed'); + renderRuntimeProfileEditor(agents, snapshot.runtimeAdapters ?? [], snapshot.mode === 'installed'); + renderFlowWatch(snapshot); + setHtml('#owner-briefing-content', globalThis.TorchOwnerDigest.render(snapshot.ownerDigest)); + + setText('#attention-count', `${attention.length} ${attention.length === 1 ? 'item' : 'items'}`); + setHtml('#attention-list', attention.length ? attention.map((item) => ` +
    + +
    ${escapeHtml(item.title)}

    ${escapeHtml(item.detail)}

    + ${item.href ? `Open relevant view` : ''}
    +
    `).join('') : empty('No owner decisions are waiting in the observed queues.')); + + renderBacklog(tasks, snapshot.backlogHealth?.findings ?? []); + setHtml('#activity-review-content', globalThis.TorchTaskActivity.render(snapshot.backlogActivity)); + renderInitiativeProgress(tasks); + setHtml('#agent-list', agents.length ? agents.map((agent) => { + const profile = [agent.runtime, agent.model, agent.reasoning].filter(Boolean).join(' / ') || 'Profile not recorded'; + const live = agent.state !== 'offline'; + return `
    + +
    ${escapeHtml(agent.title ?? agent.areaId)}${escapeHtml(agent.summary ?? profile)}${escapeHtml(agent.areaId)} · ${escapeHtml(profile)}
    +
    ${escapeHtml(agent.state ?? 'unknown')}${escapeHtml(ageLabel(agent.heartbeatAt))}
    +
    `; + }).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.'); + setHtml('#ownership-list', domains.length ? domains.map((domain) => ` +
    ${escapeHtml(domain.title ?? domain.id)}${escapeHtml((domain.owned_paths ?? []).join(', ') || 'No implementation paths assigned')}
    ${escapeHtml((domain.neighbours ?? []).join(', ') || 'No peer links')}
    + `).join('') : empty('No approved domain ownership graph.')); + renderOrganization(snapshot.organization, agents); - 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 messages = snapshot.messages?.recent ?? []; + setText('#message-count', `${snapshot.messages?.unacknowledged ?? 0} unacknowledged`); + setHtml('#message-list', messages.length ? messages.map((message) => { + const refs = [message.task, message.path, message.commit].filter(Boolean); + const ack = (message.acknowledgedBy ?? []).length + ? `Acknowledged by ${message.acknowledgedBy.join(', ')}` : 'Awaiting acknowledgement'; + return `
    ${escapeHtml(message.sender)} to ${escapeHtml(message.recipient)}${escapeHtml(ageLabel(message.createdAt))}

    ${escapeHtml(message.body)}

    ${escapeHtml(ack)}${refs.length ? ` · ${escapeHtml(refs.join(' · '))}` : ''}
    `; + }).join('') : empty('No durable messages yet. Runtime-native private reasoning is not shown here.')); - 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.'); + setHtml('#check-list', globalThis.TorchCheckEvidence.render(snapshot.checks)); - const changes = snapshot.fleetChanges ?? []; - const pendingChanges = changes.filter((change) => !['active', 'rejected'].includes(change.state)); - $('#change-count').textContent = `${pendingChanges.length} pending`; - $('#change-list').innerHTML = changes.length ? changes.map((change) => ` -
    ${safe(change.title ?? change.domainId)}${safe(change.expectedBenefit ?? 'No expected benefit recorded')}
    ${safe(change.state)}
    - `).join('') : empty('No Fleet changes proposed. The current roster remains in force.'); + const artifacts = snapshot.artifacts?.items ?? []; + setHtml('#artifact-list', artifacts.length ? artifacts.map((artifact) => ` +
    + ${localPath(artifact.thumbnailUrl ?? artifact.url) ? `${escapeHtml(artifact.alt ?? 'Project review artifact')}` : '
    Image unavailable or failed its integrity check
    '} +
    + ${escapeHtml(artifact.title ?? artifact.name)} +
    +
    Task
    ${escapeHtml(artifact.taskId ?? 'Not recorded')}
    +
    Identity
    ${escapeHtml(artifact.identityId ?? 'Not recorded')}
    + ${artifact.sessionId ? `
    Runtime session
    ${escapeHtml(artifact.sessionId)}
    ` : ''} +
    Commit
    ${escapeHtml(shortCommit(artifact.commit))}
    +
    + ${escapeHtml(artifact.integrity === 'available' ? 'Catalogued image' : 'Unavailable evidence')} · ${escapeHtml(ageLabel(artifact.createdAt))} +
    +
    +

    Review notes ${(artifact.feedback ?? []).length}

    + ${(artifact.feedback ?? []).length ? artifact.feedback.map((comment) => `

    ${escapeHtml(comment.body)}

    Owner · ${escapeHtml(ageLabel(comment.createdAt))} · sent to ${escapeHtml(comment.recipient)}
    `).join('') : '

    No owner feedback on this image.

    '} +
    + + + + + +
    +
    +
    + `).join('') : empty(snapshot.artifacts?.available === false + ? (snapshot.artifacts?.reason ?? 'Artifact review is not connected to this snapshot yet. Images need task, session, and commit provenance.') + : 'No linked screenshots or review artifacts are recorded.')); 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.'); - + setHtml('#integration-list', integrations.length ? integrations.map((item) => ` +
    #${Number(item.queueOrder) || '—'} · ${escapeHtml(item.sourceArea)} · ${escapeHtml(shortCommit(item.sourceCommit))}${item.authorizedBy ? `Authorized by ${escapeHtml(item.authorizedBy)} · ` : ''}${escapeHtml(item.reason ?? 'Awaiting checks or landing review')}
    ${escapeHtml(item.state)}
    + `).join('') : empty('No pending integration requests.')); 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.'); + setHtml('#delivery-list', deliveries.length ? deliveries.map((item) => ` +
    ${escapeHtml(item.label)}${escapeHtml(item.sourceArea)} · ${escapeHtml(shortCommit(item.commit))}
    ${escapeHtml(item.state)}
    + `).join('') : empty('No delivery records. Integration does not imply deployment.')); + setHtml('#operation-outcomes', globalThis.TorchOperationOutcomes.render(snapshot)); - 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 changes = snapshot.fleetChanges ?? []; + const proposals = snapshot.hierarchyProposals ?? []; + const allChanges = [ + ...changes.map((change) => organizationProposalCard({ type: 'fleet', proposal: change, title: change.title, state: change.state, id: change.id })), + ...proposals.map((proposal) => organizationProposalCard({ type: 'hierarchy', proposal, title: proposal.proposal?.title, state: proposal.state, id: proposal.id })), + ]; + const pending = [...changes, ...proposals].filter((change) => change.state === 'proposed'); + setText('#change-count', `${pending.length} pending`); + setHtml('#change-list', allChanges.length ? allChanges.join('') : empty('No Fleet or hierarchy proposals. The active organization remains in force.')); + + const worktrees = snapshot.worktrees ?? []; + setHtml('#worktree-list', worktrees.length ? worktrees.map((worktree) => ` +
    ${escapeHtml(worktree.area)}${escapeHtml(worktree.branch)} · ${escapeHtml(worktree.path)}
    ${escapeHtml(`${worktree.ahead ?? 0} ahead / ${worktree.behind ?? 0} behind`)}
    + `).join('') : empty('No managed worktrees have been created.')); 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 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.'); - + setHtml('#resource-list', holders.length || waiters.length + ? [...holders.map((item) => `
    ${escapeHtml(item.resourceId)}${escapeHtml(item.areaId)} holds
    `), + ...waiters.map((item) => `
    ${escapeHtml(item.resourceId)}${escapeHtml(item.areaId)} waits
    `)].join('') + : empty('All configured resources are available.')); + renderScheduleLauncher(snapshot); 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.acknowledgedBy ?? []).length ? `Acknowledged by ${(message.acknowledgedBy ?? []).join(', ')}` : 'Unacknowledged')}

    ${safe(message.body)}
    - `).join('') : empty('No durable messages yet.'); + setHtml('#provider-list', [ + `
    Forge${escapeHtml(snapshot.mode === 'installed' ? `${providers.forge?.provider ?? 'none'} · ${providers.forge?.status ?? 'unknown'}` : 'not configured')}
    `, + ...(providers.runtimes ?? []).map((runtime) => `
    ${escapeHtml(runtime.name)}${escapeHtml(runtime.configured ? 'configured' : 'unavailable')}
    `), + `
    Release${escapeHtml(providers.delivery?.release?.provider ?? 'not configured')}
    `, + `
    Deployment${escapeHtml(providers.delivery?.deployment?.provider ?? 'not configured')}
    `, + ].join('')); const decisions = snapshot.decisions?.content ?? ''; - $('#decision-list').innerHTML = decisions.trim() - ? `

    ${safe(decisions)}

    ` : empty('No tracked decisions.'); - + setHtml('#decision-list', decisions.trim() + ? `

    ${escapeHtml(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('') - : `

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

    TORCH will compare cache activity with verified outcomes, not token count alone.`; + setHtml('#context-detail', locality?.measured + ? (locality.samples ?? []).map((sample) => `
    ${escapeHtml(sample.areaId)}${escapeHtml(String(sample.cacheRead ?? 0))} cached tokens
    `).join('') + : `

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

    TORCH compares 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' }); +let liveUpdatesPaused = false; +let appliedRefreshes = 0; +const refreshScheduler = globalThis.TorchLiveRefresh.createScheduler({ + load: async () => { + if (demoWorkspace) return demoWorkspace.snapshot(); + const response = await fetch('/api/snapshot', { cache: 'no-store', signal: AbortSignal.timeout(10_000) }); 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; - } + if (snapshot.schema !== 'torch.dev/observation/v1alpha1') throw new Error('Snapshot schema is unavailable or unsupported'); + return snapshot; + }, + apply: (snapshot) => { + const editedBeforeRender = refreshProtection.dirty(); + heldPanels = 0; + renderingSnapshot = true; + try { render(snapshot); } finally { renderingSnapshot = false; } + refreshProtection.remember({ preserve: editedBeforeRender }); + $('#console-error').hidden = true; + const status = $('#live-refresh-status'); + status.dataset.generation = String(++appliedRefreshes); + status.textContent = `${liveUpdatesPaused ? 'Automatic updates paused.' : 'Updates every 15 seconds while visible.'}${heldPanels ? ' Edited forms and active previews retained; their panels may show older state.' : ''}`; + }, + onError: (caught) => { + const error = $('#console-error'); + error.textContent = `Snapshot unavailable: ${caught.message}. Last displayed evidence retained.`; + error.hidden = false; + }, +}); +function refresh({ completed = null, background = false } = {}) { + if (completed) refreshProtection.complete(completed); + return refreshScheduler.request({ supersede: !background }); } -$('#refresh-console')?.addEventListener('click', refresh); +async function feedbackRequest(path, payload) { + if (demoWorkspace) return demoWorkspace.request(path, payload); + const response = await fetch(path, { + method: 'POST', cache: 'no-store', credentials: 'same-origin', + headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload), + }); + const result = await response.json(); + if (!response.ok) throw new Error(result.message ?? result.error ?? `Request failed (${response.status})`); + return result; +} + +async function consoleActionRequest(path, payload) { + if (demoWorkspace) return demoWorkspace.request(path, payload); + const response = await fetch(path, { + method: 'POST', cache: 'no-store', credentials: 'same-origin', + headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload), + }); + const result = await response.json(); + if (!response.ok) throw new Error(result.message ?? result.error ?? `Request failed (${response.status})`); + return result; +} + +function approvalDecisionStatus(form, message, error = false) { + const status = form.querySelector('.approval-decision-status'); + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); +} + +function clearApprovalDecision(form, message = 'Preview cleared. Review the current request before deciding.') { + delete form.dataset.previewToken; + delete form.dataset.planHash; + form.querySelector('[name="decision"]').disabled = false; + form.querySelector('[name="note"]').disabled = false; + form.querySelector('[data-approval-preview]').disabled = false; + form.querySelector('[data-approval-preview-panel]').hidden = true; + form.querySelector('[data-approval-confirm]').hidden = true; + form.querySelector('[data-approval-edit]').hidden = true; + approvalDecisionStatus(form, message); +} + +$('#approval-request-list')?.addEventListener('click', async (event) => { + const edit = event.target.closest('[data-approval-edit]'); + if (edit) { + const form = edit.closest('.approval-decision-form'); + if (form) clearApprovalDecision(form); + return; + } + const previewButton = event.target.closest('[data-approval-preview]'); + const confirmButton = event.target.closest('[data-approval-confirm]'); + const form = event.target.closest('.approval-decision-form'); + if (!form) return; + if (previewButton) { + const decision = form.querySelector('[name="decision"]').value; + const note = form.querySelector('[name="note"]').value.trim(); + if (!decision) { + approvalDecisionStatus(form, 'Choose approve or reject first.', true); + form.querySelector('[name="decision"]').focus(); + return; + } + previewButton.disabled = true; + approvalDecisionStatus(form, 'Rechecking the named approver and current request revision…'); + try { + const id = form.dataset.approvalId; + const result = await consoleActionRequest(`/api/approvals/${encodeURIComponent(id)}/decision/preview`, { decision, note }); + const { plan } = result; + form.dataset.previewToken = result.token ?? ''; + form.dataset.planHash = result.planHash; + form.querySelector('[name="decision"]').disabled = true; + form.querySelector('[name="note"]').disabled = true; + form.querySelector('[data-approval-preview-panel]').hidden = false; + form.querySelector('[data-approval-preview-title]').textContent = `Confirm ${plan.decision} · ${plan.title}`; + form.querySelector('[data-approval-preview-effect]').textContent = plan.effect; + form.querySelector('[data-approval-preview-scope]').textContent = JSON.stringify({ + approvalId: plan.approvalId, requester: plan.requester, approver: plan.approver, + revision: plan.revision, task: plan.task, evidence: plan.evidence, note: plan.note, + }, null, 2); + form.querySelector('[data-approval-confirm]').textContent = `Confirm ${plan.decision}`; + form.querySelector('[data-approval-confirm]').hidden = false; + form.querySelector('[data-approval-edit]').hidden = false; + approvalDecisionStatus(form, 'Preview is bound to this request revision for five minutes.'); + } catch (error) { + approvalDecisionStatus(form, error.message, true); + } finally { previewButton.disabled = false; } + return; + } + if (!confirmButton) return; + confirmButton.disabled = true; + approvalDecisionStatus(form, 'Recording the owner decision and notifying the requester…'); + try { + const id = form.dataset.approvalId; + const result = await consoleActionRequest(`/api/approvals/${encodeURIComponent(id)}/decision`, { + decision: form.querySelector('[name="decision"]').value, + note: form.querySelector('[name="note"]').value.trim(), + token: form.dataset.previewToken, planHash: form.dataset.planHash, + }); + approvalDecisionStatus(form, `${result.approval.status} and requester notified.`); + await refresh({ completed: form }); + } catch (error) { + approvalDecisionStatus(form, error.message, true); + confirmButton.disabled = false; + } +}); + +function priorityStatus(form, message, error = false) { + const status = form.querySelector('.priority-change-status'); + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); +} + +function taskCreateStatus(form, message, error = false) { + const status = form?.querySelector('.task-create-status'); + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); +} + +function taskCreatePayload(form) { + const data = new FormData(form); + return { + title: String(data.get('title') ?? '').trim(), + description: String(data.get('description') ?? '').trim(), + priority: String(data.get('priority') ?? 'normal'), + affectedDomains: [...form.querySelector('[name="affectedDomains"]').selectedOptions].map((option) => option.value), + dependencies: [...form.querySelector('[name="dependencies"]').selectedOptions].map((option) => option.value), + acceptanceCriteria: String(data.get('acceptanceCriteria') ?? '').split('\n').map((item) => item.trim()).filter(Boolean), + feature: String(data.get('feature') ?? '').trim(), + milestone: String(data.get('milestone') ?? '').trim(), + }; +} + +function taskCreatePreviewMarkup(plan) { + const task = plan.task; + const criteria = task.acceptanceCriteria.map((item) => `
  • ${escapeHtml(item)}
  • `).join(''); + return `
    Review this proposal

    This is the exact task TORCH will add.

    +

    ${escapeHtml(task.title)}

    ${escapeHtml(task.description)}

    +
    Initial state
    Proposed · unassigned
    Priority
    ${escapeHtml(task.priority)}
    +
    Suggested domains
    ${task.affectedDomains.length ? escapeHtml(task.affectedDomains.join(', ')) : 'Session Manager triage'}
    +
    Dependencies
    ${task.dependencies.length ? escapeHtml(task.dependencies.join(', ')) : 'None'}
    +
    Feature / milestone
    ${escapeHtml([task.feature, task.milestone].filter(Boolean).join(' / ') || 'Not labeled')}
    +
    Observed commit
    ${escapeHtml(shortCommit(task.observedAt))}
    +
    Acceptance criteria
      ${criteria}
    +

    ${escapeHtml(plan.authority)} ${escapeHtml(plan.effect)}

    + `; +} + +$('#task-create-form')?.addEventListener('submit', async (event) => { + const form = event.target.closest('#task-create-form'); + if (!form) return; + event.preventDefault(); + const button = form.querySelector('button[type="submit"]'); + if (!button) return; + button.disabled = true; + taskCreateStatus(form, 'Preparing the exact task proposal…'); + try { + const payload = taskCreatePayload(form); + const result = await consoleActionRequest('/api/backlog/tasks/preview', payload); + form.dataset.createPayload = JSON.stringify(payload); + form.dataset.previewToken = result.token; + form.dataset.planHash = result.planHash; + form.querySelector('[data-task-create-fields]').disabled = true; + const preview = form.querySelector('[data-task-create-preview]'); + preview.innerHTML = taskCreatePreviewMarkup(result.plan); + preview.hidden = false; + taskCreateStatus(form, 'Review routing hints and the proposed, unassigned effect before confirming.'); + } catch (error) { + taskCreateStatus(form, error.message, true); + } finally { button.disabled = false; } +}); + +$('#task-create-form')?.addEventListener('click', async (event) => { + const form = event.currentTarget; + const edit = event.target.closest('[data-task-create-edit]'); + if (edit) { + delete form.dataset.createPayload; + delete form.dataset.previewToken; + delete form.dataset.planHash; + form.querySelector('[data-task-create-fields]').disabled = false; + form.querySelector('[data-task-create-preview]').hidden = true; + taskCreateStatus(form, 'Preview cleared. Review the edited proposal before creating it.'); + return; + } + const confirm = event.target.closest('[data-task-create-confirm]'); + if (!confirm) return; + const token = form.dataset.previewToken; + const planHash = form.dataset.planHash; + if (!token || !planHash || !form.dataset.createPayload) return; + confirm.disabled = true; + taskCreateStatus(form, 'Creating the reviewed task proposal…'); + try { + const payload = JSON.parse(form.dataset.createPayload); + const result = await consoleActionRequest('/api/backlog/tasks', { ...payload, token, planHash }); + setText('#task-create-action-status', `${result.task.id} added as proposed and unassigned. The Session Manager retains triage; no agent was started.`); + $('#task-create-action-status').hidden = false; + form.reset(); + form.querySelector('[data-task-create-fields]').disabled = false; + form.querySelector('[data-task-create-preview]').hidden = true; + delete form.dataset.createPayload; + delete form.dataset.previewToken; + delete form.dataset.planHash; + form.closest('details').open = false; + taskCreateStatus(form, 'Task proposal created.'); + await refresh({ completed: form }); + } catch (error) { + delete form.dataset.createPayload; + delete form.dataset.previewToken; + delete form.dataset.planHash; + form.querySelector('[data-task-create-fields]').disabled = false; + form.querySelector('[data-task-create-preview]').hidden = true; + taskCreateStatus(form, `${error.message} Review the task list before trying again.`, true); + } finally { confirm.disabled = false; } +}); + +$('#backlog-board')?.addEventListener('submit', async (event) => { + const form = event.target.closest('.priority-change-form'); + if (!form) return; + event.preventDefault(); + const priority = form.querySelector('[name="priority"]')?.value; + const reason = form.querySelector('[name="reason"]')?.value.trim(); + const expectedRevision = Number(form.dataset.revision); + const button = form.querySelector('button[type="submit"]'); + if (!priority || !reason || !button) return; + button.disabled = true; + priorityStatus(form, 'Preparing the exact priority-change preview…'); + try { + const result = await consoleActionRequest(`/api/backlog/tasks/${encodeURIComponent(form.dataset.taskId)}/priority/preview`, { + priority, reason, expectedRevision, + }); + const plan = result.plan; + form.dataset.previewToken = result.token; + form.dataset.planHash = result.planHash; + form.querySelector('[name="priority"]').disabled = true; + form.querySelector('[name="reason"]').disabled = true; + const preview = form.querySelector('[data-priority-preview]'); + preview.innerHTML = ` + +

    ${escapeHtml(plan.reason)}

    ${escapeHtml(plan.effect)}

    + `; + preview.hidden = false; + priorityStatus(form, 'Review the task, revision, priority, reason, and effect before applying.'); + } catch (error) { priorityStatus(form, error.message, true); } + finally { button.disabled = false; } +}); + +$('#backlog-board')?.addEventListener('click', async (event) => { + const edit = event.target.closest('[data-priority-edit]'); + if (edit) { + const form = edit.closest('.priority-change-form'); + if (!form) return; + delete form.dataset.previewToken; + delete form.dataset.planHash; + form.querySelector('[name="priority"]').disabled = false; + form.querySelector('[name="reason"]').disabled = false; + form.querySelector('[data-priority-preview]').hidden = true; + priorityStatus(form, 'Preview cleared. Review the edited priority again before applying.'); + return; + } + const confirm = event.target.closest('[data-priority-confirm]'); + if (!confirm) return; + const form = confirm.closest('.priority-change-form'); + const token = form?.dataset.previewToken; + const planHash = form?.dataset.planHash; + if (!form || !token || !planHash) return; + confirm.disabled = true; + priorityStatus(form, 'Applying the reviewed priority change…'); + try { + const result = await consoleActionRequest(`/api/backlog/tasks/${encodeURIComponent(form.dataset.taskId)}/priority`, { + priority: form.querySelector('[name="priority"]').value, + reason: form.querySelector('[name="reason"]').value.trim(), + expectedRevision: Number(form.dataset.revision), token, planHash, + }); + const actionStatus = $('#priority-action-status'); + if (actionStatus) { + actionStatus.textContent = `${result.task.id} priority is now ${result.task.priority}; task state and owner were unchanged.`; + actionStatus.hidden = false; + } + await refresh({ completed: form }); + } catch (error) { + priorityStatus(form, error.message, true); + confirm.disabled = false; + } +}); + +function workViewStatus(message) { + setText('#work-view-status', message); +} + +function applyWorkViewFilters(filters) { + const normalized = workViews.normalizeFilters(filters); + const active = currentBacklog.filter((task) => !['completed', 'cancelled'].includes(task.state)); + renderWorkViewFilters(active, normalized); + const values = { + '#work-view-search': normalized.query, + '#work-view-owner': normalized.owner, + '#work-view-state': normalized.state, + '#work-view-priority': normalized.priority, + '#work-view-domain': normalized.domain, + '#work-view-feature': normalized.feature, + '#work-view-milestone': normalized.milestone, + }; + for (const [selector, value] of Object.entries(values)) { + const control = $(selector); + if (control) control.value = value; + } + renderBacklog(currentBacklog, currentBacklogHealth); +} + +function persistCurrentView({ id = null } = {}) { + if (!workViewState.storage || !workViewState.key || !workViews) { + workViewStatus('This browser cannot save views. The project backlog was not changed.'); + return; + } + const name = $('#work-view-name')?.value ?? ''; + const result = workViews.saveView({ + storage: workViewState.storage, key: workViewState.key, views: workViewState.views, + id, name, filters: viewFiltersFromControls(), + }); + if (!result.ok) { + workViewStatus(result.message); + return; + } + workViewState.views = result.views; + workViewState.selectedId = result.view.id; + workViewState.warning = null; + const nameInput = $('#work-view-name'); + if (nameInput) { + nameInput.value = result.view.name; + delete nameInput.dataset.edited; + } + renderSavedViewPicker(); + workViewStatus(`Saved “${result.view.name}” in this browser for this project.`); + renderBacklog(currentBacklog, currentBacklogHealth); +} + +function removeSelectedView() { + if (!workViewState.storage || !workViewState.key || !workViewState.selectedId || !workViews) { + workViewStatus('Choose a saved view before deleting it.'); + return; + } + const selected = selectedSavedView(); + const result = workViews.deleteView({ + storage: workViewState.storage, key: workViewState.key, + views: workViewState.views, id: workViewState.selectedId, + }); + if (!result.ok) { + workViewStatus(result.message); + return; + } + workViewState.views = result.views; + workViewState.selectedId = ''; + const nameInput = $('#work-view-name'); + if (nameInput) { + nameInput.value = ''; + delete nameInput.dataset.edited; + } + renderSavedViewPicker(); + applyWorkViewFilters(emptyWorkFilters); + workViewStatus(`Deleted “${selected?.name ?? 'saved view'}”. Project tasks were not changed.`); +} + +const workViewTools = $('.work-view-tools'); +workViewTools?.addEventListener('input', (event) => { + if (event.target.matches('#work-view-name')) { + event.target.dataset.edited = 'true'; + return; + } + if (!event.target.matches('#work-view-search')) return; + renderBacklog(currentBacklog, currentBacklogHealth); + workViewStatus('Filter edits are temporary until you save or update a view.'); +}); +workViewTools?.addEventListener('change', (event) => { + if (event.target.matches('#work-view-select')) { + const selected = workViewState.views.find((view) => view.id === event.target.value) ?? null; + workViewState.selectedId = selected?.id ?? ''; + const nameInput = $('#work-view-name'); + if (nameInput) { + nameInput.value = selected?.name ?? ''; + delete nameInput.dataset.edited; + } + renderSavedViewPicker(); + applyWorkViewFilters(selected?.filters ?? emptyWorkFilters); + workViewStatus(selected ? `Loaded “${selected.name}”. Edit its filters, then update it or save a copy.` : 'Showing all active work.'); + return; + } + if (!event.target.matches('#work-view-owner, #work-view-state, #work-view-priority, #work-view-domain, #work-view-feature, #work-view-milestone')) return; + renderBacklog(currentBacklog, currentBacklogHealth); + workViewStatus('Filter edits are temporary until you save or update a view.'); +}); +$('#work-view-save')?.addEventListener('click', () => persistCurrentView()); +$('#work-view-update')?.addEventListener('click', () => persistCurrentView({ id: workViewState.selectedId })); +$('#work-view-delete')?.addEventListener('click', removeSelectedView); +$('#work-view-clear')?.addEventListener('click', () => { + workViewState.selectedId = ''; + const nameInput = $('#work-view-name'); + if (nameInput) { + nameInput.value = ''; + delete nameInput.dataset.edited; + } + renderSavedViewPicker(); + applyWorkViewFilters(emptyWorkFilters); + workViewStatus('Filters cleared. All active work is shown.'); +}); + +function ownerRequestStatus(form, message, error = false) { + const status = form.querySelector('.owner-request-status'); + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); +} + +function clearOwnerRequestPreview(form, message = 'Preview cleared. Review the exact request before sending.') { + delete form.dataset.previewToken; + delete form.dataset.planHash; + form.querySelector('[name="recipient"]').disabled = false; + form.querySelector('[name="taskId"]').disabled = false; + form.querySelector('[name="body"]').disabled = false; + form.querySelector('[data-owner-request-preview]').disabled = false; + form.querySelector('[data-owner-request-preview]').hidden = false; + form.querySelector('[data-owner-request-review]').hidden = true; + form.querySelector('[data-owner-request-confirm]').disabled = false; + form.querySelector('[data-owner-request-confirm]').hidden = true; + form.querySelector('[data-owner-request-edit]').hidden = true; + ownerRequestStatus(form, message); +} + +$('#owner-request-form')?.addEventListener('click', async (event) => { + const edit = event.target.closest('[data-owner-request-edit]'); + if (edit) { + clearOwnerRequestPreview(edit.closest('[data-owner-request]')); + return; + } + + const previewButton = event.target.closest('[data-owner-request-preview]'); + if (previewButton) { + const form = previewButton.closest('[data-owner-request]'); + if (!form) return; + const recipient = form.querySelector('[name="recipient"]').value; + const body = form.querySelector('[name="body"]').value.trim(); + if (!recipient || !body) { + ownerRequestStatus(form, 'Choose an identity and write the request before previewing.', true); + return; + } + previewButton.disabled = true; + ownerRequestStatus(form, 'Checking the selected identity, task, and repository…'); + try { + const taskId = form.querySelector('[name="taskId"]').value; + const result = await consoleActionRequest('/api/owner-requests/preview', { recipient, body, taskId }); + const plan = result.plan; + form.dataset.previewToken = result.token; + form.dataset.planHash = result.planHash; + form.querySelector('[name="recipient"]').disabled = true; + form.querySelector('[name="taskId"]').disabled = true; + form.querySelector('[name="body"]').disabled = true; + previewButton.hidden = true; + const panel = form.querySelector('[data-owner-request-review]'); + panel.querySelector('[data-owner-request-title]').textContent = `Request to ${plan.recipientTitle} · ${plan.recipient}`; + panel.querySelector('[data-owner-request-effect]').textContent = `${plan.effect} Repository: ${shortCommit(plan.currentHead)}${plan.task ? ` · Linked task: ${plan.task.id} (${plan.task.state}, owner ${plan.task.owner ?? 'unassigned'})` : ''}.`; + panel.querySelector('[data-owner-request-exact]').textContent = plan.body; + panel.querySelector('[data-owner-request-confirm]').hidden = false; + panel.querySelector('[data-owner-request-edit]').hidden = false; + panel.hidden = false; + ownerRequestStatus(form, 'Preview ready. Check the recipient, task link, and exact request.'); + } catch (error) { + ownerRequestStatus(form, error.message, true); + } finally { previewButton.disabled = false; } + return; + } + + const confirm = event.target.closest('[data-owner-request-confirm]'); + if (!confirm) return; + const form = confirm.closest('[data-owner-request]'); + const token = form?.dataset.previewToken; + const planHash = form?.dataset.planHash; + if (!form || !token || !planHash) return; + confirm.disabled = true; + ownerRequestStatus(form, 'Sending the reviewed durable request…'); + try { + const result = await consoleActionRequest('/api/owner-requests', { + recipient: form.querySelector('[name="recipient"]').value, + taskId: form.querySelector('[name="taskId"]').value, + body: form.querySelector('[name="body"]').value.trim(), token, planHash, + }); + const status = $('#owner-request-action-status'); + if (status) { + status.textContent = `Sent durable request ${result.message.id} to ${result.message.recipient}. It does not start or wake the agent.`; + status.hidden = false; + } + form.querySelector('[name="body"]').value = ''; + clearOwnerRequestPreview(form, 'Request sent. You can send another request when ready.'); + await refresh({ completed: form }); + } catch (error) { + ownerRequestStatus(form, error.message, true); + confirm.disabled = false; + } +}); + +const runtimeProfileForm = $('#runtime-profile-form'); +runtimeProfileForm?.querySelector('[name="areaId"]')?.addEventListener('change', () => { + const agent = runtimeProfileAgents.find((entry) => entry.areaId === runtimeProfileForm.querySelector('[name="areaId"]').value); + runtimeProfileInputs(runtimeProfileForm, agent, agent?.runtime); +}); +runtimeProfileForm?.querySelector('[name="runtime"]')?.addEventListener('change', () => { + const agent = runtimeProfileAgents.find((entry) => entry.areaId === runtimeProfileForm.querySelector('[name="areaId"]').value); + runtimeProfileInputs(runtimeProfileForm, agent, runtimeProfileForm.querySelector('[name="runtime"]').value); +}); +runtimeProfileForm?.addEventListener('click', async (event) => { + const edit = event.target.closest('[data-runtime-profile-edit]'); + if (edit) { + clearRuntimeProfilePreview(runtimeProfileForm); + runtimeProfileStatus(runtimeProfileForm, 'Preview cleared. Review the exact profile before saving.'); + return; + } + + const previewButton = event.target.closest('[data-runtime-profile-preview]'); + if (previewButton) { + const payload = { + areaId: runtimeProfileForm.querySelector('[name="areaId"]').value, + runtime: runtimeProfileForm.querySelector('[name="runtime"]').value, + model: runtimeProfileForm.querySelector('[name="model"]').value.trim(), + reasoning: runtimeProfileForm.querySelector('[name="reasoning"]').value.trim(), + }; + if (!payload.areaId || !payload.runtime) { + runtimeProfileStatus(runtimeProfileForm, 'Choose an identity and an available agent.', true); + return; + } + previewButton.disabled = true; + runtimeProfileStatus(runtimeProfileForm, 'Validating this profile with its selected local adapter…'); + try { + const result = await consoleActionRequest('/api/runtime-profiles/preview', payload); + const plan = result.plan; + runtimeProfileForm.dataset.profilePayload = JSON.stringify(payload); + runtimeProfileForm.dataset.previewToken = result.token; + runtimeProfileForm.dataset.planHash = result.planHash; + for (const field of runtimeProfileForm.querySelectorAll('select, input')) field.disabled = true; + previewButton.hidden = true; + const panel = runtimeProfileForm.querySelector('[data-runtime-profile-review]'); + panel.querySelector('[data-profile-before]').textContent = profileDescription(plan.beforeProfile); + panel.querySelector('[data-profile-after]').textContent = profileDescription(plan.profile); + panel.querySelector('[data-profile-effect]').textContent = `${plan.effect} Reviewed at ${shortCommit(plan.currentHead)}.${plan.resetLaunchPolicy ? ' Switching adapters also clears the prior adapter-specific launch-policy override; the new adapter default applies.' : ''}`; + panel.querySelector('[data-runtime-profile-confirm]').hidden = false; + panel.querySelector('[data-runtime-profile-edit]').hidden = false; + panel.hidden = false; + runtimeProfileStatus(runtimeProfileForm, 'Review the current and next-launch profiles. Nothing has changed yet.'); + } catch (error) { + runtimeProfileStatus(runtimeProfileForm, error.message, true); + } finally { previewButton.disabled = false; } + return; + } + + const confirm = event.target.closest('[data-runtime-profile-confirm]'); + if (!confirm) return; + const token = runtimeProfileForm.dataset.previewToken; + const planHash = runtimeProfileForm.dataset.planHash; + let payload; + try { payload = JSON.parse(runtimeProfileForm.dataset.profilePayload ?? ''); } catch { return; } + if (!token || !planHash) return; + confirm.disabled = true; + runtimeProfileStatus(runtimeProfileForm, 'Saving the reviewed profile for future launches…'); + try { + const result = await consoleActionRequest('/api/runtime-profiles', { ...payload, token, planHash }); + const status = $('#runtime-profile-action-status'); + if (status) { + status.textContent = `Updated ${result.profile.areaId} for future launches. Any running session remains unchanged.`; + status.hidden = false; + } + clearRuntimeProfilePreview(runtimeProfileForm); + await refresh({ completed: runtimeProfileForm }); + } catch (error) { + runtimeProfileStatus(runtimeProfileForm, error.message, true); + confirm.disabled = false; + } +}); + +const scheduleLauncherDetails = $('[data-schedule-launcher]'); +scheduleLauncherDetails?.addEventListener('click', async (event) => { + const edit = event.target.closest('[data-schedule-launcher-edit]'); + if (edit) { + clearScheduleLauncherPreview(); + return; + } + + const previewButton = event.target.closest('[data-schedule-launcher-preview]'); + if (previewButton) { + const action = scheduleLauncherDetails.dataset.action; + if (!['install', 'reconcile'].includes(action) || previewButton.dataset.launcherReady !== 'true') return; + previewButton.disabled = true; + scheduleLauncherStatus('Checking current ownership, unit files, and schedule configuration…'); + try { + const result = await consoleActionRequest('/api/schedule-launcher/preview', { action }); + showScheduleLauncherPlan(scheduleLauncherDetails, result); + } catch (error) { + scheduleLauncherStatus(error.message, true); + } finally { previewButton.disabled = false; } + return; + } + + const confirm = event.target.closest('[data-schedule-launcher-confirm]'); + if (!confirm) return; + const token = scheduleLauncherDetails.dataset.previewToken; + const planHash = scheduleLauncherDetails.dataset.planHash; + const action = scheduleLauncherDetails.dataset.action; + if (!token || !planHash || !['install', 'reconcile'].includes(action)) return; + confirm.disabled = true; + scheduleLauncherStatus('Applying the reviewed timer setup…'); + try { + const result = await consoleActionRequest('/api/schedule-launcher', { action, token, planHash }); + const status = $('#schedule-launcher-action-status'); + if (status) { + const verb = result.action === 'install' ? 'Installed' : 'Refreshed'; + status.textContent = `${verb} the TORCH schedule timer for ${result.systemSchedules.length} system schedule(s). User systemd accepted the enable/start request; active state is not re-queried here. Audit ${result.auditEventId}.`; + status.hidden = false; + } + clearScheduleLauncherPreview('Timer action completed. The refreshed snapshot below reports owned files and configuration digest.'); + await refresh({ completed: scheduleLauncherDetails }); + } catch (error) { + scheduleLauncherStatus(error.message, true); + confirm.disabled = false; + } +}); + +function feedbackStatus(form, message, error = false) { + const status = form.querySelector('.feedback-form-status'); + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); +} + +function showFeedbackPreview(form, result) { + const preview = form.querySelector('[data-feedback-preview]'); + const textarea = form.querySelector('textarea[name="body"]'); + if (!preview || !textarea) return; + const plan = result.preview; + form.dataset.previewToken = result.token; + form.dataset.planHash = result.planHash; + textarea.disabled = true; + preview.innerHTML = ` + +

    ${escapeHtml(plan.body)}

    +

    This creates one durable message linked to this artifact. It does not change task status, priority, or ownership.

    + `; + preview.hidden = false; + feedbackStatus(form, 'Preview ready. Check the recipient and references before sending.'); +} + +$('#artifact-list')?.addEventListener('submit', async (event) => { + const form = event.target.closest('.artifact-feedback-form'); + if (!form) return; + event.preventDefault(); + const textarea = form.querySelector('textarea[name="body"]'); + const button = form.querySelector('button[type="submit"]'); + if (!textarea || !button || !textarea.value.trim()) return; + button.disabled = true; + feedbackStatus(form, 'Preparing a review preview…'); + try { + const artifactId = form.dataset.artifactId; + const result = await feedbackRequest(`/api/artifacts/${encodeURIComponent(artifactId)}/feedback/preview`, { + body: textarea.value, + }); + showFeedbackPreview(form, result); + } catch (error) { + feedbackStatus(form, error.message, true); + } finally { button.disabled = false; } +}); + +$('#artifact-list')?.addEventListener('click', async (event) => { + const edit = event.target.closest('[data-feedback-edit]'); + if (edit) { + const form = edit.closest('.artifact-feedback-form'); + const textarea = form?.querySelector('textarea[name="body"]'); + const preview = form?.querySelector('[data-feedback-preview]'); + if (form && textarea && preview) { + delete form.dataset.previewToken; + delete form.dataset.planHash; + textarea.disabled = false; + preview.hidden = true; + feedbackStatus(form, 'Preview cleared. Review the edited message again before sending.'); + textarea.focus(); + } + return; + } + + const confirm = event.target.closest('[data-feedback-confirm]'); + if (!confirm) return; + const form = confirm.closest('.artifact-feedback-form'); + const textarea = form?.querySelector('textarea[name="body"]'); + const token = form?.dataset.previewToken; + const planHash = form?.dataset.planHash; + if (!form || !textarea || !token || !planHash) return; + confirm.disabled = true; + feedbackStatus(form, 'Sending the reviewed message…'); + try { + const artifactId = form.dataset.artifactId; + const result = await feedbackRequest(`/api/artifacts/${encodeURIComponent(artifactId)}/feedback`, { + body: textarea.value, token, planHash, + }); + const actionStatus = $('#feedback-action-status'); + if (actionStatus) { + actionStatus.textContent = `Feedback sent to ${result.feedback.recipient} for ${result.feedback.taskId} at ${shortCommit(result.feedback.commit)}.`; + actionStatus.hidden = false; + } + await refresh({ completed: form }); + } catch (error) { + feedbackStatus(form, error.message, true); + confirm.disabled = false; + } +}); + +function organizationReviewStatus(form, message, error = false) { + const status = form.querySelector('.organization-review-status'); + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); +} + +function clearOrganizationReview(form, message = 'Preview cleared. Review the current proposal before deciding.') { + delete form.dataset.previewToken; + delete form.dataset.planHash; + form.querySelector('[name="action"]').disabled = false; + form.querySelector('[name="reason"]').disabled = false; + form.querySelector('[data-review-preview]').disabled = false; + form.querySelector('[data-review-preview-panel]').hidden = true; + form.querySelector('[data-review-confirm]').hidden = true; + form.querySelector('[data-review-edit]').hidden = true; + organizationReviewStatus(form, message); +} + +$('#change-list')?.addEventListener('click', async (event) => { + const activationPreviewButton = event.target.closest('[data-hierarchy-activation-preview]'); + if (activationPreviewButton) { + const card = activationPreviewButton.closest('.hierarchy-activation'); + const id = activationPreviewButton.dataset.id; + const status = card?.querySelector('[data-hierarchy-activation-status]'); + const setStatus = (message, error = false) => { + if (!status) return; + status.textContent = message; + status.classList.toggle('is-error', error); + }; + activationPreviewButton.disabled = true; + setStatus('Rechecking owner approval, repository state, and pilot blockers…'); + try { + const result = await consoleActionRequest(`/api/organization-proposals/hierarchy/${encodeURIComponent(id)}/activate/preview`, {}); + const { plan } = result; + const panel = card.querySelector('[data-hierarchy-activation-panel]'); + card.dataset.previewToken = result.token ?? ''; + card.dataset.planHash = result.planHash; + panel.querySelector('[data-hierarchy-activation-title]').textContent = `${plan.confirmationLabel} · ${plan.title}`; + panel.querySelector('[data-hierarchy-activation-effect]').textContent = plan.effect; + panel.querySelector('[data-hierarchy-activation-scope]').textContent = JSON.stringify({ + ownerIdentity: plan.ownerIdentity, + currentHead: plan.currentHead, + pilotPlan: plan.pilotPlan, + }, null, 2); + const confirm = panel.querySelector('[data-hierarchy-activation-confirm]'); + confirm.textContent = plan.confirmationLabel; + confirm.hidden = !result.token || !plan.pilotPlan.canProceed; + panel.hidden = false; + setStatus(plan.pilotPlan.canProceed + ? 'Ready. Review the exact files and commit effect, then explicitly confirm.' + : `Blocked by ${plan.pilotPlan.blockers.length} current condition${plan.pilotPlan.blockers.length === 1 ? '' : 's'}. Resolve them, then preview again.`, + !plan.pilotPlan.canProceed); + } catch (error) { + setStatus(error.message, true); + } finally { activationPreviewButton.disabled = false; } + return; + } + + const activationConfirm = event.target.closest('[data-hierarchy-activation-confirm]'); + if (activationConfirm) { + const card = activationConfirm.closest('.hierarchy-activation'); + const proposalCard = activationConfirm.closest('.organization-proposal'); + const id = proposalCard?.querySelector('[data-hierarchy-activation-preview]')?.dataset.id; + const token = card?.dataset.previewToken; + const planHash = card?.dataset.planHash; + const status = card?.querySelector('[data-hierarchy-activation-status]'); + if (!id || !token || !planHash) return; + activationConfirm.disabled = true; + if (status) { + status.textContent = 'Committing the reviewed hierarchy pilot…'; + status.classList.remove('is-error'); + } + try { + const result = await consoleActionRequest(`/api/organization-proposals/hierarchy/${encodeURIComponent(id)}/activate`, { token, planHash }); + const state = $('#organization-action-status'); + if (state) { + const timerNote = result.proposal.timerReconciliationRequired + ? ' Reconcile the installed timer separately before trusting the new cadence.' + : ' No timer was installed or changed.'; + state.textContent = `${id} is piloting at ${shortCommit(result.proposal.activationCommit)}. No agent or runtime was started.${timerNote}`; + state.hidden = false; + } + await refresh({ completed: card }); + } catch (error) { + if (status) { + status.textContent = error.message; + status.classList.add('is-error'); + } + activationConfirm.disabled = false; + } + return; + } + + const edit = event.target.closest('[data-review-edit]'); + if (edit) { + const form = edit.closest('[data-organization-review]'); + if (form) clearOrganizationReview(form); + return; + } + + const previewButton = event.target.closest('[data-review-preview]'); + if (previewButton) { + const form = previewButton.closest('[data-organization-review]'); + const action = form?.querySelector('[name="action"]')?.value; + if (!form || !action) { + if (form) organizationReviewStatus(form, 'Choose a decision first.', true); + return; + } + previewButton.disabled = true; + organizationReviewStatus(form, 'Checking current proposal and owner authority…'); + try { + const type = form.dataset.type; + const id = form.dataset.id; + const reason = form.querySelector('[name="reason"]').value.trim(); + const result = await consoleActionRequest(`/api/organization-proposals/${encodeURIComponent(type)}/${encodeURIComponent(id)}/preview`, { + action, reason, + }); + const plan = result.plan; + form.dataset.previewToken = result.token; + form.dataset.planHash = result.planHash; + form.querySelector('[name="action"]').disabled = true; + form.querySelector('[name="reason"]').disabled = true; + const panel = form.querySelector('[data-review-preview-panel]'); + panel.querySelector('[data-review-title]').textContent = `${plan.confirmationLabel} · ${plan.title}`; + panel.querySelector('[data-review-effect]').textContent = `${plan.effect} Current state: ${plan.currentState}. Repository: ${shortCommit(plan.currentHead)}.`; + panel.querySelector('[data-review-scope]').textContent = JSON.stringify({ + type: plan.type, id: plan.id, action: plan.action, reason: plan.reason, + proposalRevision: plan.proposalRevision, + baseCommit: plan.baseCommit, currentHead: plan.currentHead, + ownerIdentity: plan.ownerIdentity, + activeGraphRevision: plan.activeGraphRevision, + activeGraphDigest: plan.activeGraphDigest, + scope: plan.scope ?? { + impact: plan.impact, observationWindow: plan.observationWindow, + signals: plan.signals, alternatives: plan.alternatives, + pilot: plan.pilot, roleChanges: plan.roleChanges, + managerCheckInSchedules: plan.managerCheckInSchedules, + timerReconciliation: plan.timerReconciliation, + }, + expectedBenefit: plan.expectedBenefit, evidence: plan.evidence, effect: plan.effect, + }, null, 2); + panel.querySelector('[data-review-confirm]').textContent = plan.confirmationLabel; + panel.querySelector('[data-review-confirm]').hidden = !result.token || plan.canProceed === false; + panel.querySelector('[data-review-edit]').hidden = false; + panel.hidden = false; + organizationReviewStatus(form, plan.canProceed === false + ? 'Blocked. Inspect the current conditions in the plan; no confirmation is available.' + : 'Preview ready. Read the effect and exact proposal scope before confirming.', plan.canProceed === false); + } catch (error) { + organizationReviewStatus(form, error.message, true); + } finally { previewButton.disabled = false; } + return; + } + + const confirm = event.target.closest('[data-review-confirm]'); + if (!confirm) return; + const form = confirm.closest('[data-organization-review]'); + const token = form?.dataset.previewToken; + const planHash = form?.dataset.planHash; + if (!form || !token || !planHash) return; + confirm.disabled = true; + organizationReviewStatus(form, 'Recording the reviewed owner decision…'); + try { + const result = await consoleActionRequest(`/api/organization-proposals/${encodeURIComponent(form.dataset.type)}/${encodeURIComponent(form.dataset.id)}`, { + action: form.querySelector('[name="action"]').value, + reason: form.querySelector('[name="reason"]').value.trim(), token, planHash, + }); + const status = $('#organization-action-status'); + if (status) { + const note = result.type === 'hierarchy-conclusion' + ? demoWorkspace + ? ' Demo decision only. No Git commit, agent or timer was changed.' + : ' One scoped local conclusion commit was recorded. No agent or timer was started or stopped; review obsolete schedules separately.' + : result.type === 'fleet' + ? ' This does not provision or start the proposed identity.' + : result.decision === 'approve-pilot' + ? ' This approves planning only; after reviewing a fresh hierarchy-plan, the owner separately runs hierarchy-activate --by owner --yes. Activation does not start identities or timers.' + : ' The active organization remains unchanged.'; + status.textContent = `${result.proposal.id} recorded as ${result.proposal.state}.${note}`; + status.hidden = false; + } + await refresh({ completed: form }); + } catch (error) { + organizationReviewStatus(form, error.message, true); + confirm.disabled = false; + } +}); + +$('#attention-list')?.addEventListener('click', (event) => { + const link = event.target.closest('a[href^="#"]'); + const target = link && document.getElementById(link.getAttribute('href').slice(1)); + for (let parent = target?.parentElement; parent; parent = parent.parentElement) { + if (parent.tagName === 'DETAILS') parent.open = true; + } +}); + +$('#refresh-console')?.addEventListener('click', () => refresh()); +$('#toggle-live-refresh')?.addEventListener('click', () => { + liveUpdatesPaused = !liveUpdatesPaused; + $('#toggle-live-refresh').textContent = liveUpdatesPaused ? 'Resume updates' : 'Pause updates'; + $('#toggle-live-refresh').setAttribute('aria-pressed', String(liveUpdatesPaused)); + $('#live-refresh-status').textContent = liveUpdatesPaused ? 'Automatic updates paused. Manual refresh preserves edits.' : 'Updates every 15 seconds while visible.'; + if (!liveUpdatesPaused && document.visibilityState === 'visible') refresh(); +}); +let liveRefreshTimer = null; +function startLiveRefreshTimer() { + if (liveRefreshTimer !== null) return; + liveRefreshTimer = setInterval(() => { + if (!liveUpdatesPaused && document.visibilityState === 'visible') refresh({ background: true }); + }, 15_000); +} +startLiveRefreshTimer(); +document.addEventListener('visibilitychange', () => { + if (!liveUpdatesPaused && document.visibilityState === 'visible') refresh(); +}); +globalThis.addEventListener('pagehide', () => { + clearInterval(liveRefreshTimer); + liveRefreshTimer = null; +}); +globalThis.addEventListener('pageshow', (event) => { + startLiveRefreshTimer(); + if (event.persisted && !liveUpdatesPaused && document.visibilityState === 'visible') refresh(); +}); +if (demoWorkspace) { + document.title = 'TORCH Dashboard'; + $('#demo-banner').hidden = false; + $('#reset-demo')?.addEventListener('click', () => { + demoWorkspace.reset(); + globalThis.location.reload(); + }); +} refresh(); diff --git a/site/demo-artifact.svg b/site/demo-artifact.svg new file mode 100644 index 0000000..f5ea3f9 --- /dev/null +++ b/site/demo-artifact.svg @@ -0,0 +1,22 @@ + + + + NorthstarProject home · Wireframe + + + WorkspaceOverview + Active workReviewsActivity + Your project, taking shape. + Public beta · Current milestone + + Active work + Build the activity feedServices · In progress + + Verify offline recoveryQuality · In review + + Needs your review + Project home layoutInterface is waiting on you + Review proposal + Illustrative design artifact · Demo data + + diff --git a/site/demo.html b/site/demo.html new file mode 100644 index 0000000..174a075 --- /dev/null +++ b/site/demo.html @@ -0,0 +1,10 @@ + + + + + + + TORCH Dashboard + +View dashboard demo + diff --git a/site/demo.js b/site/demo.js new file mode 100644 index 0000000..20bfb67 --- /dev/null +++ b/site/demo.js @@ -0,0 +1,302 @@ +// Demo transport for the real Console. State and actions remain in this tab. +(() => { + const clone = (value) => JSON.parse(JSON.stringify(value)); + const canonical = (value) => JSON.stringify(Object.keys(value).sort().map((key) => [key, value[key]])); + function createWorkspace({ clock = () => new Date() } = {}) { + const timestamp = () => clock().toISOString(); + const head = '91c4e2a70000000000000000000000000000000000'; + const profiles = [ + { name: 'codex', title: 'Codex', available: true, model: 'gpt-6-luna', reasoning: 'high' }, + { name: 'claude', title: 'Claude', available: true, model: 'sonnet', reasoning: null }, + { name: 'pi', title: 'Pi', available: true, model: null, reasoning: null }, + ]; + const titles = { + 'session-manager': 'Fleet Operations', 'program-director': 'Program Director', + interface: 'Interface', services: 'Services', qa: 'Quality', documentation: 'Documentation', + }; + let state; + let sequence; + const previews = new Map(); + const preferences = new Map(); + function reset() { + sequence = 0; + previews.clear(); + preferences.clear(); + const summaries = ['Routing handoffs and watching Fleet health', 'Sequencing the first public beta', + 'Waiting for your review of the project home', 'Building the project activity API', + 'Verifying the latest integration', 'Setup guide ready for peer review']; + const agents = Object.entries(titles).map(([areaId, title], index) => ({ + areaId, title, runtime: index === 3 ? 'claude' : index === 5 ? 'pi' : 'codex', + model: index === 3 ? 'sonnet' : index === 5 ? null : 'gpt-6-luna', + reasoning: index === 3 || index === 5 ? null : 'high', + state: index === 2 ? 'waiting' : index === 5 ? 'idle' : 'working', + summary: summaries[index], heartbeatAt: timestamp(), + })); + const task = (id, title, owner, taskState, feature, priority = 'normal') => ({ + id, title, owner, state: taskState, feature, priority, milestone: 'Public beta', revision: 1, + affectedDomains: [owner], dependencies: [], observedAt: head, + description: title + '. Record review evidence against the implementation commit.', + acceptanceCriteria: ['Verify behavior with project checks', 'Record evidence for the owner'], + }); + const backlog = [ + task('APP-12', 'Review the project home', 'interface', 'blocked', 'Project workspace', 'high'), + task('API-08', 'Build the activity feed', 'services', 'in_progress', 'Project workspace', 'high'), + task('QA-06', 'Verify offline recovery', 'qa', 'verification', 'Reliability'), + task('DOC-04', 'Write the installation guide', 'documentation', 'ready', 'Onboarding'), + task('APP-14', 'Keyboard navigation for the work board', 'interface', 'ready', 'Project workspace'), + task('API-05', 'Persist owner review notes', 'services', 'completed', 'Project workspace'), + task('QA-03', 'Test interrupted session recovery', 'qa', 'completed', 'Reliability'), + task('DOC-02', 'Document the quick start', 'documentation', 'completed', 'Onboarding'), + ]; + const domains = agents.slice(2).map((agent) => ({ + id: agent.areaId, title: agent.title, + owned_paths: [agent.areaId === 'interface' ? 'site/**' : agent.areaId + '/**'], + neighbours: ['interface', 'services', 'qa'].filter((id) => id !== agent.areaId), + })); + state = { + schema: 'torch.dev/snapshot/v1alpha1', mode: 'installed', generatedAt: timestamp(), + project: { id: 'torch-demo-northstar', name: 'Northstar' }, + repository: { name: 'Northstar', branch: 'main', head, dirty: false }, + doctor: { healthy: true, findings: [] }, recoverability: { level: 'Recorded' }, + contextLocality: { measured: false, reason: 'Cache savings need a measured comparison.' }, + agents, runtimeAdapters: profiles, backlog, backlogHealth: { findings: [] }, + managerCheckIns: { available: true, managers: [{ + managerId: 'program-director', title: 'Program Director', state: 'working', + attentionRequired: true, cadence: { type: 'interval', seconds: 900 }, findings: [], + directReports: agents.slice(2).map((agent) => ({ + ...agent, task: backlog.find((item) => item.owner === agent.areaId)?.id, + })), approvalWaits: [{ requester: 'interface', approver: 'owner' }], + }] }, + approvalRequests: { available: true, pendingCount: 1, items: [{ + id: 'review-home', revision: 1, requester: 'interface', approver: 'owner', status: 'pending', + title: 'Choose the project home layout', task: 'APP-12', evidence: 'home-wireframe', + summary: 'The proposed home puts your active work and review requests first. Is this the direction you want?', + createdAt: timestamp(), + }] }, + organization: { domains, graph: { revision: 1, roles: [ + { id: 'owner', identity_id: 'owner', title: 'Project owner', kind: 'owner', responsibilities: ['Product direction and final acceptance'] }, + ...agents.map((agent) => ({ + id: agent.areaId, identity_id: agent.areaId, title: agent.title, + kind: agent.areaId === 'session-manager' ? 'fleet-operations' + : agent.areaId === 'program-director' ? 'program-director' : 'specialist', + reports_to: [agent.areaId === 'program-director' || agent.areaId === 'session-manager' ? 'owner' : 'program-director'], + consults_with: domains.filter((domain) => domain.id !== agent.areaId).map((domain) => domain.id), + responsibilities: [agent.summary], authority: ['Assigned role responsibilities'], + })), + ], + implementation_owners: domains.map((domain) => ({ identity_id: domain.id, surface: domain.owned_paths[0] })), + }, lifecycle: { + startupOrder: agents.map((agent) => agent.areaId), + shutdownOrder: agents.map((agent) => agent.areaId).reverse(), blockers: [], + managerIdsByIdentity: Object.fromEntries(domains.map((domain) => [domain.id, ['program-director']])), + } }, + messages: { unacknowledged: 1, recent: [ + { id: 'message-1', sender: 'interface', recipient: 'services', body: 'Can the activity API include task links? The home view can then take the owner directly to the work needing review.', acknowledgedBy: ['services'], createdAt: timestamp() }, + { id: 'message-2', sender: 'services', recipient: 'interface', body: 'Yes. The contract now includes taskId and evidenceId. I have sent the examples to Quality as well.', acknowledgedBy: ['interface'], createdAt: timestamp() }, + { id: 'message-3', sender: 'qa', recipient: 'program-director', body: 'Offline recovery checks passed. The integration receipt is ready; owner acceptance is still open.', acknowledgedBy: [], createdAt: timestamp() }, + ] }, + backlogActivity: { available: true, complete: true, generatedAt: timestamp(), staleDays: 3, scannedCommits: 12, + tasks: [{ taskId: 'APP-12', title: 'Project home owner review', status: 'stale', ageDays: 4, + ownerRequested: true, expectedWaiting: true, closureIntent: [] }] }, + checks: { prepared: [{ id: 'sample-frozen-browser', checkId: 'browser-review', areaId: 'qa', state: 'running', createdAt: timestamp(), + snapshot: { commit: head, digest: 'sample-input-digest', copyStrategy: 'reflink-or-independent-copy' } }, + { id: 'sample-abandoned-browser', checkId: 'interrupted-browser', areaId: 'qa', state: 'abandoned', createdAt: timestamp(), + snapshot: { commit: head, digest: 'sample-abandoned-input-digest', copyStrategy: 'reflink-or-independent-copy' }, + recovery: { recordedAt: timestamp(), cleanupStatus: 'pending', cleanupReason: 'CHECK_SNAPSHOT_INVALID', stoppedExecutors: 'owner-attested' } }], receipts: [ + { checkId: 'unit-tests', areaId: 'services', commit: head, result: 'passed' }, + { checkId: 'recovery-scenario', areaId: 'qa', commit: head, result: 'passed', + snapshot: { commit: head, digest: 'sample-input-digest', copyStrategy: 'reflink-or-independent-copy' }, + conditions: { before: { valid: true, report: { conditions: { visible: true, clockMoving: true } } }, + after: { valid: true, report: { conditions: { visible: true, clockMoving: true } } } } }, + ] }, + artifacts: { available: true, items: [{ + id: 'home-wireframe', title: 'Project home · layout proposal', url: '/demo-artifact.svg', + alt: 'Illustrative project home wireframe with active work and review requests', + taskId: 'APP-12', identityId: 'interface', commit: head, integrity: 'available', createdAt: timestamp(), + feedback: [{ body: 'Keep the work needing my decision easy to find.', recipient: 'interface', createdAt: timestamp() }], + }] }, + integration: [{ queueOrder: 1, sourceArea: 'services', sourceCommit: head, state: 'authorized', + authorizedBy: 'session-manager', reason: 'Exact-commit checks passed; awaiting serialized landing.' }], + deliveries: [{ label: 'Public beta', sourceArea: 'program-director', commit: head, state: 'Awaiting owner acceptance' }], + canonicalFetches: [{ id: 'sample-fetch', remote: 'origin', branch: 'main', commit: head, state: 'succeeded', updatedAt: timestamp(), + attempts: [{ ordinal: 1, state: 'failed', classification: 'transient-network' }, { ordinal: 2, state: 'succeeded', classification: 'verified-object-import' }] }], + deliveryOperations: [{ id: 'sample-deploy', deliveryId: 'public-beta', commit: head, operation: 'deploy', provider: 'sample-adapter', state: 'unknown', updatedAt: timestamp() }], + deliveryAttempts: [{ id: 'sample-attempt', operationId: 'sample-deploy', ordinal: 1, state: 'unknown', + receiptSummary: { status: 'failed', classification: 'transient', effects: 'unknown', hasReference: false } }], + deliveryEvidenceCoverage: 'Sample adapter-reported evidence, not live deployment verification.', + fleetChanges: [{ + id: 'release-specialist', title: 'Add a release specialist', changeType: 'create-domain', + domainId: 'release', proposer: 'session-manager', state: 'proposed', baseCommit: head, + rationale: 'Packaging and release work has become a recurring shared responsibility.', + expectedBenefit: 'Give the beta packaging pipeline one accountable specialist.', + evidence: ['DOC-04', 'QA-06'], revision: 1, + }], hierarchyProposals: [{ + id: 'sample-coordination-pilot', state: 'piloting', proposer: 'session-manager', + proposal: { title: 'Coordination pilot', rationale: 'Test whether a domain lead reduces recurring integration waits.' }, + pilotReviews: [{ id: 'sample-pilot-review', reviewedAt: '2026-09-29T11:00:00Z', + recommendation: 'inconclusive', summary: 'Waits appear shorter, but the current evidence is qualitative.', + window: { complete: false }, activationCommit: head, evidenceVerification: 'reviewer-reported', + comparisons: [{ name: 'Cross-domain wait', unit: 'days median', baseline: '3', observed: 'About 2', + classification: 'qualitative', evidence: ['Sample coordination notes'] }], + success: [{ criterion: 'Reduce cross-domain waiting.', outcome: 'unknown', evidence: ['Sample coordination notes'] }], + stop: [{ criterion: 'Management delays direct collaboration.', outcome: 'not-met', evidence: ['Sample peer handoffs'] }], + }], + }], + worktrees: domains.map((domain) => ({ area: domain.id, branch: 'torch/' + domain.id, + path: '/sample-workspaces/northstar-' + domain.id, ahead: domain.id === 'services' ? 2 : 0, behind: 0 })), + resources: { holders: [{ resourceId: 'browser-check', areaId: 'qa' }], waiters: [] }, + schedules: [], scheduleLauncher: { installed: false }, + managerWakes: { available: true, blockingCount: 1, limit: 100, reservations: [{ + id: 'sample-wake-01', managerId: 'session-manager', scheduleId: 'manager-check-in', + messageId: 'sample-check-in-01', budgetDay: '2026-09-29', + reservedAt: '2026-09-29T10:00:00Z', outcome: 'reserved', + blocksManagerWake: true, managerState: 'waiting', runtimeStoppedVerified: false, + }] }, + providers: { forge: { provider: 'git', status: 'sample' }, + runtimes: profiles.map((profile) => ({ name: profile.name, configured: true })) }, + decisions: { content: 'Prioritize project visibility and recovery for the public beta. Keep specialist communication direct. Review new roles as recurring work emerges.' }, + }; + const section = (items) => ({ available: true, count: items.length, items, truncated: false }); + state.ownerDigest = { id: 'sample-owner-briefing', report: { + schema: 'torch.dev/owner-digest/v1alpha1', generatedAt: timestamp(), + window: { since: new Date(clock().getTime() - 86_400_000).toISOString(), until: timestamp(), hours: 24 }, + needsOwner: section([{ id: 'review-home', title: 'Choose the project home layout', requester: 'interface' }]), + deliveryReview: section([]), lastDeployment: null, shipped: section([]), + landed: section([{ sourceArea: 'services', commit: head }]), + completed: section([{ taskId: 'API-05', title: 'Persist owner review notes' }]), + decisions: section([]), blocked: section([{ taskId: 'APP-12', reason: 'Owner layout review requested' }]), + neglectedRequests: section([]), activityCoverage: { complete: true }, + coverage: ['Sample publication, not a live project report.', 'Structured approval decisions only; source documents are not time-indexed.'], + } }; + } + function message(recipient, body, task = null) { + const item = { id: 'demo-message-' + ++sequence, sender: 'owner', recipient, body, task, + createdAt: timestamp(), acknowledgedBy: [] }; + state.messages.recent.unshift(item); + state.messages.unacknowledged++; + return item; + } + function operation(path, input) { + const route = path.endsWith('/preview') ? path.slice(0, -8) : path; + const taskId = route.match(/^\/api\/backlog\/tasks\/([^/]+)\/priority$/)?.[1]; + const approvalId = route.match(/^\/api\/approvals\/([^/]+)\/decision$/)?.[1]; + const artifactId = route.match(/^\/api\/artifacts\/([^/]+)\/feedback$/)?.[1]; + const proposalId = route.match(/^\/api\/organization-proposals\/fleet\/([^/]+)$/)?.[1]; + const conclusionId = route.match(/^\/api\/organization-proposals\/hierarchy-conclusion\/([^/]+)$/)?.[1]; + const requireItem = (items, id, key = 'id') => { + const item = items.find((entry) => entry[key] === id); + if (!item) throw new Error('That sample record no longer exists. Reset the demo to try again.'); + return item; + }; + if (taskId) { + const task = requireItem(state.backlog, taskId); + if (!['urgent', 'high', 'normal', 'low'].includes(input.priority) || !input.reason?.trim()) throw new Error('Choose a priority and give a reason.'); + return { route, plan: { taskId, title: task.title, from: task.priority, to: input.priority, + revision: task.revision, reason: input.reason, effect: 'Change this sample task’s priority.' }, + apply: () => { task.priority = input.priority; task.revision++; return { task }; } }; + } + if (route === '/api/backlog/tasks') { + if (!input.title?.trim() || !input.description?.trim()) throw new Error('Add a title and description.'); + const task = { ...clone(input), state: 'proposed', owner: null, observedAt: head, revision: 1, + affectedDomains: input.affectedDomains ?? [], dependencies: input.dependencies ?? [], acceptanceCriteria: input.acceptanceCriteria ?? [] }; + return { route, plan: { task, authority: 'Owner proposal.', effect: 'Add an unassigned task to the sample backlog.' }, + apply: () => { task.id = 'OWNER-' + ++sequence; state.backlog.push(task); return { task }; } }; + } + if (route === '/api/owner-requests') { + const agent = requireItem(state.agents, input.recipient, 'areaId'); + if (!input.body?.trim()) throw new Error('Write a request first.'); + const task = input.taskId ? requireItem(state.backlog, input.taskId) : null; + return { route, plan: { ...input, recipientTitle: agent.title, task, currentHead: head, + effect: 'Add your request to this sample agent’s inbox.' }, + apply: () => ({ message: message(agent.areaId, input.body, input.taskId) }) }; + } + if (artifactId) { + const artifact = requireItem(state.artifacts.items, artifactId); + if (!input.body?.trim()) throw new Error('Write your feedback first.'); + const feedback = { body: input.body, recipient: artifact.identityId, taskId: artifact.taskId, commit: artifact.commit }; + return { route, plan: feedback, feedback: true, apply: () => { + const item = { ...feedback, createdAt: timestamp() }; artifact.feedback.push(item); + message(item.recipient, item.body, item.taskId); return { feedback: item }; + } }; + } + if (approvalId) { + const approval = requireItem(state.approvalRequests.items, approvalId); + if (approval.status !== 'pending' || !['approved', 'rejected'].includes(input.decision)) throw new Error('Choose a decision for a pending request.'); + return { route, plan: { ...clone(approval), ...input, approvalId, effect: 'Record your decision in the sample workspace.' }, apply: () => { + approval.status = input.decision; approval.revision++; state.approvalRequests.pendingCount--; + message(approval.requester, approval.title + ': ' + input.decision + '. ' + (input.note ?? ''), approval.task); + state.managerCheckIns.managers[0].approvalWaits = []; + return { approval }; + } }; + } + if (route === '/api/runtime-profiles') { + const agent = requireItem(state.agents, input.areaId, 'areaId'); + const adapter = requireItem(profiles, input.runtime, 'name'); + const profile = { areaId: agent.areaId, runtime: input.runtime, + model: input.model || adapter.model, reasoning: input.reasoning || adapter.reasoning }; + return { route, plan: { beforeProfile: clone(agent), profile, currentHead: head, + effect: 'Change this sample agent’s next-launch profile.', resetLaunchPolicy: agent.runtime !== input.runtime }, + apply: () => { Object.assign(agent, profile); return { profile }; } }; + } + if (conclusionId) { + const proposal = requireItem(state.hierarchyProposals, conclusionId); + if (!['adopt', 'reverse'].includes(input.action) || !input.reason?.trim()) throw new Error('Choose a conclusion and explain your decision.'); + if (proposal.state !== 'piloting') throw new Error('This sample pilot is no longer active.'); + const blocked = input.action === 'adopt'; + const nextGraph = clone(state.organization.graph); + if (!blocked) { + nextGraph.revision++; + for (const role of nextGraph.roles) { + if (role.kind === 'specialist') role.reports_to = ['session-manager']; + } + } + return { route, plan: { type: 'hierarchy-conclusion', id: proposal.id, action: input.action, + title: proposal.proposal.title, reason: input.reason, currentState: proposal.state, + currentHead: head, ownerIdentity: 'owner', canProceed: !blocked, + confirmationLabel: blocked ? 'Commit pilot adoption' : 'Commit pilot reversal', + effect: 'Change only this sample organization. No agent or timer is started or stopped.', + scope: { nextGraph, activeWork: state.backlog.filter((task) => task.state !== 'completed'), + schedulesRetained: true, blockers: blocked ? [{ type: 'adoption-review-required' }] : [] } }, + apply: () => { proposal.state = 'reversed'; state.organization.graph = nextGraph; + return { type: 'hierarchy-conclusion', proposal, decision: input.action }; } }; + } + if (proposalId) { + const proposal = requireItem(state.fleetChanges, proposalId); + if (!['approve', 'reject'].includes(input.action) || (input.action === 'reject' && !input.reason?.trim())) throw new Error('Choose a decision and explain any rejection.'); + return { route, plan: { proposalId, type: 'fleet', action: input.action, title: proposal.title, + id: proposalId, reason: input.reason, currentState: proposal.state, currentHead: head, + baseCommit: head, ownerIdentity: 'owner', proposalRevision: proposal.revision, + confirmationLabel: input.action === 'approve' ? 'Approve for activation review' : 'Reject', + effect: 'Record this sample proposal decision. Creation of the identity is a separate step.', proposal: clone(proposal) }, + apply: () => { proposal.state = input.action === 'approve' ? 'approved' : 'rejected'; + return { type: 'fleet', proposal, decision: input.action }; } }; + } + throw new Error('This operation is unavailable in the demo workspace.'); + } + async function request(path, payload) { + const input = clone(payload); + if (path.endsWith('/preview')) { + const op = operation(path, input); + if (op.plan.canProceed === false) return clone({ plan: op.plan, token: null, planHash: 'sample-blocked' }); + const token = 'demo-review-' + ++sequence; + previews.set(token, { op, input, expiresAt: clock().getTime() + 300000 }); + return clone({ [op.feedback ? 'preview' : 'plan']: op.plan, token, planHash: token }); + } + const record = previews.get(input.token); + const { token, planHash, ...submitted } = input; + if (!record || planHash !== token || clock().getTime() > record.expiresAt + || path !== record.op.route || canonical(submitted) !== canonical(record.input)) { + throw new Error('Review this demo action again before confirming it.'); + } + if (!record.result) record.result = clone(record.op.apply()); + return clone(record.result); + } + reset(); + return { snapshot: () => { state.generatedAt = timestamp(); return clone(state); }, request, reset, + storage: { getItem: (key) => preferences.get(key) ?? null, setItem: (key, value) => preferences.set(key, value) } }; + } + globalThis.TorchDemo = { createWorkspace }; + if (globalThis.location && new URLSearchParams(globalThis.location.search).get('demo') === '1') { + globalThis.TorchConsoleDemo = createWorkspace(); + } +})(); diff --git a/site/index.html b/site/index.html index 31589bc..03bc9a4 100644 --- a/site/index.html +++ b/site/index.html @@ -3,19 +3,22 @@ - - TORCH — one repository, a coordinated AI engineering team + + + TORCH — an AI fleet shaped by your project +
    - T TORCH + TORCH
    @@ -23,92 +26,92 @@
    -

    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.

    +

    A project-aware AI fleet

    +

    An AI team shaped by your project.

    +

    TORCH studies your repository and goals, then proposes the sessions your work needs. Choose Codex, Claude, Pi, or another local runtime for each role—and keep the team coordinated as the project changes.

    -
    +
    -

    Example fleet · Request CR-184

    - +

    From project brief to checked work

    +
      -
    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. +
    11. P
      Project + specificationTORCH maps code, tests, constraints, and stated goals.
    12. +
    13. T
      Proposed teamYou review domains, responsibilities, and reporting lines.
    14. +
    15. AI
      Your runtime for each sessionCodex, Claude, Pi, or a trusted local adapter.
      Direct peer links
    16. +
    17. ↔
      Shared coordinationSessions exchange messages, status, tasks, and evidence.
    18. +
    19. ✓
      Checked integrationWork converges through an authorized, serialized path.
    -

    Ready at owner request.

    +

    Start with the repository. The proposed fleet stays yours to shape.

    -

    Git required

    GitHub optional

    Provider independent

    Local first

    Human approved

    +

    Built around your repo

    Codex · Claude · Pi

    Choose per session

    Local-first control

    Owner-shaped fleet

    -

    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.

    +

    The project shapes the organization.

    +

    TORCH does not start from a fixed roster. It studies the repository and specifications you choose, then proposes a team you can split, merge, rename, or reject before anything is installed.

    - Input -

    Code and intent

    -

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

    + Understand +

    Read the real project

    +

    Architecture, tests, working practices, shared files, and your selected specifications become evidence for a proposed fleet.

    - Design -

    Explicit boundaries

    -

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

    + Coordinate +

    Give each session a lane

    +

    Roles get explicit scope, exclusions, neighbors, checks, and worktree boundaries. Each session can use the runtime and model that fit its job.

    - Operate -

    Persistent specialists

    -

    Stable identities survive runtime sessions. When a durable new boundary emerges, the manager can propose another specialist—but only owner approval wires it into the Fleet.

    + Grow +

    Change shape when needed

    +

    As coordination grows, TORCH can recommend a lead or new specialist when there is real integration work to own. You review the evidence and approve every organization change.

    -

    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.

    +

    Keep the right context close to the work.

    +

    Each long-lived session can focus on a coherent part of the project, while durable messages and shared records connect it to the rest of the fleet. The aim is less context thrash—not a set of agents trapped in silos.

    +

    Lower cache and context costs are a hypothesis TORCH still needs to measure.

    -

    Proof has a commit.

    -

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

    +

    Trust comes from evidence.

    +

    TORCH tracks what passed, what is still a live gate, and which exact change the evidence covers. A working local scenario is not the same as a qualified production fleet.

    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 successorIsolated build + activationStable boot open
    -
    COMBATRIG migration33 areas, 570 tasks inventoriedBoundaries open
    +
    Project analysis and fleet designRepository/spec scenariosProven locally
    +
    Shared messages, status, and tasksDurable local control planeProven locally
    +
    Safe checks and integrationExact-commit, serialized flowProven locally
    +
    Codex, Claude, Pi, and local adaptersAdapter and coordination scenariosLive provider gate open
    +
    TORCH managing its own repositoryIsolated candidate checksClean install gate open
    +
    COMBATRIG importRead-only inventory; ownership review neededMigration gated
    -

    Start with the work you actually have.

    -

    Point TORCH at a Git repository, a product specification, or both. Bootstrap gives your Session Architect AI a bounded evidence brief; installation waits for your reviewed proposal.

    +

    Start with the project you already have.

    +

    Point TORCH at a Git repository and, if useful, a product specification. It prepares an evidence brief for a fleet proposal; installing or starting the team waits for your review and approval.

    @@ -127,7 +130,7 @@ diff --git a/site/live-refresh.js b/site/live-refresh.js new file mode 100644 index 0000000..1c217ee --- /dev/null +++ b/site/live-refresh.js @@ -0,0 +1,51 @@ +(() => { + function createScheduler({ load, apply, onError = () => {} }) { + let generation = 0; + let running = null; + function request({ supersede = true } = {}) { + if (running && !supersede) return running; + generation += 1; + if (running) return running; + running = (async () => { + let observed; + do { + observed = generation; + try { + const snapshot = await load(); + if (observed === generation) apply(snapshot); + } catch (error) { if (observed === generation) onError(error); } + } while (observed !== generation); + })().finally(() => { running = null; }); + return running; + } + return Object.freeze({ request }); + } + function createProtection(document) { + const baseline = new WeakMap(); + const fields = (element) => [...element.querySelectorAll('input,select,textarea')]; + const signature = (field) => JSON.stringify({ value: field.value, checked: field.checked, + selected: field.tagName === 'SELECT' ? [...field.selectedOptions].map((option) => option.value) : null }); + const dirty = () => new Set(fields(document).filter((field) => baseline.has(field) && baseline.get(field) !== signature(field))); + const remember = ({ preserve = new Set() } = {}) => fields(document).forEach((field) => { + if (!preserve.has(field)) baseline.set(field, signature(field)); + }); + function protects(element) { + if (!element) return false; + const focused = document.activeElement; + if (element.contains(focused) && focused?.matches('input,select,textarea,button,a,summary')) return true; + if (element.matches('[data-preview-token]') || element.querySelector('[data-preview-token]')) return true; + return fields(element).some((field) => baseline.has(field) && baseline.get(field) !== signature(field)); + } + function complete(element) { + if (!element) return; + fields(element).forEach((field) => baseline.set(field, signature(field))); + [element, ...element.querySelectorAll('[data-preview-token]')].forEach((node) => { + delete node.dataset.previewToken; + delete node.dataset.planHash; + }); + if (element.contains(document.activeElement)) document.activeElement.blur(); + } + return Object.freeze({ remember, dirty, protects, complete }); + } + globalThis.TorchLiveRefresh = Object.freeze({ createScheduler, createProtection }); +})(); diff --git a/site/operation-outcomes.js b/site/operation-outcomes.js new file mode 100644 index 0000000..0e2b40a --- /dev/null +++ b/site/operation-outcomes.js @@ -0,0 +1,29 @@ +(() => { + const text = (value) => String(value ?? '').slice(0, 1200).replace(/[&<>"']/g, + (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]); + const status = (state, type) => state === 'running' ? 'Running — completion unconfirmed' + : state === 'unknown' ? 'Outcome unknown — review required' + : state === 'succeeded' && type === 'delivery' ? 'Adapter succeeded — verify lifecycle and live evidence' + : state === 'succeeded' ? 'Object import succeeded — not deployment' + : text(state ?? 'not recorded'); + function render(snapshot) { + const fetches = (snapshot.canonicalFetches ?? []).slice(0, 20); + const operations = (snapshot.deliveryOperations ?? []).slice(0, 40); + const attempts = snapshot.deliveryAttempts ?? []; + return `

    Canonical fetch outcomes

    Object imports do not move branches or deploy releases. Latest 20 recorded operations.

    + ${fetches.length ? fetches.map((item) => `
    ${text(item.remote)} · ${status(item.state, 'fetch')} +

    Branch ${text(item.branch)} · commit ${text(item.commit)}

    ${text(item.updatedAt)}

    +
      ${(item.attempts ?? []).slice(0, 5).map((attempt) => `
    • Attempt ${text(attempt.ordinal)}: ${status(attempt.state, 'fetch')} · ${text(attempt.classification ?? 'result not recorded')}
    • `).join('')}
    +
    `).join('') : '

    No canonical fetch receipts recorded.

    '} +

    Delivery operation outcomes

    Uncertain or interrupted external actions need review. No automatic retry or recovery is offered here.

    + ${operations.length ? operations.map((item) => `
    ${text(item.operation)} · ${text(item.provider)} · ${status(item.state, 'delivery')} +

    Delivery ${text(item.deliveryId)} · commit ${text(item.commit)} · ${text(item.updatedAt)}

    +
      ${attempts.filter((attempt) => attempt.operationId === item.id).slice(0, 5).map((attempt) => { + const receipt = attempt.receiptSummary; + return `
    • Attempt ${text(attempt.ordinal)}: ${status(attempt.state, 'delivery')} +

      ${receipt ? `Receipt: ${text(receipt.status)}${receipt.classification ? ` · ${text(receipt.classification)} failure` : ''}${receipt.effects ? ` · effects ${text(receipt.effects)}` : ''}${receipt.reason ? ` · ${text(receipt.reason)}` : ''}. ${receipt.hasReference ? 'Adapter reference recorded; not exposed in this view.' : 'No adapter reference recorded.'}` : 'Receipt summary unavailable.'}

    • `; + }).join('')}
    `).join('') : '

    No delivery operation receipts recorded. Landing is not deployment.

    '} +

    ${text(snapshot.deliveryEvidenceCoverage ?? 'Receipt history may be incomplete. Inspect CLI evidence for details.')}

    `; + } + globalThis.TorchOperationOutcomes = Object.freeze({ render }); +})(); diff --git a/site/owner-digest.js b/site/owner-digest.js new file mode 100644 index 0000000..bbe9447 --- /dev/null +++ b/site/owner-digest.js @@ -0,0 +1,50 @@ +// Shared, read-only briefing renderer. Never interpret project Markdown as HTML. +(() => { + const text = (value) => String(value ?? '').slice(0, 1200).replace(/[&<>"']/g, (character) => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + })[character]); + const count = (value) => Number.isInteger(value) && value >= 0 ? value : null; + function list(title, section, describe) { + const items = Array.isArray(section?.items) ? section.items.slice(0, 100) : []; + let body; + if (section?.available !== true) body = '

    Evidence unavailable.

    '; + else if (!items.length) body = '

    No matching recorded items.

    '; + else body = `
      ${items.map((item) => `
    • ${describe(item)}
    • `).join('')}
    `; + const total = count(section?.count); + if (section?.truncated || (total !== null && total > items.length)) body += `

    Showing ${items.length} of ${total ?? 'an unknown total'}.

    `; + return `

    ${title}

    ${body}
    `; + } + function render(publication, { now = new Date() } = {}) { + const report = publication?.report; + if (report?.schema !== 'torch.dev/owner-digest/v1alpha1' || !Number.isFinite(Date.parse(report.generatedAt))) { + return '

    No published briefing

    Generate a local report with torch digest build. To save it for this view, the owner can review and run torch digest publish --by owner --yes.

    Publication does not send a message, deploy work or install a timer.

    '; + } + const elapsed = now.getTime() - Date.parse(report.generatedAt); + const hours = Number.isFinite(report.window?.hours) && report.window.hours > 0 ? report.window.hours : 24; + const freshness = elapsed < -300_000 ? 'Check publication clock' : elapsed > hours * 2 * 3_600_000 ? 'Older publication' : 'Saved publication'; + const deployment = report.lastDeployment; + const states = { unknown: 'Outcome unknown', running: 'Execution unresolved', failed: 'Failed', + succeeded: 'Success reported; local reconciliation pending', applied: 'Receipt applied; live verification separate', + 'reviewed-not-applied': 'Owner confirmed no deployment' }; + return `

    ${freshness}

    +

    Window: ${text(report.window?.since)} to ${text(report.window?.until)} (UTC). Recorded evidence, not independent live verification.

    +

    This briefing stays as published. Approvals and work may have changed since then.

    + ${report.activityCoverage?.complete === false ? '

    Activity history is incomplete; inactivity is not proven.

    ' : ''} +
    ${list('Needs you', report.needsOwner, (item) => `${text(item.title)}${text(item.requester)} · ${text(item.id)}`)} + ${list('Delivery needs review', report.deliveryReview, (item) => `${text(item.operation)}: ${text(states[item.state] ?? item.state)}${text(item.commit)}`)}
    +

    Review current approvals and session waits

    +

    Last deployment

    ${deployment + ? `

    ${text(deployment.label)} — ${text(states[deployment.state] ?? deployment.state)}

    ${text(deployment.commit)}${deployment.failureReason ? `

    ${text(deployment.failureReason)}

    ` : ''}` + : '

    No deployment operation recorded.

    '}
    +
    Shipping and integration + ${list('Shipped — receipt reported', report.shipped, (item) => `${text(item.label ?? item.deliveryId)}${text(item.state)} · ${text(item.recordedAt)}${text(item.commit)}`)} + ${list('Landed — not necessarily deployed', report.landed, (item) => `${text(item.sourceArea)}${text(item.commit)}`)} + ${list('Completed tasks', report.completed, (item) => `${text(item.title)}${text(item.taskId)}`)}
    +
    Decisions, blockers and neglected requests + ${list('Recorded approval decisions', report.decisions, (item) => `${text(item.title)}${text(item.status)} · ${text(item.decidedBy)}`)} + ${list('Blocked work', report.blocked, (item) => `${text(item.taskId)}${text(item.reason)}`)} + ${list('Neglected owner requests', report.neglectedRequests, (item) => `${text(item.title)}${text(item.taskId)} · ${text(item.ageDays)} days${item.expectedWaiting ? ' · recorded blocked wait' : ''}`)}
    +
    Evidence coverage
      ${(Array.isArray(report.coverage) ? report.coverage : []).slice(0, 100).map((note) => `
    • ${text(note)}
    • `).join('')}
    `; + } + globalThis.TorchOwnerDigest = Object.freeze({ render }); +})(); diff --git a/site/styles.css b/site/styles.css index 24cdfa6..0a87989 100644 --- a/site/styles.css +++ b/site/styles.css @@ -1,14 +1,19 @@ :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); + color-scheme: dark; + --surface: #101a22; + --paper: #1a2a34; + --ink: #e8f0f4; + --muted: #adbec8; + --line: #3a505c; + --signal: #ff8154; + --route: #76b2ff; + --field: #70d2a2; + --deep: #0b141b; + --raised: #223944; + --warning-surface: #39231d; + --warning-ink: #ffd0bd; + --success-surface: #1b342b; + --shadow: 0 18px 50px rgba(0, 0, 0, 0.34); --display: "Bahnschrift", "DIN Alternate", "Arial Narrow", sans-serif; --body: "Source Sans 3", "Noto Sans", "Segoe UI", sans-serif; } @@ -18,13 +23,44 @@ 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 { position: fixed; left: 1rem; top: -5rem; z-index: 30; background: var(--route); color: var(--deep); padding: .65rem 1rem; } .skip-link:focus { top: 1rem; } :focus-visible { outline: 3px solid var(--signal); outline-offset: 3px; } +.owner-briefing { max-width: 100%; } +.check-evidence { padding-block: .75rem; border-bottom: 1px solid var(--line); min-width: 0; } +.check-evidence summary { cursor: pointer; overflow-wrap: anywhere; } +.check-evidence p, .check-evidence code, .check-evidence dd { overflow-wrap: anywhere; white-space: normal; } +.check-evidence dl { margin-block: .5rem; } +.check-evidence dt { font-weight: 600; } +#activity-review p { max-width: 72ch; } +#operation-outcomes p { max-width: 72ch; } +.check-evidence dd { margin-inline-start: 1rem; } +.owner-briefing p { max-width: 72ch; } +.owner-briefing code { overflow-wrap: anywhere; white-space: normal; } +.briefing-publication > p:first-child { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; } +.briefing-publication time, .briefing-muted { color: var(--muted); } +.briefing-focus { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 2rem; margin-top: 1.5rem; } +.briefing-focus > :first-child { border-left: 3px solid var(--signal); padding-left: 1.1rem; } +.briefing-block h3, .briefing-deployment h3 { font-size: 1.15rem; line-height: 1.3; } +.briefing-block ul { list-style: none; padding: 0; margin: .7rem 0; } +.briefing-block li { padding: .7rem 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; } +.briefing-block li strong, .briefing-block li span, .briefing-block li code { display: block; } +.briefing-block li span { color: var(--muted); font-size: .9rem; margin-top: .25rem; } +.briefing-deployment { margin-top: 1.6rem; padding-block: 1rem; border-top: 1px solid var(--line); } +.briefing-detail { border-top: 1px solid var(--line); padding-block: .9rem; } +.briefing-detail summary { cursor: pointer; font-weight: 600; } +.briefing-detail .briefing-block { margin-top: 1rem; } +.briefing-warning { color: var(--warning-ink); border-left: 3px solid var(--signal); padding-left: 1rem; } +.briefing-empty { padding: 1rem 0; } +@media (max-width: 700px) { + .console-section.owner-briefing { scroll-margin-top: 5.5rem; } + .briefing-focus { grid-template-columns: minmax(0, 1fr); gap: 1rem; } +} + .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%); } +.wordmark img { display: block; width: 2.35rem; height: 2.35rem; object-fit: contain; } nav { display: flex; gap: clamp(1rem, 3vw, 2.4rem); font-size: .92rem; } nav a { text-decoration: none; } nav a:hover { text-decoration: underline; } @@ -32,14 +68,14 @@ 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); } +.status-line span { width: .72rem; height: .72rem; border-radius: 50%; background: var(--field); box-shadow: 0 0 0 .28rem rgba(112, 210, 162, .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; } +.lede { max-width: 630px; font-size: clamp(1.1rem, 1.6vw, 1.38rem); color: var(--muted); 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 { display: inline-flex; background: var(--signal); color: var(--deep); padding: .82rem 1.15rem; text-decoration: none; font-weight: 700; } .primary-action:hover { background: var(--route); } .text-action { font-weight: 650; } @@ -47,13 +83,13 @@ h3 { font-size: 1.55rem; line-height: 1.15; } .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; } +.dispatch-head button:hover, .console-nav button:hover, .copy-button:hover { background: var(--raised); color: var(--ink); } .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-active .node { background: var(--signal); color: var(--deep); border-color: var(--signal); transform: scale(1.12); } +.route-step.is-complete .node { background: var(--field); color: var(--deep); 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; } @@ -66,24 +102,24 @@ h3 { font-size: 1.55rem; line-height: 1.15; } .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; } +.section-intro p { margin: 0; max-width: 650px; font-size: 1.18rem; color: var(--muted); } .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 { min-height: 330px; padding: clamp(1.5rem, 3vw, 3rem); background: rgba(26, 42, 52, .94); 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; } +.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(--deep); color: var(--ink); } +.context-section p { max-width: 650px; color: var(--muted); 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); } +.wide-context { position: relative; display: grid; place-items: center; height: 250px; border: 1px solid var(--muted); 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; } +.churn.third { transform: rotate(66deg); background: var(--line); } +.versus { color: var(--muted); 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); } +.focused-context span { display: grid; place-items: center; min-height: 74px; border: 1px solid var(--muted); background: var(--raised); font-family: var(--display); } .evidence-section { background: var(--paper); } .ledger { border-top: 2px solid var(--ink); } @@ -91,52 +127,431 @@ h3 { font-size: 1.55rem; line-height: 1.15; } .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; } +.pending { color: var(--signal); 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; } +.command-panel { background: var(--deep); color: var(--ink); box-shadow: var(--shadow); } +.command-tabs { display: flex; border-bottom: 1px solid var(--line); } +.command-tabs button { border: 0; border-right: 1px solid var(--line); background: transparent; color: var(--muted); padding: .75rem 1rem; cursor: pointer; } +.command-tabs button[aria-selected="true"] { background: var(--raised); color: var(--ink); } 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; } +.copy-button { margin: 0 1.4rem 1.4rem; border-color: var(--muted); color: var(--ink); } 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(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; } -.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); } +.console-body { min-height: 100vh; background: var(--surface); font-size: 15px; } +.console-nav { min-height: 68px; gap: 1.4rem; padding-inline: clamp(1rem, 2.4vw, 2.5rem); background: var(--paper); } +.console-project { min-width: 0; display: grid; gap: .05rem; margin-right: auto; } +.console-project strong { font: 650 1.05rem/1.15 var(--display); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.console-project span, .console-tools > span { color: var(--muted); font-size: .78rem; } +.console-tools { display: flex; align-items: center; gap: 1rem; } +.console-tools button { border: 1px solid var(--route); background: var(--route); color: var(--deep); padding: .48rem .75rem; cursor: pointer; font-weight: 650; } +.console-tools button:hover { background: var(--route); border-color: var(--route); } +.console-layout { display: grid; grid-template-columns: 172px minmax(0, 1fr); max-width: 1720px; margin-inline: auto; min-height: calc(100vh - 68px); } +.console-rail { position: sticky; top: 0; align-self: start; display: grid; gap: .2rem; padding: 1.5rem .75rem; border-right: 1px solid var(--line); } +.console-rail a { padding: .55rem .7rem; color: var(--muted); font-weight: 650; text-decoration: none; border-left: 3px solid transparent; } +.console-rail a:hover, .console-rail a:focus-visible { color: var(--ink); background: var(--paper); border-left-color: var(--route); } +.rail-note { margin: 2rem .7rem 0; color: var(--muted); font-size: .76rem; line-height: 1.7; } +.rail-note span { color: var(--ink); font-family: ui-monospace, monospace; } +.console-workspace { min-width: 0; padding: 0 clamp(1rem, 2.5vw, 2.5rem) 3rem; } +.console-error { margin: 1.2rem 0 0; background: var(--warning-surface); color: var(--warning-ink); border-left: 4px solid var(--signal); padding: 1rem; } +.pulse-zone { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(270px, .8fr); border-bottom: 1px solid var(--ink); } +.pulse-main { min-width: 0; padding: 2rem 2rem 1.6rem 0; } +.pulse-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.25rem; } +.section-kicker { margin: 0 0 .35rem; color: var(--muted); font-size: .78rem; font-weight: 700; } +.pulse-heading h1, .section-heading h2 { max-width: none; font-size: clamp(1.75rem, 2.8vw, 2.5rem); line-height: 1.05; letter-spacing: -.035em; } +.health-flag { display: inline-flex; align-items: center; border: 1px solid var(--field); color: var(--field); padding: .25rem .55rem; font-size: .78rem; font-weight: 700; } +.pulse-summary { max-width: 60ch; margin: 1rem 0 1.65rem; color: var(--muted); font-size: 1.05rem; } +.pulse-readouts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--line); } +.pulse-readouts div { min-width: 0; display: grid; align-content: start; gap: .3rem; padding: .8rem .8rem .2rem 0; } +.pulse-readouts div + div { padding-left: .8rem; border-left: 1px solid var(--line); } +.pulse-readouts span { color: var(--muted); font-size: .74rem; } +.pulse-readouts strong { overflow-wrap: anywhere; font-family: var(--display); font-size: .95rem; } +.attention-panel { padding: 1.45rem 0 1rem 1.35rem; border-left: 1px solid var(--line); } +.panel-heading, .section-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; } +.panel-heading { padding-bottom: .65rem; border-bottom: 1px solid var(--line); } +.panel-heading h2, .panel-heading h3 { margin: 0; font: 650 1rem/1.2 var(--display); } +.panel-heading span, .section-heading > span { color: var(--muted); font-size: .76rem; text-align: right; } +.attention-list { display: grid; } +.attention-item { display: grid; grid-template-columns: 9px 1fr; gap: .7rem; padding: .8rem 0; border-bottom: 1px solid var(--line); } +.attention-mark { width: 7px; height: 7px; margin-top: .45rem; background: var(--field); } +.tone-urgent .attention-mark { background: var(--signal); } +.tone-review .attention-mark { background: var(--route); } +.tone-decision .attention-mark { background: #f3bd55; } +.attention-item strong { display: block; font-size: .86rem; overflow-wrap: anywhere; } +.attention-item p { margin: .18rem 0; color: var(--muted); font-size: .78rem; line-height: 1.4; } +.attention-item a { color: var(--route); font-size: .76rem; font-weight: 650; } +.board-section, .console-section { padding-block: 1.65rem; border-bottom: 1px solid var(--line); scroll-margin-top: 1rem; } +.section-heading { margin-bottom: 1rem; } +.section-heading h2 { margin: 0; } +.work-view-tools { margin: -.15rem 0 1rem; padding: .8rem .85rem .55rem; border-top: 1px solid var(--line); border-left: 3px solid var(--route); background: #172832; } +.work-filter-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); gap: .55rem; align-items: end; } +.search-filter { grid-column: span 2; } +.work-filter-row label, .saved-view-actions label { min-width: 0; display: grid; gap: .2rem; color: var(--muted); font-size: .68rem; font-weight: 700; } +.work-filter-row input, .work-filter-row select, .saved-view-actions input { width: 100%; min-width: 0; height: 2.25rem; padding: .35rem .5rem; border: 1px solid var(--line); border-radius: 0; background: var(--deep); color: var(--ink); font: 500 .78rem/1.25 var(--body); } +.work-filter-row select, .saved-view-actions button { cursor: pointer; } +.saved-view-actions { display: flex; align-items: end; flex-wrap: wrap; gap: .55rem; padding-top: .65rem; } +.saved-view-actions label { width: min(100%, 220px); } +.saved-view-actions button { min-height: 2.25rem; padding: .4rem .68rem; border: 1px solid var(--route); background: var(--route); color: var(--deep); font: 650 .74rem/1.2 var(--body); } +.saved-view-actions button:hover:not(:disabled) { border-color: var(--route); background: var(--route); } +.saved-view-actions button.quiet-action, .work-filter-row button.quiet-action { background: transparent; color: var(--ink); } +.saved-view-actions button.quiet-action:hover:not(:disabled), .work-filter-row button.quiet-action:hover:not(:disabled) { border-color: var(--line); background: var(--raised); color: var(--ink); } +.saved-view-actions button[hidden] { display: none; } +.work-filter-row button { min-height: 2.25rem; padding: .4rem .6rem; border: 1px solid var(--ink); background: transparent; color: var(--ink); font: 650 .72rem/1.2 var(--body); } +.view-storage-note, .work-view-status, .work-view-summary { margin: 0; color: var(--muted); font-size: .7rem; } +.view-storage-note { align-self: center; max-width: 32ch; padding: 0 .3rem; } +.work-view-status:not(:empty) { padding-top: .45rem; color: var(--signal); } +.work-view-summary { padding-top: .15rem; font-weight: 650; } +.initiative-note { max-width: 88ch; margin: -.25rem 0 .8rem; color: var(--muted); font-size: .75rem; } +.initiative-rollup { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; } +.initiative-rollup-columns { min-width: 0; display: grid; grid-column: 1 / -1; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; } +.initiative-column { min-width: 0; border-top: 1px solid var(--ink); } +.initiative-column > header { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; padding: .55rem .1rem; border-bottom: 1px solid var(--line); } +.initiative-column > header h3 { font: 650 1rem/1.2 var(--display); } +.initiative-column > header span { color: var(--muted); font-size: .7rem; } +.initiative-list { display: grid; } +.initiative-row { border-bottom: 1px solid var(--line); } +.initiative-row > summary { display: grid; grid-template-columns: minmax(0, 1.3fr) auto minmax(80px, .65fr) minmax(100px, 1fr); align-items: center; gap: .65rem; padding: .62rem .1rem; cursor: pointer; list-style-position: inside; } +.initiative-row > summary::marker { color: var(--route); } +.initiative-row > summary strong { min-width: 0; font: 650 .83rem/1.2 var(--display); overflow-wrap: anywhere; } +.initiative-row progress { width: 100%; height: .45rem; accent-color: var(--field); } +.initiative-row .initiative-state { width: fit-content; padding: .12rem .38rem; border: 1px solid var(--line); color: var(--muted); font-size: .66rem; } +.initiative-row .initiative-state.is-risk { border-color: var(--signal); color: var(--signal); } +.initiative-row .initiative-count { color: var(--muted); font-size: .68rem; text-align: right; } +.initiative-details { display: grid; gap: .3rem; padding: 0 .4rem .7rem 1.35rem; } +.initiative-details p { margin: 0; color: var(--muted); font-size: .7rem; } +.initiative-details ul { display: grid; gap: .25rem; margin: 0; padding: 0; list-style: none; } +.initiative-details li { display: grid; grid-template-columns: minmax(0, 1.2fr) auto auto; gap: .65rem; border-top: 1px solid var(--line); padding-top: .3rem; font-size: .7rem; } +.initiative-details li > span:first-child { overflow-wrap: anywhere; } +.initiative-details code, .initiative-details li > span:last-child { color: var(--muted); font-size: .65rem; } +.backlog-board { display: grid; grid-template-columns: repeat(5, minmax(165px, 1fr)); gap: .7rem; align-items: start; overflow-x: auto; padding-bottom: .6rem; } +.board-lane { min-width: 165px; background: rgba(26, 42, 52, .64); border-top: 2px solid var(--line); } +.board-lane:nth-child(2) { border-top-color: var(--route); } +.board-lane:nth-child(3) { border-top-color: var(--field); } +.board-lane:nth-child(4) { border-top-color: var(--signal); } +.board-lane:nth-child(5) { border-top-color: var(--ink); } +.board-lane > header { display: flex; justify-content: space-between; align-items: baseline; padding: .65rem .6rem; } +.board-lane > header h3 { margin: 0; font: 650 .9rem/1.2 var(--display); } +.board-lane > header span { min-width: 1.5rem; padding: .04rem .3rem; text-align: center; color: var(--muted); font-size: .75rem; } +.lane-items { display: grid; gap: .55rem; padding-inline: .4rem; } +.task-ticket { min-width: 0; padding: .68rem; background: var(--paper); border: 1px solid var(--line); border-left: 2px solid var(--route); } +.task-ticket h4 { margin: .45rem 0 .3rem; font: 650 .95rem/1.24 var(--display); } +.task-ticket > p { margin: 0; color: var(--muted); font-size: .78rem; line-height: 1.42; overflow-wrap: anywhere; } +.evidence-warning { margin-top: .55rem; padding: .42rem .5rem; border-left: 2px solid var(--signal); background: var(--warning-surface); color: var(--warning-ink); } +.evidence-warning strong { display: block; font-size: .72rem; } +.evidence-warning p { margin: .1rem 0 0; font-size: .69rem; line-height: 1.35; } +.ticket-topline { display: flex; justify-content: space-between; align-items: center; gap: .5rem; } +.ticket-topline code, .ticket-meta code { font-size: .7rem; color: var(--muted); } +.priority { padding: .08rem .32rem; font-size: .68rem; text-transform: capitalize; border: 1px solid var(--line); color: var(--muted); } +.priority-urgent, .priority-high { border-color: var(--signal); color: var(--signal); } +.ticket-meta { display: grid; gap: .25rem; margin: .6rem 0 0; font-size: .7rem; } +.ticket-meta div { display: grid; grid-template-columns: 4.6rem minmax(0, 1fr); gap: .35rem; } +.ticket-meta dt { color: var(--muted); } +.ticket-meta dd { margin: 0; overflow-wrap: anywhere; } +.task-ticket details { margin-top: .55rem; border-top: 1px solid var(--line); padding-top: .45rem; font-size: .73rem; } +.task-ticket details summary, .more-details summary { cursor: pointer; font-weight: 650; } +.priority-change-form { display: grid; gap: .5rem; padding-top: .55rem; } +.priority-change-form label { display: grid; gap: .2rem; color: var(--muted); font-size: .68rem; font-weight: 700; } +.priority-change-form input, .priority-change-form select { width: 100%; min-height: 2rem; padding: .35rem .45rem; border: 1px solid var(--line); border-radius: 0; background: var(--deep); color: var(--ink); font: 500 .75rem/1.25 var(--body); } +.priority-change-form button { width: fit-content; min-height: 2rem; padding: .35rem .55rem; border: 1px solid var(--route); background: var(--route); color: var(--deep); font: 650 .7rem/1.2 var(--body); cursor: pointer; } +.priority-change-form button:hover:not(:disabled) { border-color: var(--route); background: var(--route); } +.priority-change-form button:disabled { cursor: wait; opacity: .62; } +.priority-change-status, .priority-action-status, .owner-control-note { margin: 0; color: var(--muted); font-size: .7rem; } +.priority-change-status.is-error { color: var(--signal); } +.priority-action-status { margin-top: .75rem; padding: .55rem .7rem; border-left: 3px solid var(--field); background: var(--success-surface); } +.priority-action-status[hidden] { display: none; } +.owner-control-note { padding-top: .4rem; } +.task-create-panel { margin: 1rem 0 1.4rem; padding: .85rem 1rem; border: 1px solid var(--line); border-left: 3px solid var(--signal); background: var(--deep); } +.task-create-panel > summary { width: fit-content; color: var(--ink); font-family: var(--display); font-size: 1.05rem; font-weight: 700; cursor: pointer; } +.task-create-panel[open] > summary { color: var(--signal); } +.task-create-form { display: grid; gap: .7rem; max-width: 960px; margin-top: .6rem; } +.task-create-form fieldset { display: grid; gap: .7rem; min-width: 0; padding: 0; border: 0; } +.task-create-form label { display: grid; gap: .25rem; color: var(--muted); font-size: .76rem; font-weight: 700; } +.task-create-form input, .task-create-form textarea, .task-create-form select { width: 100%; min-height: 2.2rem; padding: .45rem .55rem; border: 1px solid var(--line); border-radius: 0; background: var(--paper); color: var(--ink); font: 500 .86rem/1.35 var(--body); } +.task-create-form textarea { resize: vertical; } +.task-create-form select[multiple] { min-height: 7rem; } +.task-create-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .7rem; } +.task-create-routing { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.task-create-form button { width: fit-content; min-height: 2.25rem; padding: .45rem .72rem; border: 1px solid var(--signal); background: var(--signal); color: var(--deep); font: 700 .8rem/1.2 var(--body); cursor: pointer; } +.task-create-form button:hover:not(:disabled) { border-color: var(--route); background: var(--route); } +.task-create-form button:disabled { cursor: wait; opacity: .62; } +.task-create-form fieldset:disabled { opacity: .66; } +.task-create-status { margin: 0; color: var(--muted); font-size: .76rem; } +.task-create-status.is-error { color: var(--signal); } +.task-create-preview { display: grid; gap: .6rem; padding: .8rem; border: 1px solid var(--route); background: var(--paper); } +.task-create-preview[hidden] { display: none; } +.task-create-preview h4, .task-create-preview p { margin: 0; } +.task-create-preview dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .55rem; margin: 0; } +.task-create-preview dl > div { min-width: 0; } +.task-create-preview dt { color: var(--muted); font-size: .7rem; } +.task-create-preview dd { margin: .15rem 0 0; overflow-wrap: anywhere; } +.task-ticket ul { padding-left: 1.1rem; } +.fleet-columns, .evidence-columns, .delivery-columns { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); gap: 1.4rem; } +.work-surface { min-width: 0; padding: .85rem .9rem; border-top: 1px solid var(--line); background: rgba(26, 42, 52, .55); } +.work-surface .panel-heading { align-items: start; } +.flow-watch-note { max-width: 74ch; margin: -.1rem 0 1rem; color: var(--muted); font-size: .82rem; } +.flow-watch-grid { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); gap: 1.4rem; align-items: start; } +.flow-column { min-width: 0; padding: .8rem .9rem .5rem; border-top: 2px solid var(--route); background: rgba(11, 20, 27, .48); } +.approvals-column { border-top-color: var(--signal); } +.flow-column .panel-heading { align-items: baseline; } +.flow-column .panel-heading h3 { font: 650 1rem/1.2 var(--display); } +.flow-column .panel-heading span { color: var(--muted); font-size: .7rem; text-align: right; } +.manager-watch { padding: .65rem 0; border-top: 1px solid var(--line); } +.manager-watch-heading, .flow-report { display: flex; justify-content: space-between; align-items: baseline; gap: .8rem; } +.manager-watch-heading strong { font: 650 .88rem/1.2 var(--display); } +.manager-watch-heading small, .manager-cadence, .flow-report small { color: var(--muted); font-size: .7rem; overflow-wrap: anywhere; } +.manager-cadence { margin: .28rem 0 .25rem; } +.flow-report { padding: .34rem 0; border-top: 1px solid rgba(58, 80, 92, .65); } +.flow-report > span:first-child { min-width: 0; overflow-wrap: anywhere; font-size: .75rem; } +.flow-state { flex: 0 0 auto; color: var(--field); font-size: .68rem; } +.flow-state.is-waiting, .flow-state.is-stale, .flow-state.is-missing { color: var(--signal); } +.flow-state.is-idle, .flow-state.is-offline { color: var(--muted); } +.approval-item { margin-top: .55rem; padding: .62rem .72rem; border-left: 3px solid var(--route); background: var(--surface); } +.approval-item.wait-owner { border-left-color: #f3bd55; } +.approval-item.wait-manager { border-left-color: var(--field); } +.approval-item h4 { margin: 0; font: 650 .86rem/1.25 var(--display); overflow-wrap: anywhere; } +.approval-item p { margin: .25rem 0; color: var(--muted); font-size: .73rem; line-height: 1.4; overflow-wrap: anywhere; } +.approval-target { color: var(--ink); font-size: .68rem; } +.approval-item details { margin-top: .35rem; color: var(--muted); font-size: .7rem; } +.approval-item details p { white-space: pre-wrap; } +.approval-decision-form { display: grid; gap: .48rem; margin-top: .65rem; padding-top: .6rem; border-top: 1px solid var(--line); } +.approval-decision-form label { display: grid; gap: .25rem; color: var(--muted); font-size: .69rem; } +.approval-decision-form label span { display: inline; } +.approval-decision-form select, .approval-decision-form textarea { width: 100%; } +.approval-decision-actions { display: flex; align-items: center; gap: .65rem; flex-wrap: wrap; } +.approval-decision-actions p { margin: 0; } +.approval-decision-preview { padding: .62rem; border-left: 2px solid var(--signal); background: var(--paper); } +.approval-decision-preview h4 { margin-bottom: .3rem; } +.approval-decision-preview pre { max-height: 12rem; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; } +.approval-owner-boundary { padding: .4rem .55rem; border-left: 2px solid var(--route); background: var(--paper); } +.flow-watch-footnote { margin: .75rem 0 .15rem; color: var(--muted); font-size: .69rem; } +.agent-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 1.15rem; } +.agent-row { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; gap: .65rem; align-items: start; padding: .75rem .1rem; border-bottom: 1px solid var(--line); } +.agent-copy { min-width: 0; display: grid; gap: .2rem; } +.agent-copy strong { font-family: var(--display); font-size: .9rem; } +.agent-copy span, .agent-copy small, .agent-state small { color: var(--muted); font-size: .73rem; overflow-wrap: anywhere; } +.agent-state { display: grid; justify-items: end; gap: .2rem; } +.agent-state strong { font-size: .72rem; font-weight: 650; } +.presence { width: 8px; height: 8px; margin-top: .35rem; border-radius: 50%; background: #8297a2; } +.presence.working, .presence.online, .presence.starting { 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, .message-row small { display: block; color: var(--muted); margin-top: .2rem; } +.console-list { min-width: 0; } +.list-row { display: flex; justify-content: space-between; gap: .8rem; align-items: baseline; padding: .62rem 0; border-bottom: 1px solid var(--line); } +.list-row > div { min-width: 0; } +.list-row strong { font-size: .82rem; overflow-wrap: anywhere; } +.list-row small { display: block; margin-top: .18rem; color: var(--muted); font-size: .72rem; overflow-wrap: anywhere; } +.list-row > span { max-width: 45%; color: var(--route); font-size: .72rem; text-align: right; overflow-wrap: anywhere; } +.organization-note { max-width: 78ch; margin: -.2rem 0 1rem; color: var(--muted); font-size: .82rem; } +.organization-roles { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: .75rem; align-items: start; } +.organization-role { min-width: 0; padding: .85rem .9rem; border: 1px solid var(--line); border-top: 2px solid var(--route); background: var(--paper); scroll-margin-top: 1rem; } +.organization-role.kind-owner { border-top-color: var(--signal); } +.organization-role.kind-owner-facing, .organization-role.kind-program-direction, .organization-role.kind-fleet-operations { border-top-color: var(--ink); } +.organization-role.kind-domain-coordination { border-top-color: var(--route); } +.organization-role.kind-specialist { border-top-color: var(--field); } +.organization-role.kind-independent-review { border-top-color: #f3bd55; } +.organization-role-heading { display: flex; justify-content: space-between; align-items: start; gap: .65rem; padding-bottom: .55rem; border-bottom: 1px solid var(--line); } +.organization-role-heading h3 { font: 650 1rem/1.2 var(--display); } +.organization-role-heading p { margin: .18rem 0 0; color: var(--muted); font-size: .72rem; overflow-wrap: anywhere; } +.organization-role-heading code { font-size: .7rem; } +.organization-state { flex: 0 0 auto; max-width: 45%; color: var(--muted); font-size: .7rem; text-align: right; } +.organization-responsibilities { margin: .6rem 0; padding-left: 1.1rem; color: var(--muted); font-size: .76rem; line-height: 1.4; } +.organization-relations { display: grid; gap: .42rem; margin: .6rem 0 0; font-size: .72rem; } +.organization-relations > div { display: grid; grid-template-columns: 6.8rem minmax(0, 1fr); gap: .5rem; } +.organization-relations dt { color: var(--muted); } +.organization-relations dd { min-width: 0; margin: 0; overflow-wrap: anywhere; } +.organization-relations a { display: inline-block; margin-right: .45rem; color: var(--route); text-decoration-thickness: 1px; text-underline-offset: .15em; } +.organization-lifecycle { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--line); } +.lifecycle-heading { display: flex; justify-content: space-between; align-items: start; gap: 1rem; margin-bottom: .8rem; } +.lifecycle-heading h3 { font-size: 1.1rem; } +.lifecycle-heading p { max-width: 70ch; margin: .3rem 0 0; color: var(--muted); font-size: .76rem; } +.lifecycle-readonly { flex: 0 0 auto; color: var(--field); font-size: .7rem; } +.lifecycle-orders { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } +.lifecycle-orders > section { min-width: 0; } +.lifecycle-orders h4 { margin: 0; padding: .5rem 0; border-bottom: 1px solid var(--line); font: 650 .82rem/1.3 var(--display); } +.lifecycle-order { display: grid; gap: 0; max-height: 20rem; overflow: auto; margin: 0; padding: 0; list-style: none; scrollbar-color: var(--line) var(--deep); } +.lifecycle-order li { display: grid; grid-template-columns: 1.7rem minmax(0, 1fr); align-items: start; gap: .55rem; padding: .48rem .25rem; border-bottom: 1px solid var(--line); } +.lifecycle-order-index { display: grid; place-items: center; width: 1.45rem; height: 1.45rem; border: 1px solid var(--route); color: var(--route); font: 650 .66rem/1 var(--display); } +.lifecycle-order strong, .lifecycle-order small { display: block; overflow-wrap: anywhere; } +.lifecycle-order strong { font-size: .75rem; line-height: 1.3; } +.lifecycle-order small { margin-top: .12rem; color: var(--muted); font-size: .67rem; line-height: 1.35; } +.lifecycle-clear { margin: .7rem 0 0; color: var(--muted); font-size: .72rem; } +.lifecycle-blockers { display: grid; gap: .3rem; margin: .7rem 0 0; padding: .65rem .75rem .65rem 2rem; border-left: 2px solid var(--signal); background: var(--warning-surface); color: var(--warning-ink); font-size: .72rem; } +.communication-section { border-bottom: 1px solid var(--line); } +.conversation-stream { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1.5rem; } +.conversation-item { min-width: 0; padding: .8rem 0; border-bottom: 1px solid var(--line); } +.conversation-meta { display: flex; justify-content: space-between; gap: .7rem; font-size: .78rem; } +.conversation-meta span, .conversation-item small { color: var(--muted); font-size: .72rem; } +.conversation-item p { margin: .4rem 0; line-height: 1.45; overflow-wrap: anywhere; } +.artifact-list { min-height: 110px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; padding-top: .7rem; } +.review-gallery { grid-column: 1 / -1; } +.artifact-item { min-width: 0; overflow: hidden; border: 1px solid var(--line); background: var(--paper); } +.artifact-item img { display: block; width: 100%; max-height: 320px; aspect-ratio: 16 / 10; object-fit: contain; background: var(--deep); } +.artifact-unavailable { display: grid; place-items: center; min-height: 100px; padding: .75rem; background: var(--deep); color: var(--muted); font-size: .76rem; text-align: center; } +.artifact-caption { display: grid; gap: .45rem; padding: .85rem 1rem; border-bottom: 1px solid var(--line); } +.artifact-caption > strong { font: 650 1.15rem/1.2 var(--display); } +.artifact-caption > small { color: var(--muted); font-size: .72rem; overflow-wrap: anywhere; } +.artifact-provenance { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .4rem .8rem; margin: 0; } +.artifact-provenance > div { min-width: 0; } +.artifact-provenance dt { color: var(--muted); font-size: .65rem; } +.artifact-provenance dd { margin: 0; overflow-wrap: anywhere; font-size: .75rem; } +.artifact-feedback { display: grid; gap: .45rem; padding: .65rem 1rem .85rem; border-left: 3px solid var(--signal); } +.artifact-feedback h4 { display: flex; justify-content: space-between; margin: 0; font: 650 .9rem/1.2 var(--display); } +.artifact-feedback h4 span { color: var(--muted); font: 600 .75rem/1 var(--body); } +.artifact-feedback blockquote { margin: 0; padding: .45rem .6rem; background: var(--surface); } +.artifact-feedback blockquote p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: .82rem; } +.artifact-feedback blockquote footer, .no-feedback { margin: .2rem 0 0; color: var(--muted); font-size: .7rem; } +.artifact-feedback-form { display: grid; gap: .55rem; margin-top: .55rem; } +.artifact-feedback-form label { font-size: .76rem; font-weight: 650; } +.artifact-feedback-form textarea { width: 100%; min-height: 5.2rem; resize: vertical; padding: .65rem .75rem; border: 1px solid var(--line); border-radius: 0; background: var(--paper); color: var(--ink); font: 400 .86rem/1.45 var(--body); } +.artifact-feedback-form textarea:disabled { color: var(--ink); opacity: 1; background: var(--raised); } +.feedback-form-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .55rem .8rem; } +.feedback-form-actions button { border: 1px solid var(--route); padding: .48rem .72rem; background: var(--route); color: var(--deep); font: 650 .76rem/1.2 var(--body); cursor: pointer; } +.feedback-form-actions button:hover:not(:disabled) { background: var(--route); border-color: var(--route); } +.feedback-form-actions button:disabled { cursor: wait; opacity: .62; } +.feedback-form-actions .quiet-action { background: transparent; color: var(--ink); } +.feedback-form-actions .quiet-action:hover:not(:disabled) { background: var(--raised); border-color: var(--line); color: var(--ink); } +.feedback-form-actions > span { color: var(--muted); font-size: .7rem; } +.feedback-form-status, .feedback-action-status { margin: 0; color: var(--field); font-size: .76rem; } +.feedback-form-status.is-error { color: var(--signal); } +.feedback-action-status { margin: .8rem 0 0; padding: .55rem .7rem; border-left: 3px solid var(--field); background: var(--success-surface); } +.owner-request-composer { max-width: 68rem; margin: .8rem 0 1rem; padding: .7rem 0; border-block: 1px solid var(--line); } +.owner-request-composer > summary { width: fit-content; color: var(--route); font: 650 .9rem/1.3 var(--display); cursor: pointer; } +.owner-request-composer > p { max-width: 74ch; margin: .55rem 0; color: var(--muted); font-size: .75rem; } +.owner-request-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .65rem; } +.owner-request-composer form { display: grid; gap: .6rem; margin-top: .7rem; } +.owner-request-composer label { display: grid; gap: .2rem; color: var(--muted); font-size: .72rem; font-weight: 650; } +.owner-request-composer label span { font-weight: 400; } +.owner-request-composer select, .owner-request-composer textarea { width: 100%; min-height: 2.25rem; padding: .42rem .55rem; border: 1px solid var(--line); border-radius: 0; background: var(--deep); color: var(--ink); font: 500 .78rem/1.4 var(--body); } +.owner-request-composer textarea { min-height: 6rem; resize: vertical; } +.owner-request-composer select:disabled, .owner-request-composer textarea:disabled { opacity: 1; background: var(--raised); color: var(--muted); } +.owner-request-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .7rem; } +.owner-request-actions button { min-height: 2.25rem; padding: .42rem .68rem; border: 1px solid var(--route); background: var(--route); color: var(--deep); font: 650 .74rem/1.2 var(--body); cursor: pointer; } +.owner-request-actions button:hover:not(:disabled) { background: #9bc8ff; border-color: #9bc8ff; } +.owner-request-actions button.quiet-action { background: transparent; color: var(--ink); } +.owner-request-actions button:disabled { cursor: wait; opacity: .65; } +.owner-request-status { margin: 0; color: var(--muted); font-size: .7rem; } +.owner-request-status.is-error { color: var(--signal); } +.owner-request-preview { padding: .75rem; border-left: 2px solid var(--route); background: #172832; } +.owner-request-preview h3 { margin: 0; font: 650 .9rem/1.3 var(--display); } +.owner-request-preview > p { margin: .4rem 0; color: var(--muted); font-size: .74rem; } +.owner-request-preview blockquote { margin: .55rem 0; padding: .45rem .65rem; border-left: 2px solid var(--field); color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; font-size: .8rem; } +.owner-request-action-status:not([hidden]) { margin: .7rem 0; padding: .6rem .75rem; border-left: 3px solid var(--field); background: var(--success-surface); color: var(--ink); font-size: .78rem; } +.runtime-profile-tools { margin-top: 1rem; padding-top: .8rem; border-top: 1px solid var(--line); } +.runtime-profile-tools > summary { width: fit-content; color: var(--route); font: 650 .88rem/1.3 var(--display); cursor: pointer; } +.runtime-profile-tools > p { max-width: 66ch; margin: .55rem 0; color: var(--muted); font-size: .74rem; } +.runtime-profile-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .65rem; } +.runtime-profile-fields label { display: grid; gap: .2rem; color: var(--muted); font-size: .72rem; font-weight: 650; } +.runtime-profile-fields select, .runtime-profile-fields input { width: 100%; min-height: 2.25rem; padding: .42rem .55rem; border: 1px solid var(--line); border-radius: 0; background: var(--deep); color: var(--ink); font: 500 .78rem/1.4 var(--body); } +.runtime-profile-fields :disabled { opacity: .72; } +.runtime-profile-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .7rem; margin-top: .7rem; } +.runtime-profile-actions button { min-height: 2.25rem; padding: .42rem .68rem; border: 1px solid var(--route); background: var(--route); color: var(--deep); font: 650 .74rem/1.2 var(--body); cursor: pointer; } +.runtime-profile-actions button:hover:not(:disabled) { background: #9bc8ff; border-color: #9bc8ff; } +.runtime-profile-actions button.quiet-action { background: transparent; color: var(--ink); } +.runtime-profile-actions button:disabled { cursor: wait; opacity: .62; } +.runtime-profile-status { margin: 0; color: var(--muted); font-size: .7rem; } +.runtime-profile-status.is-error { color: var(--signal); } +.runtime-profile-preview { margin-top: .75rem; padding: .8rem; border-left: 3px solid var(--signal); background: var(--warning-surface); } +.runtime-profile-comparison { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: .65rem; } +.runtime-profile-comparison > div { display: grid; gap: .15rem; min-width: 0; } +.runtime-profile-comparison > div > span { color: var(--muted); font-size: .66rem; } +.runtime-profile-comparison strong { overflow-wrap: anywhere; font: 650 .92rem/1.25 var(--display); } +.profile-shift { color: var(--signal); font-size: .7rem; } +.runtime-profile-preview > p { margin: .7rem 0 0; color: var(--muted); font-size: .73rem; } +.runtime-profile-action-status:not([hidden]) { margin: .7rem 0; padding: .6rem .75rem; border-left: 3px solid var(--field); background: var(--success-surface); color: var(--ink); font-size: .78rem; } +.schedule-launcher-tools { margin-top: .8rem; padding-top: .7rem; border-top: 1px solid var(--line); } +.schedule-launcher-tools > summary { width: fit-content; color: var(--route); font: 650 .84rem/1.3 var(--display); cursor: pointer; } +.schedule-launcher-tools > p { max-width: 66ch; margin: .5rem 0; color: var(--muted); font-size: .72rem; } +.schedule-launcher-state { margin: .6rem 0; padding: .45rem .6rem; border-left: 2px solid var(--route); background: var(--deep); color: var(--muted); font-size: .72rem; overflow-wrap: anywhere; } +.schedule-launcher-tools > button, .schedule-launcher-actions button { min-height: 2.2rem; padding: .4rem .65rem; border: 1px solid var(--route); background: var(--route); color: var(--deep); font: 650 .73rem/1.2 var(--body); cursor: pointer; } +.schedule-launcher-tools button:hover:not(:disabled) { background: #9bc8ff; border-color: #9bc8ff; } +.schedule-launcher-tools button:disabled { cursor: not-allowed; opacity: .58; } +.schedule-launcher-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .65rem; } +.schedule-launcher-actions button.quiet-action { background: transparent; color: var(--ink); } +.schedule-launcher-status { min-height: 1.1em; } +.schedule-launcher-status.is-error { color: var(--signal); } +.schedule-launcher-preview { display: grid; gap: .45rem; margin-top: .7rem; padding: .75rem; border-left: 3px solid var(--signal); background: var(--warning-surface); } +.schedule-launcher-preview[hidden] { display: none; } +.schedule-launcher-preview h4, .schedule-launcher-preview p { margin: 0; } +.schedule-launcher-preview h4 { font: 650 .9rem/1.25 var(--display); } +.schedule-launcher-preview > p { color: var(--warning-ink); font-size: .73rem; } +.schedule-launcher-items { display: grid; gap: .35rem; margin: 0; padding: 0; list-style: none; } +.schedule-launcher-items li { display: grid; gap: .15rem; padding: .4rem .5rem; border-left: 1px solid var(--line); background: rgba(11, 20, 27, .55); } +.schedule-launcher-items strong { font-size: .75rem; } +.schedule-launcher-items small, .schedule-launcher-files { color: var(--muted); font-size: .67rem; overflow-wrap: anywhere; } +.schedule-launcher-action-status:not([hidden]) { margin: .65rem 0 0; padding: .55rem .65rem; border-left: 3px solid var(--field); background: var(--success-surface); font-size: .74rem; overflow-wrap: anywhere; } +.feedback-preview { display: grid; gap: .6rem; margin-top: .15rem; padding: .8rem; border: 1px solid var(--signal); background: var(--warning-surface); } +.feedback-preview-heading { display: flex; justify-content: space-between; flex-wrap: wrap; gap: .4rem; } +.feedback-preview-heading strong { font: 650 .94rem/1.2 var(--display); } +.feedback-preview-heading span { color: var(--muted); font-size: .68rem; } +.feedback-preview-target { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .5rem; margin: 0; } +.feedback-preview-target > div { min-width: 0; } +.feedback-preview-target dt { color: var(--muted); font-size: .65rem; } +.feedback-preview-target dd { margin: 0; overflow-wrap: anywhere; font-size: .76rem; } +.feedback-preview > blockquote { border-left: 2px solid var(--signal); } +.feedback-preview > p { margin: 0; color: var(--muted); font-size: .72rem; } +.gallery-note { max-width: 78ch; margin: .7rem 0 1rem; color: var(--muted); font-size: .8rem; } .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); } -.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); } +.organization-proposal { margin: .7rem 0; padding: .9rem 0 .9rem .9rem; border-left: 2px solid var(--line); border-bottom: 1px solid var(--line); } +.organization-proposal.is-pending { border-left-color: #f3bd55; } +.organization-proposal.is-activation-ready { border-left-color: #f3bd55; } +.organization-proposal-heading { display: flex; justify-content: space-between; align-items: start; gap: 1rem; } +.organization-proposal-heading > div > span { color: var(--muted); font-size: .7rem; } +.organization-proposal-heading h3 { margin: .16rem 0; font: 650 1rem/1.25 var(--display); overflow-wrap: anywhere; } +.organization-proposal-heading code { color: var(--muted); font-size: .66rem; } +.organization-proposal-heading > strong { flex: 0 0 auto; padding: .18rem .4rem; border: 1px solid var(--line); color: var(--muted); font-size: .68rem; font-weight: 600; } +.organization-proposal.is-pending .organization-proposal-heading > strong { border-color: #f3bd55; color: #f3bd55; } +.organization-proposal > p { margin: .55rem 0; max-width: 82ch; color: var(--muted); font-size: .8rem; } +.organization-proposal-details, .organization-review { margin-top: .55rem; font-size: .76rem; } +.pilot-review-history { margin-top: .75rem; font-size: .8rem; } +.pilot-review-history summary { cursor: pointer; color: var(--route); } +.pilot-review-record { padding: .8rem 0; border-bottom: 1px solid var(--line); } +.pilot-review-record h4 { margin: 0; font-size: .9rem; } +.pilot-review-record p { max-width: 78ch; } +.pilot-review-provenance, .pilot-review-record small { display: block; color: var(--muted); font-size: .72rem; overflow-wrap: anywhere; } +.pilot-comparison-scroll { max-width: 100%; overflow-x: auto; } +.pilot-comparison-table { width: 100%; border-collapse: collapse; text-align: left; font-size: .76rem; } +.pilot-comparison-table caption { padding: .5rem 0; text-align: left; color: var(--muted); } +.pilot-comparison-table th, .pilot-comparison-table td { padding: .55rem .6rem .55rem 0; border-bottom: 1px solid var(--line); vertical-align: top; overflow-wrap: anywhere; } +.organization-proposal-details > summary, .organization-review > summary { width: fit-content; color: var(--route); cursor: pointer; } +.organization-proposal pre, .organization-review pre { max-height: 21rem; overflow: auto; margin: .55rem 0; padding: .65rem; border-left: 2px solid var(--line); background: var(--deep); color: var(--muted); white-space: pre-wrap; overflow-wrap: anywhere; font: 400 .68rem/1.45 ui-monospace, "Cascadia Code", monospace; } +.organization-review { max-width: 58rem; padding-top: .6rem; border-top: 1px solid var(--line); } +.hierarchy-activation { max-width: 58rem; margin-top: .65rem; padding-top: .6rem; border-top: 1px solid #f3bd55; font-size: .76rem; } +.hierarchy-activation > summary { width: fit-content; color: #f3bd55; cursor: pointer; } +.hierarchy-activation-preview { border-color: #f3bd55; } +.organization-review-form { display: grid; gap: .5rem; margin-top: .6rem; } +.organization-review-form label { display: grid; gap: .2rem; color: var(--muted); font-size: .72rem; font-weight: 650; } +.organization-review-form label span { font-weight: 400; } +.organization-review-form select, .organization-review-form textarea { width: min(100%, 36rem); min-height: 2.25rem; padding: .42rem .55rem; border: 1px solid var(--line); border-radius: 0; background: var(--deep); color: var(--ink); font: 500 .78rem/1.35 var(--body); } +.organization-review-form textarea { min-height: 4.2rem; resize: vertical; } +.organization-review-form select:disabled, .organization-review-form textarea:disabled { opacity: 1; background: var(--raised); color: var(--muted); } +.organization-review-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .7rem; } +.organization-review-actions button { min-height: 2.25rem; padding: .42rem .65rem; border: 1px solid var(--route); background: var(--route); color: var(--deep); font: 650 .73rem/1.2 var(--body); cursor: pointer; } +.organization-review-actions button:hover:not(:disabled) { background: #9bc8ff; border-color: #9bc8ff; } +.organization-review-actions button.quiet-action { background: transparent; color: var(--ink); } +.organization-review-actions button.commit-activation { border-color: #f3bd55; background: #f3bd55; color: #19170f; } +.organization-review-actions button.commit-activation:hover:not(:disabled) { border-color: #ffd879; background: #ffd879; } +.organization-review-actions button:disabled { cursor: wait; opacity: .65; } +.organization-review-status { margin: 0; color: var(--field); font-size: .7rem; } +.organization-review-status.is-error { color: var(--signal); } +.organization-review-preview { margin-top: .25rem; padding: .75rem; border: 1px solid #f3bd55; background: var(--warning-surface); } +.organization-review-preview h4 { margin: 0; font: 650 .9rem/1.25 var(--display); } +.organization-review-preview > p { margin: .45rem 0; color: var(--warning-ink); font-size: .76rem; } +.organization-review-preview details > summary { cursor: pointer; color: var(--muted); font-size: .7rem; } +.organization-action-status:not([hidden]) { margin: .6rem 0; padding: .6rem .75rem; border-left: 3px solid var(--field); background: var(--success-surface); color: var(--ink); font-size: .78rem; } +.empty-state { max-width: 40ch; margin: 0; padding: .85rem .55rem; color: var(--muted); font-size: .78rem; line-height: 1.45; } +.context-detail p { max-width: 55ch; margin: .65rem 0; color: var(--muted); } +.context-detail small { color: var(--muted); font-size: .72rem; } +.more-details { padding-block: 1.3rem; border-bottom: 1px solid var(--line); } +.more-details > summary { width: fit-content; color: var(--route); font-family: var(--display); } +.operations-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; padding-top: 1rem; } +.console-footer { display: flex; justify-content: space-between; gap: 1rem; padding-top: 1rem; color: var(--muted); font-size: .72rem; } + +.console-demo-banner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .7rem clamp(1rem, 3vw, 2.5rem); border-bottom: 1px solid var(--line); background: #24313a; } +.console-demo-banner[hidden] { display: none; } +.console-demo-banner div { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; } +.console-demo-banner span { color: var(--muted); font-size: .8rem; } +.console-demo-banner button { flex-shrink: 0; padding: .4rem .8rem; border: 1px solid var(--line); border-radius: .4rem; background: transparent; color: var(--ink); cursor: pointer; } @media (max-width: 900px) { .hero { grid-template-columns: 1fr; min-height: auto; } @@ -147,9 +562,21 @@ footer div { display: flex; gap: 1.5rem; } .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); } - .console-grid { grid-template-columns: repeat(2, 1fr); } - .agents-panel { grid-column: span 2; } + .console-layout { grid-template-columns: 1fr; } + .console-rail { position: sticky; z-index: 5; top: 0; display: flex; overflow-x: auto; padding: .35rem .6rem; border-right: 0; border-bottom: 1px solid var(--line); background: var(--paper); } + .console-rail a { flex: 0 0 auto; padding: .5rem .65rem; border-left: 0; border-bottom: 3px solid transparent; } + .console-rail a:hover, .console-rail a:focus-visible { border-left-color: transparent; border-bottom-color: var(--route); } + .rail-note { display: none; } + .console-workspace { padding-inline: clamp(1rem, 3vw, 2rem); } + .pulse-zone { grid-template-columns: minmax(0, 1.15fr) minmax(240px, .85fr); } + .work-filter-row { grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); } + .initiative-rollup { grid-template-columns: 1fr; } + .initiative-rollup-columns { grid-template-columns: 1fr; } + .lifecycle-orders { grid-template-columns: 1fr; } + .initiative-row > summary { grid-template-columns: minmax(0, 1.2fr) auto minmax(90px, .8fr); } + .initiative-row .initiative-count { grid-column: 1 / -1; text-align: left; padding-left: 1.2rem; } + .backlog-board { grid-template-columns: repeat(5, minmax(190px, 1fr)); } + .operations-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 600px) { @@ -166,10 +593,34 @@ footer div { display: flex; gap: 1.5rem; } .ledger-row > [data-label="Capability"]::before { content: ""; } .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; } + .console-nav { min-height: 62px; gap: .7rem; } + .console-project { margin-right: 0; } + .console-project span { display: none; } + .console-tools > span { display: none; } + .console-rail { top: 0; } + .section-heading { flex-wrap: wrap; } + .section-heading > div, .section-heading > span { min-width: 0; overflow-wrap: anywhere; } + .console-workspace { padding-inline: .8rem; } + .pulse-zone, .fleet-columns, .evidence-columns, .delivery-columns, .flow-watch-grid { grid-template-columns: 1fr; } + .owner-request-fields, .runtime-profile-fields { grid-template-columns: 1fr; } + .schedule-launcher-actions { align-items: stretch; flex-direction: column; } + .lifecycle-heading { flex-direction: column; gap: .25rem; } + .task-create-grid, .task-create-preview dl { grid-template-columns: 1fr; } + .saved-view-actions { align-items: stretch; } + .saved-view-actions label { width: 100%; } + .initiative-row > summary { grid-template-columns: minmax(0, 1fr) auto; } + .initiative-row progress { grid-column: 1 / 2; margin-left: 1.1rem; width: calc(100% - 1.1rem); } + .initiative-row .initiative-count { grid-column: 1 / -1; padding-left: 1.1rem; } + .initiative-details li { grid-template-columns: minmax(0, 1fr) auto; } + .initiative-details li > span:last-child { grid-column: 1 / -1; } + .pulse-main { padding: 1.5rem 0 1rem; } + .attention-panel { padding: 1rem 0; border-left: 0; border-top: 1px solid var(--line); } + .pulse-readouts { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .pulse-readouts div:nth-child(3) { padding-left: 0; border-left: 0; } + .agent-list, .conversation-stream, .artifact-list, .operations-grid { grid-template-columns: 1fr; } + .feedback-preview-target { grid-template-columns: 1fr; } + .backlog-board { grid-template-columns: repeat(5, minmax(230px, 1fr)); } + .console-footer { flex-direction: column; } } @media (prefers-reduced-motion: reduce) { diff --git a/site/task-activity.js b/site/task-activity.js new file mode 100644 index 0000000..0902621 --- /dev/null +++ b/site/task-activity.js @@ -0,0 +1,28 @@ +(() => { + const text = (value) => String(value ?? '').slice(0, 1200).replace(/[&<>"']/g, + (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]); + function render(activity) { + if (!activity?.available) return '

    Commit activity unavailable. No inactivity conclusion can be drawn. Use task history and current session state to review work.

    '; + const tasks = (activity.tasks ?? []).filter((task) => ['stale', 'unknown'].includes(task.status)) + .sort((a, b) => Number(Boolean(b.ownerRequested)) - Number(Boolean(a.ownerRequested)) + || (b.ageDays ?? -1) - (a.ageDays ?? -1) || String(a.taskId).localeCompare(String(b.taskId))); + const stale = tasks.filter((task) => task.status === 'stale'); + const unknown = tasks.filter((task) => task.status === 'unknown'); + const group = (title, items) => `

    ${title} (${items.length})

    ${items.length ? `
      ${items.slice(0, 40).map((task) => + `
    • ${text(task.title)} ${text(task.taskId)} +

      ${task.ownerRequested ? 'Owner request. ' : ''}${task.status === 'stale' ? `${text(task.ageDays)} days without a named commit.` : 'Absence of recent progress is unknown.'} + ${task.expectedWaiting ? 'Blocked: waiting may be expected; review the dependency.' : ''}

      + ${task.lastActivity ? `

      Last named commit ${text(task.lastActivity.commit)} at ${text(task.lastActivity.committedAt)}.

      ` : ''} + ${(task.closureIntent ?? []).length ? '

      Closure mentioned in a commit; only verified landing can close the task.

      ' : ''}
    • `).join('')}
    ` : '

    None in this observation.

    '} + ${items.length > 40 ? `

    Showing 40 of ${items.length}; inspect CLI activity for the remainder.

    ` : ''}`; + return `

    Named-commit activity as of ${text(activity.generatedAt)}. Metadata edits do not reset inactivity. This is not a measure of uncommitted work.

    +

    ${activity.complete ? 'Managed history scan complete within configured bounds.' : 'History coverage incomplete: unknown work must not be classified as neglected.'} + Scanned ${text(activity.scannedCommits)} commits; review threshold ${text(activity.staleDays)} days.

    + ${(activity.unavailableBranches ?? []).length ? `

    Unavailable branches: ${activity.unavailableBranches.map(text).join(', ')}.

    ` : ''} + ${activity.truncated ? '

    Commit scan truncated.

    ' : ''} + ${(activity.futureCommits ?? []).length ? '

    Future-dated commits excluded from activity; review repository clocks.

    ' : ''} + ${group('Stale work — owner requests first', stale)}${group('Unknown activity — owner requests first', unknown)} +

    Review against the authoritative work board. Observation never changes task state or closes work.

    `; + } + globalThis.TorchTaskActivity = Object.freeze({ render }); +})(); diff --git a/site/torch-mark.svg b/site/torch-mark.svg new file mode 100644 index 0000000..b7d4d07 --- /dev/null +++ b/site/torch-mark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/site/work-progress.js b/site/work-progress.js new file mode 100644 index 0000000..c01bd0c --- /dev/null +++ b/site/work-progress.js @@ -0,0 +1,40 @@ +(function installTorchWorkProgress(root) { + function summarize(tasks, field) { + const groups = new Map(); + for (const task of tasks ?? []) { + const name = typeof task[field] === 'string' ? task[field].trim() : ''; + if (!name) continue; + const group = groups.get(name) ?? { name, tasks: [] }; + group.tasks.push({ id: task.id, title: task.title ?? task.id, state: task.state, owner: task.owner ?? null }); + groups.set(name, group); + } + return [...groups.values()].map((group) => { + const counts = { completed: 0, cancelled: 0, blocked: 0, working: 0, review: 0, queued: 0, other: 0 }; + for (const task of group.tasks) { + if (task.state === 'completed') counts.completed += 1; + else if (task.state === 'cancelled') counts.cancelled += 1; + else if (task.state === 'blocked') counts.blocked += 1; + else if (['assigned', 'in_progress'].includes(task.state)) counts.working += 1; + else if (['verification', 'ready_to_integrate'].includes(task.state)) counts.review += 1; + else if (['proposed', 'ready'].includes(task.state)) counts.queued += 1; + else counts.other += 1; + } + const active = group.tasks.length - counts.cancelled; + const outstanding = active - counts.completed; + const progress = active ? Math.round((counts.completed / active) * 100) : 0; + const status = outstanding === 0 + ? (counts.completed ? 'Complete' : 'Cancelled') + : counts.blocked ? 'At risk' + : counts.queued === outstanding ? 'Queued' + : counts.review > 0 && counts.working === 0 && counts.queued === 0 ? 'In review' + : 'In progress'; + return { ...group, counts, active, progress, status }; + }).sort((left, right) => left.name.localeCompare(right.name)); + } + + function summarizeInitiatives(tasks = []) { + return { features: summarize(tasks, 'feature'), milestones: summarize(tasks, 'milestone') }; + } + + root.TorchWorkProgress = Object.freeze({ summarizeInitiatives }); +}(globalThis)); diff --git a/site/work-views.js b/site/work-views.js new file mode 100644 index 0000000..56cc8c2 --- /dev/null +++ b/site/work-views.js @@ -0,0 +1,144 @@ +(function installTorchWorkViews(root) { + const SCHEMA = 'torch.dev/console-saved-views/v1alpha1'; + const MAX_VIEWS = 20; + const MAX_NAME = 64; + const MAX_QUERY = 120; + const FILTER_KEYS = ['query', 'owner', 'state', 'priority', 'domain', 'feature', 'milestone']; + + function emptyFilters() { + return { query: '', owner: '', state: '', priority: '', domain: '', feature: '', milestone: '' }; + } + + function normalizeFilters(filters = {}) { + const normalized = emptyFilters(); + for (const key of FILTER_KEYS) { + if (typeof filters[key] !== 'string') continue; + const limit = key === 'query' ? MAX_QUERY : 100; + normalized[key] = filters[key].trim().slice(0, limit); + } + return normalized; + } + + function hasFilters(filters = {}) { + const normalized = normalizeFilters(filters); + return FILTER_KEYS.some((key) => normalized[key] !== ''); + } + + function filterTasks(tasks = [], filters = {}) { + const selected = normalizeFilters(filters); + const query = selected.query.toLocaleLowerCase(); + return tasks.filter((task) => { + if (['completed', 'cancelled'].includes(task.state)) return false; + if (selected.owner && (task.owner ?? 'Unassigned') !== selected.owner) return false; + if (selected.state && task.state !== selected.state) return false; + if (selected.priority && (task.priority ?? 'normal') !== selected.priority) return false; + if (selected.domain && !(task.affectedDomains ?? []).includes(selected.domain)) return false; + if (selected.feature && task.feature !== selected.feature) return false; + if (selected.milestone && task.milestone !== selected.milestone) return false; + if (!query) return true; + const haystack = [ + task.id, task.title, task.description, task.owner, task.state, task.priority, + task.feature, task.milestone, + ...(task.affectedDomains ?? []), ...(task.dependencies ?? []), + ].filter(Boolean).join(' ').toLocaleLowerCase(); + return haystack.includes(query); + }); + } + + function storageKey(projectIdentity) { + const identity = String(projectIdentity ?? '').trim(); + if (!identity) throw new Error('Saved views require a project identity.'); + return `torch.console.saved-views:${encodeURIComponent(identity)}`; + } + + function normalizeRecord(record) { + if (!record || typeof record.id !== 'string' || !record.id || typeof record.name !== 'string') return null; + const name = record.name.trim().slice(0, MAX_NAME); + if (!name) return null; + return { + id: record.id.slice(0, 96), name, + filters: normalizeFilters(record.filters), + updatedAt: typeof record.updatedAt === 'string' ? record.updatedAt : null, + }; + } + + function validRecord(record) { + if (!record || typeof record.id !== 'string' || !record.id || record.id.length > 96 + || typeof record.name !== 'string' || !record.name.trim() || record.name.trim().length > MAX_NAME + || !record.filters || typeof record.filters !== 'object' || Array.isArray(record.filters)) return false; + if (record.updatedAt !== undefined && record.updatedAt !== null && typeof record.updatedAt !== 'string') return false; + return FILTER_KEYS.every((key) => record.filters[key] === undefined + || (typeof record.filters[key] === 'string' && record.filters[key].length <= (key === 'query' ? MAX_QUERY : 100))); + } + + function readSavedViews(storage, key) { + try { + const raw = storage.getItem(key); + if (raw === null) return { views: [], warning: null }; + const parsed = JSON.parse(raw); + if (parsed?.schema !== SCHEMA || !Array.isArray(parsed.views) || parsed.views.length > MAX_VIEWS) { + return { views: [], warning: 'Saved views have an unsupported format; start a new set for this project.' }; + } + const ids = new Set(); + const views = []; + for (const record of parsed.views) { + if (!validRecord(record) || ids.has(record.id)) { + return { views: [], warning: 'Saved views contain invalid data; the unfiltered backlog is shown.' }; + } + ids.add(record.id); + views.push(normalizeRecord(record)); + } + return { views, warning: null }; + } catch { + return { views: [], warning: 'Saved views are unavailable in this browser; the backlog remains unchanged.' }; + } + } + + function persist(storage, key, views) { + try { + storage.setItem(key, JSON.stringify({ schema: SCHEMA, views: views.slice(0, MAX_VIEWS) })); + return { ok: true }; + } catch { + return { ok: false, message: 'This browser could not save the view. Your project backlog was not changed.' }; + } + } + + function makeId() { + return root.crypto?.randomUUID?.() + ?? `view-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; + } + + function saveView({ storage, key, views = [], id = null, name, filters, now = () => new Date() } = {}) { + const cleanName = typeof name === 'string' ? name.trim().slice(0, MAX_NAME) : ''; + if (!cleanName) return { ok: false, code: 'VIEW_NAME_REQUIRED', message: 'Enter a name for this view.' }; + const duplicate = views.find((view) => view.id !== id && view.name.toLocaleLowerCase() === cleanName.toLocaleLowerCase()); + if (duplicate) return { ok: false, code: 'VIEW_NAME_EXISTS', message: 'A saved view already uses that name.' }; + if (!id && views.length >= MAX_VIEWS) { + return { ok: false, code: 'VIEW_LIMIT_REACHED', message: `You can save up to ${MAX_VIEWS} views per project.` }; + } + if (id && !views.some((view) => view.id === id)) { + return { ok: false, code: 'VIEW_NOT_FOUND', message: 'That saved view is no longer available.' }; + } + const view = { + id: id ?? makeId(), name: cleanName, + filters: normalizeFilters(filters), updatedAt: now().toISOString(), + }; + const next = id ? views.map((existing) => existing.id === id ? view : existing) : [...views, view]; + const written = persist(storage, key, next); + return written.ok ? { ok: true, view, views: next } : { ...written, code: 'VIEW_STORAGE_UNAVAILABLE' }; + } + + function deleteView({ storage, key, views = [], id } = {}) { + if (!views.some((view) => view.id === id)) { + return { ok: false, code: 'VIEW_NOT_FOUND', message: 'That saved view is no longer available.' }; + } + const next = views.filter((view) => view.id !== id); + const written = persist(storage, key, next); + return written.ok ? { ok: true, views: next } : { ...written, code: 'VIEW_STORAGE_UNAVAILABLE' }; + } + + root.TorchWorkViews = Object.freeze({ + schema: SCHEMA, maxViews: MAX_VIEWS, emptyFilters, normalizeFilters, + hasFilters, filterTasks, storageKey, readSavedViews, saveView, deleteView, + }); +}(globalThis)); diff --git a/src/adapters/claude.mjs b/src/adapters/claude.mjs index e0d1121..1552325 100644 --- a/src/adapters/claude.mjs +++ b/src/adapters/claude.mjs @@ -24,6 +24,31 @@ function text(value, name) { return value.trim(); } +const CLAUDE_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']); +const CLAUDE_PERMISSION_MODES = new Set(['default', 'acceptEdits', 'auto', 'manual', 'dontAsk', 'plan']); + +function effortArgs(reasoning) { + if (reasoning === undefined || reasoning === null || reasoning === '') return []; + const effort = text(reasoning, 'reasoning'); + if (!CLAUDE_EFFORTS.has(effort)) { + throw new TorchError(`Unsupported Claude effort level: ${effort}`, { + code: 'RUNTIME_PROFILE_INVALID', details: { adapter: 'claude', field: 'reasoning', value: effort }, + }); + } + return ['--effort', effort]; +} + +function launchPolicyArgs(policy = {}) { + const entries = Object.entries(policy ?? {}); + if (entries.some(([field, value]) => field !== 'permissionMode' + || typeof value !== 'string' || !CLAUDE_PERMISSION_MODES.has(value))) { + throw new TorchError('Claude launch policy is invalid or unsupported', { + code: 'RUNTIME_PROFILE_INVALID', details: { adapter: 'claude', launchPolicy: policy }, + }); + } + return policy.permissionMode === undefined ? [] : ['--permission-mode', policy.permissionMode]; +} + function commandRecord(command, args, cwd) { return { command, args, cwd, mutatesRuntime: true }; } @@ -66,9 +91,16 @@ export class ClaudeRuntimeAdapter { this.executable = executable; this.nodeExecutable = nodeExecutable; this.idFactory = idFactory; + this.configuration = Object.freeze({ model: 'sonnet', background: true }); this.capabilities = Object.freeze({ detect: true, configure: 'plan-only', + modelSelection: true, + reasoningSelection: true, + perInvocationCostCeiling: Object.freeze({ createSession: Object.freeze([]), resumeSession: Object.freeze([]) }), + launchPolicy: Object.freeze({ fields: Object.freeze({ + permissionMode: Object.freeze([...CLAUDE_PERMISSION_MODES]), + }) }), createSession: true, resumeSession: true, sendOrSteer: 'durable-fallback', @@ -105,12 +137,14 @@ export class ClaudeRuntimeAdapter { }; } - createSession({ areaId, title, worktree, promptFile, firstMessage, model = 'opus', background = true, mcp } = {}) { + createSession({ areaId, title, worktree, promptFile, firstMessage, model = 'sonnet', reasoning, launchPolicy, background = true, mcp } = {}) { const runtimeSessionId = background ? null : this.idFactory(); const args = []; if (background) args.push('--bg'); args.push( '--model', text(model, 'model'), + ...launchPolicyArgs(launchPolicy), + ...effortArgs(reasoning), ...(!background ? ['--session-id', runtimeSessionId] : []), '-n', `TORCH · ${text(title ?? areaId, 'title')}`, '--append-system-prompt-file', text(promptFile, 'promptFile'), @@ -124,10 +158,14 @@ export class ClaudeRuntimeAdapter { }; } - resumeSession({ areaId, runtimeSessionId, worktree, model = 'opus', message, background = true, mcp } = {}) { + resumeSession({ areaId, runtimeSessionId, worktree, promptFile, model = 'sonnet', reasoning, launchPolicy, message, background = true, mcp } = {}) { const args = []; if (background) args.push('--bg'); - args.push('--model', text(model, 'model'), ...mcpArgs(mcp), '--resume', text(runtimeSessionId, 'runtimeSessionId')); + args.push( + '--model', text(model, 'model'), ...launchPolicyArgs(launchPolicy), ...effortArgs(reasoning), + '--append-system-prompt-file', text(promptFile, 'promptFile'), ...mcpArgs(mcp), + '--resume', text(runtimeSessionId, 'runtimeSessionId'), + ); if (message) args.push(text(message, 'message')); return { adapter: this.name, areaId: text(areaId, 'areaId'), runtimeSessionId, diff --git a/src/adapters/codex.mjs b/src/adapters/codex.mjs index afdc9bf..2652312 100644 --- a/src/adapters/codex.mjs +++ b/src/adapters/codex.mjs @@ -1,7 +1,7 @@ import { accessSync, constants, existsSync } from 'node:fs'; import { delimiter, join } from 'node:path'; import { TorchError } from '../kernel/errors.mjs'; -import { unsupportedCapability, validateRuntimeAdapter } from './runtime.mjs'; +import { profileCapabilityIssues, unsupportedCapability, validateRuntimeAdapter } from './runtime.mjs'; function findExecutable(name, env = process.env) { for (const directory of (env.PATH ?? '').split(delimiter).filter(Boolean)) { @@ -47,6 +47,28 @@ function mcpName(areaId) { return `torch-${areaId.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-|-$/g, '')}`; } +function launchPolicyArgs(policy = {}) { + const issues = profileCapabilityIssues({ name: 'codex', capabilities: { + launchPolicy: { + fields: { + sandbox: ['read-only', 'workspace-write', 'danger-full-access'], + approval: ['approve-for-me', 'on-request', 'never'], + }, + conflicts: [{ field: 'approval', value: 'approve-for-me', with: 'sandbox' }], + }, + } }, { launchPolicy: policy }); + if (issues.length) { + throw new TorchError('Codex launch policy is invalid or unsupported', { + code: 'RUNTIME_PROFILE_INVALID', details: { adapter: 'codex', issues }, + }); + } + const args = []; + if (policy.sandbox !== undefined) args.push('--sandbox', policy.sandbox); + if (policy.approval === 'approve-for-me') args.push('--approve-for-me'); + else if (policy.approval !== undefined) args.push('--ask-for-approval', policy.approval); + return args; +} + function parseJsonLines(output) { const records = []; for (const line of String(output ?? '').split('\n').map((value) => value.trim()).filter(Boolean)) { @@ -67,9 +89,25 @@ export class CodexRuntimeAdapter { this.runner = runner; this.executable = executable; this.nodeExecutable = nodeExecutable; + this.configuration = Object.freeze({ + model: 'gpt-6-luna', reasoning: 'high', + launchPolicy: Object.freeze({ approval: 'approve-for-me' }), + }); this.capabilities = Object.freeze({ detect: true, configure: 'plan-only', + modelSelection: true, + reasoningSelection: true, + perInvocationCostCeiling: Object.freeze({ createSession: Object.freeze([]), resumeSession: Object.freeze([]) }), + launchPolicy: Object.freeze({ + fields: Object.freeze({ + sandbox: Object.freeze(['read-only', 'workspace-write', 'danger-full-access']), + approval: Object.freeze(['approve-for-me', 'on-request', 'never']), + }), + conflicts: Object.freeze([ + Object.freeze({ field: 'approval', value: 'approve-for-me', with: 'sandbox' }), + ]), + }), createSession: true, resumeSession: true, sendOrSteer: 'native-with-durable-fallback', @@ -109,14 +147,15 @@ export class CodexRuntimeAdapter { }; } - createSession({ areaId, worktree, promptFile, firstMessage, model, mcp } = {}) { + createSession({ areaId, worktree, promptFile, firstMessage, model, reasoning, launchPolicy, mcp } = {}) { const id = text(areaId, 'areaId'); const cwd = text(worktree, 'worktree'); const args = [ - '--cd', cwd, '--approve-for-me', + '--cd', cwd, ...launchPolicyArgs(launchPolicy ?? this.configuration.launchPolicy), ...mcpArgs(mcp), + ...((reasoning ?? this.configuration.reasoning) ? ['-c', `model_reasoning_effort=${JSON.stringify(text(reasoning ?? this.configuration.reasoning, 'reasoning'))}`] : []), 'exec', '--json', - ...optionalModelArgs(model), + ...optionalModelArgs(model ?? this.configuration.model), `${text(firstMessage, 'firstMessage')}\n\nRead and follow the TORCH domain prompt at ${text(promptFile, 'promptFile')}.`, ]; return { @@ -129,15 +168,19 @@ export class CodexRuntimeAdapter { }; } - resumeSession({ areaId, runtimeSessionId, worktree, message, model, mcp } = {}) { + resumeSession({ areaId, runtimeSessionId, worktree, message, instructionText, instructionDigest, model, reasoning, launchPolicy, mcp } = {}) { const id = text(areaId, 'areaId'); const sessionId = text(runtimeSessionId, 'runtimeSessionId'); const cwd = text(worktree, 'worktree'); + const currentInstructions = instructionText + ? `\n\nAUTHORITATIVE CURRENT TORCH INSTRUCTIONS (sha256 ${text(instructionDigest, 'instructionDigest')}; replace stale briefing or conversation memory):\n${text(instructionText, 'instructionText')}` + : ''; const args = [ - '--cd', cwd, '--approve-for-me', + '--cd', cwd, ...launchPolicyArgs(launchPolicy ?? this.configuration.launchPolicy), ...mcpArgs(mcp), + ...((reasoning ?? this.configuration.reasoning) ? ['-c', `model_reasoning_effort=${JSON.stringify(text(reasoning ?? this.configuration.reasoning, 'reasoning'))}`] : []), 'exec', 'resume', '--json', - ...optionalModelArgs(model), sessionId, text(message, 'message'), + ...optionalModelArgs(model ?? this.configuration.model), sessionId, `${text(message, 'message')}${currentInstructions}`, ]; return { adapter: this.name, diff --git a/src/adapters/delivery.mjs b/src/adapters/delivery.mjs index f6c8e3a..f8afbfc 100644 --- a/src/adapters/delivery.mjs +++ b/src/adapters/delivery.mjs @@ -14,6 +14,10 @@ export function validateDeliveryAdapter(adapter) { code: 'INVALID_DELIVERY_ADAPTER', details: { operation }, }); } + const safety = adapter.retrySafety?.[operation]; + if (safety !== undefined && !['never', 'idempotent', 'read-only'].includes(safety)) { + throw new TorchError('Invalid delivery retry safety declaration', { code: 'INVALID_DELIVERY_ADAPTER', details: { operation } }); + } } return adapter; } diff --git a/src/adapters/pi.mjs b/src/adapters/pi.mjs new file mode 100644 index 0000000..8f01ad9 --- /dev/null +++ b/src/adapters/pi.mjs @@ -0,0 +1,212 @@ +import { accessSync, constants, existsSync } from 'node:fs'; +import { delimiter, join, resolve } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { fileURLToPath } from 'node:url'; +import { TorchError } from '../kernel/errors.mjs'; +import { unsupportedCapability, validateRuntimeAdapter } from './runtime.mjs'; + +const PI_THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh']); +const PI_EXTENSION = fileURLToPath(new URL('./pi/extension.mjs', import.meta.url)); + +function text(value, name) { + if (typeof value !== 'string' || !value.trim()) { + throw new TorchError(`${name} must be a non-empty string`, { + code: 'INVALID_RUNTIME_INPUT', details: { field: name }, + }); + } + return value.trim(); +} + +function findExecutable(name, env = process.env) { + if (name.includes('/') && existsSync(name)) { + try { accessSync(name, constants.X_OK); return name; } catch { return null; } + } + for (const directory of (env.PATH ?? '').split(delimiter).filter(Boolean)) { + const candidate = join(directory, name); + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { /* keep searching */ } + } + return null; +} + +function thinkingArgs(reasoning) { + if (reasoning === undefined || reasoning === null || reasoning === '') return []; + const value = text(reasoning, 'reasoning'); + if (!PI_THINKING_LEVELS.has(value)) { + throw new TorchError(`Unsupported Pi thinking level: ${value}`, { + code: 'RUNTIME_PROFILE_INVALID', details: { adapter: 'pi', field: 'reasoning', value }, + }); + } + return ['--thinking', value]; +} + +function bridgeArgs(integration) { + if (integration?.type !== 'pi-mcp-extension') { + throw new TorchError('Pi requires the TORCH identity-bound MCP extension integration', { + code: 'RUNTIME_CONFIGURATION_INVALID', details: { adapter: 'pi' }, + }); + } + return [ + '--no-extensions', '--no-context-files', '--no-approve', + '--extension', text(integration.extensionPath, 'extensionPath'), + '--torch-root', text(integration.repositoryRoot, 'repositoryRoot'), + '--torch-area', text(integration.areaId, 'areaId'), + '--torch-mcp-entry', text(integration.mcpEntry, 'mcpEntry'), + '--torch-node', text(integration.nodeExecutable, 'nodeExecutable'), + '--session-dir', text(integration.sessionDir, 'sessionDir'), + ]; +} + +function launchPolicyArgs(policy = {}) { + if (Object.keys(policy ?? {}).length) { + throw new TorchError('Pi launch policy overrides are unsupported; safe approval and extension flags are fixed by TORCH', { + code: 'RUNTIME_PROFILE_INVALID', details: { adapter: 'pi', launchPolicy: policy }, + }); + } + return []; +} + +function commandRecord(command, args, cwd) { + return { command, args, cwd, mutatesRuntime: true }; +} + +export class PiRuntimeAdapter { + constructor({ + env = process.env, + executable = 'pi', + nodeExecutable = process.execPath, + extensionPath = PI_EXTENSION, + idFactory = randomUUID, + } = {}) { + this.name = 'pi'; + this.env = env; + this.executable = executable; + this.nodeExecutable = nodeExecutable; + this.extensionPath = resolve(extensionPath); + this.idFactory = idFactory; + this.configuration = Object.freeze({ model: null, reasoning: null, mode: 'json' }); + this.capabilities = Object.freeze({ + detect: true, + configure: 'plan-only', + modelSelection: true, + modelRequired: true, + reasoningSelection: true, + perInvocationCostCeiling: Object.freeze({ createSession: Object.freeze([]), resumeSession: Object.freeze([]) }), + launchPolicy: Object.freeze({ fields: Object.freeze({}) }), + createSession: true, + resumeSession: true, + sendOrSteer: 'durable-fallback', + getStatus: false, + listSessions: false, + stopSession: false, + captureRuntimeId: true, + installHooks: false, + removeHooks: false, + liveSteering: false, + durableMessaging: true, + processModel: 'resumable-turn', + }); + validateRuntimeAdapter(this); + } + + detect() { + const path = findExecutable(this.executable, this.env); + return { available: Boolean(path), executable: path, adapter: this.name }; + } + + configure({ repositoryRoot, areaId, mcpEntry, stateRoot } = {}) { + const root = resolve(text(repositoryRoot, 'repositoryRoot')); + const id = text(areaId, 'areaId'); + const entry = resolve(text(mcpEntry, 'mcpEntry')); + const state = resolve(text(stateRoot, 'stateRoot')); + return { + adapter: this.name, + mutationPerformed: false, + integration: { + type: 'pi-mcp-extension', + extensionPath: this.extensionPath, + repositoryRoot: root, + areaId: id, + mcpEntry: entry, + nodeExecutable: this.nodeExecutable, + sessionDir: join(state, 'sessions', 'pi', id), + }, + note: 'The bundled TORCH Pi extension exposes the same identity-bound MCP tools; its MCP process runs only for a tool call.', + }; + } + + createSession({ areaId, title, worktree, promptFile, firstMessage, model, reasoning, launchPolicy, integration } = {}) { + const id = text(areaId, 'areaId'); + const cwd = text(worktree, 'worktree'); + const sessionId = text(this.idFactory(), 'runtimeSessionId'); + const args = [ + '--mode', 'json', '--print', + ...launchPolicyArgs(launchPolicy), + ...bridgeArgs(integration), + '--session-id', sessionId, + '--model', text(model, 'model'), + ...thinkingArgs(reasoning), + '-n', `TORCH · ${text(title ?? areaId, 'title')}`, + '--append-system-prompt', text(promptFile, 'promptFile'), + text(firstMessage, 'firstMessage'), + ]; + return { + adapter: this.name, areaId: id, runtimeSessionId: sessionId, + requiresRuntimeIdCapture: false, completionState: 'idle', + launch: commandRecord(this.executable, args, cwd), + }; + } + + resumeSession({ areaId, runtimeSessionId, worktree, message, promptFile, model, reasoning, launchPolicy, integration } = {}) { + const id = text(areaId, 'areaId'); + const sessionId = text(runtimeSessionId, 'runtimeSessionId'); + const args = [ + '--mode', 'json', '--print', + ...launchPolicyArgs(launchPolicy), + ...bridgeArgs(integration), + '--append-system-prompt', text(promptFile, 'promptFile'), + '--session-id', sessionId, + '--model', text(model, 'model'), + ...thinkingArgs(reasoning), + ...(message ? [text(message, 'message')] : []), + ]; + return { + adapter: this.name, areaId: id, runtimeSessionId: sessionId, + requiresRuntimeIdCapture: false, completionState: 'idle', + launch: commandRecord(this.executable, args, text(worktree, 'worktree')), + }; + } + + sendOrSteer({ controlPlane, sender = 'session-manager', recipient, body, references } = {}) { + if (!controlPlane) { + throw new TorchError('Pi durable messaging requires the TORCH control plane', { + code: 'CONTROL_PLANE_REQUIRED', + }); + } + const message = controlPlane.sendMessage({ sender, recipient, body, references }); + return { delivery: 'durable', liveSteeringAttempted: false, message }; + } + + captureRuntimeId({ areaId, runtimeSessionId } = {}) { + return { + areaId: text(areaId, 'areaId'), + runtimeSessionId: text(runtimeSessionId, 'runtimeSessionId'), + }; + } + + getStatus() { return unsupportedCapability(this.name, 'getStatus'); } + + listSessions() { return unsupportedCapability(this.name, 'listSessions'); } + + stopSession() { return unsupportedCapability(this.name, 'stopSession'); } + + installHooks() { return unsupportedCapability(this.name, 'installHooks'); } + + removeHooks() { return unsupportedCapability(this.name, 'removeHooks'); } +} + +export function createPiAdapter(options) { + return new PiRuntimeAdapter(options); +} diff --git a/src/adapters/pi/extension.mjs b/src/adapters/pi/extension.mjs new file mode 100644 index 0000000..a24a867 --- /dev/null +++ b/src/adapters/pi/extension.mjs @@ -0,0 +1,182 @@ +import { spawn } from 'node:child_process'; +import { resolve } from 'node:path'; +import { describeTorchTools } from '../../mcp/tools.mjs'; + +const PROTOCOL_VERSION = '2025-06-18'; +const REQUEST_TIMEOUT_MS = 120_000; + +function requiredFlag(pi, name) { + const value = pi.getFlag(name); + if (typeof value !== 'string' || !value.trim()) { + throw new Error(`TORCH Pi extension requires --${name}`); + } + return value.trim(); +} + +function safeArguments(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; + return value; +} + +export async function callTorchMcp({ + nodeExecutable, mcpEntry, repositoryRoot, areaId, name, input, + spawnProcess = spawn, signal, +} = {}) { + const executable = requiredString(nodeExecutable, 'nodeExecutable'); + const entry = requiredString(mcpEntry, 'mcpEntry'); + const root = requiredString(repositoryRoot, 'repositoryRoot'); + const identity = requiredString(areaId, 'areaId'); + const toolName = requiredString(name, 'name'); + const child = spawnProcess(executable, [entry, '--root', root, '--area', identity], { + cwd: root, stdio: ['pipe', 'pipe', 'pipe'], + }); + const pending = new Map(); + let nextId = 0; + let buffer = ''; + let stderr = ''; + let terminated = false; + + const failPending = (error) => { + for (const request of pending.values()) { + clearTimeout(request.timer); + request.reject(error); + } + pending.clear(); + }; + + child.stdout.on('data', (chunk) => { + buffer += chunk.toString(); + const lines = buffer.split('\n'); + buffer = lines.pop() ?? ''; + for (const line of lines.map((entryLine) => entryLine.trim()).filter(Boolean)) { + let message; + try { message = JSON.parse(line); } catch { + failPending(new Error('TORCH MCP bridge returned invalid JSON.')); + continue; + } + if (message.id === undefined) continue; + const request = pending.get(message.id); + if (!request) continue; + pending.delete(message.id); + clearTimeout(request.timer); + if (message.error) request.reject(new Error(message.error.message ?? 'TORCH MCP request failed.')); + else request.resolve(message.result); + } + }); + child.stderr.on('data', (chunk) => { + stderr = `${stderr}${chunk.toString()}`.slice(-8192); + }); + child.on('error', (error) => failPending(error)); + child.on('exit', (code, childSignal) => { + if (!terminated) { + failPending(new Error(`TORCH MCP bridge exited (${code ?? childSignal ?? 'unknown'}). ${stderr}`.trim())); + } + }); + + const request = (method, params, timeoutMs = REQUEST_TIMEOUT_MS) => { + if (signal?.aborted) return Promise.reject(new Error('TORCH MCP tool call was cancelled.')); + const id = ++nextId; + return new Promise((resolveResponse, rejectResponse) => { + const timer = setTimeout(() => { + pending.delete(id); + rejectResponse(new Error(`TORCH MCP ${method} timed out after ${timeoutMs} ms. ${stderr}`.trim())); + }, timeoutMs); + pending.set(id, { resolve: resolveResponse, reject: rejectResponse, timer }); + child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`, (error) => { + if (!error) return; + clearTimeout(timer); + pending.delete(id); + rejectResponse(error); + }); + }); + }; + + const abort = () => { + terminated = true; + child.kill('SIGTERM'); + failPending(new Error('TORCH MCP tool call was cancelled.')); + }; + signal?.addEventListener('abort', abort, { once: true }); + try { + await request('initialize', { + protocolVersion: PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'torch-pi-extension', version: '0.1.0-alpha.0' }, + }, 15_000); + child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n`); + const result = await request('tools/call', { + name: toolName, arguments: safeArguments(input), + }); + if (result?.isError) { + throw new Error(result.content?.map((part) => part.text ?? '').filter(Boolean).join('\n') || 'TORCH tool returned an error.'); + } + return result; + } finally { + signal?.removeEventListener('abort', abort); + terminated = true; + child.stdin.end(); + if (child.exitCode === null && child.signalCode === null) { + await new Promise((resolveExit) => { + const timer = setTimeout(() => { + child.kill('SIGTERM'); + resolveExit(); + }, 1000); + timer.unref?.(); + child.once('exit', () => { + clearTimeout(timer); + resolveExit(); + }); + }); + } + } +} + +function requiredString(value, name) { + if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-empty string.`); + return value.trim(); +} + +export function createTorchPiExtension(pi, { + Type, + spawnProcess = spawn, +} = {}) { + for (const [name, description] of [ + ['torch-root', 'Absolute TORCH project repository root.'], + ['torch-area', 'Bound TORCH Fleet identity for this Pi session.'], + ['torch-mcp-entry', 'Absolute path to TORCH MCP server entry point.'], + ['torch-node', 'Node.js executable used to launch the TORCH MCP server.'], + ]) { + pi.registerFlag(name, { type: 'string', description }); + } + const registered = []; + for (const tool of describeTorchTools()) { + pi.registerTool({ + name: tool.name, + label: `TORCH ${tool.name}`, + description: tool.description, + parameters: Type.Unsafe(tool.inputSchema), + async execute(_toolCallId, input, signal) { + const result = await callTorchMcp({ + nodeExecutable: requiredFlag(pi, 'torch-node'), + mcpEntry: requiredFlag(pi, 'torch-mcp-entry'), + repositoryRoot: resolve(requiredFlag(pi, 'torch-root')), + areaId: requiredFlag(pi, 'torch-area'), + name: tool.name, input, spawnProcess, signal, + }); + const text = result?.content?.map((part) => part.text ?? '').filter(Boolean).join('\n') + || JSON.stringify(result?.structuredContent ?? result ?? {}); + return { + content: [{ type: 'text', text }], + details: result?.structuredContent ?? result ?? null, + }; + }, + }); + registered.push(tool.name); + } + return { registered }; +} + +export default async function torchPiExtension(pi) { + const { Type } = await import('typebox'); + createTorchPiExtension(pi, { Type }); +} diff --git a/src/adapters/plugins.mjs b/src/adapters/plugins.mjs new file mode 100644 index 0000000..73746ea --- /dev/null +++ b/src/adapters/plugins.mjs @@ -0,0 +1,229 @@ +import { createRequire } from 'node:module'; +import { randomUUID } from 'node:crypto'; +import { + chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync, +} from 'node:fs'; +import { homedir } from 'node:os'; +import { dirname, extname, isAbsolute, join, resolve } from 'node:path'; +import { TorchError } from '../kernel/errors.mjs'; +import { sha256 } from '../kernel/files.mjs'; + +export const RUNTIME_PLUGIN_TRUST_SCHEMA = 'torch.dev/runtime-plugin-trust/v1alpha1'; +const require = createRequire(import.meta.url); +const BUILT_INS = new Set(['claude', 'codex', 'pi']); + +function pluginError(message, code, details) { + return new TorchError(message, { code, details }); +} + +export function runtimePluginTrustPath(env = process.env) { + const configHome = resolve(env.XDG_CONFIG_HOME || join(homedir(), '.config')); + return join(configHome, 'torch', 'runtime-adapters.json'); +} + +function validatePluginName(name) { + if (typeof name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(name)) { + throw pluginError('Runtime plugin name must be a lowercase adapter identifier.', 'RUNTIME_PLUGIN_INVALID', { name }); + } + if (BUILT_INS.has(name)) { + throw pluginError(`Runtime plugin cannot replace the built-in adapter: ${name}`, 'RUNTIME_PLUGIN_RESERVED', { name }); + } + return name; +} + +function inspectEntrypoint(modulePath) { + if (typeof modulePath !== 'string' || !modulePath.trim() || !isAbsolute(modulePath)) { + throw pluginError('Runtime plugin entrypoint must be an absolute local path.', 'RUNTIME_PLUGIN_PATH_INVALID', { modulePath }); + } + const path = resolve(modulePath); + if (extname(path) !== '.cjs') { + throw pluginError('Runtime plugin entrypoints must be standalone CommonJS .cjs files.', 'RUNTIME_PLUGIN_FORMAT_UNSUPPORTED', { path }); + } + let metadata; + let canonicalPath; + try { + metadata = lstatSync(path); + canonicalPath = realpathSync(path); + } catch (error) { + throw pluginError(`Cannot inspect runtime plugin entrypoint: ${path}`, 'RUNTIME_PLUGIN_UNAVAILABLE', { + path, cause: error.message, + }); + } + if (!metadata.isFile() || canonicalPath !== path) { + throw pluginError('Runtime plugin entrypoint must be a regular, non-symlink file at its canonical path.', 'RUNTIME_PLUGIN_PATH_UNSAFE', { + path, canonicalPath, + }); + } + return { path, digest: sha256(readFileSync(path)) }; +} + +function emptyStore() { + return { schema: RUNTIME_PLUGIN_TRUST_SCHEMA, plugins: [] }; +} + +export function readRuntimePluginTrust({ env = process.env } = {}) { + const path = runtimePluginTrustPath(env); + if (!existsSync(path)) return { path, store: emptyStore() }; + let metadata; + let store; + try { + metadata = lstatSync(path); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw pluginError('Runtime plugin trust store must be a regular non-symlink file.', 'RUNTIME_PLUGIN_STORE_UNSAFE', { path }); + } + store = JSON.parse(readFileSync(path, 'utf8')); + } catch (error) { + if (error instanceof TorchError) throw error; + throw pluginError(`Cannot read runtime plugin trust store: ${path}`, 'RUNTIME_PLUGIN_STORE_INVALID', { + path, cause: error.message, + }); + } + if (store?.schema !== RUNTIME_PLUGIN_TRUST_SCHEMA || !Array.isArray(store.plugins)) { + throw pluginError('Runtime plugin trust store has an unsupported schema.', 'RUNTIME_PLUGIN_STORE_INVALID', { path }); + } + const names = new Set(); + for (const record of store.plugins) { + if (!record || typeof record.name !== 'string' || typeof record.modulePath !== 'string' + || !/^[a-z][a-z0-9-]*$/.test(record.name) || !/^[a-f0-9]{64}$/.test(record.sha256) + || typeof record.trustedAt !== 'string' || names.has(record.name)) { + throw pluginError('Runtime plugin trust store contains an invalid or duplicate record.', 'RUNTIME_PLUGIN_STORE_INVALID', { path }); + } + names.add(record.name); + } + return { path, store }; +} + +function writeStore(path, store) { + const directory = dirname(path); + mkdirSync(directory, { recursive: true, mode: 0o700 }); + chmodSync(directory, 0o700); + const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`; + writeFileSync(temporary, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + renameSync(temporary, path); + return path; +} + +export function planRuntimePluginTrust({ name, modulePath, env = process.env } = {}) { + const adapterName = validatePluginName(name); + const entrypoint = inspectEntrypoint(modulePath); + const { path: storePath, store } = readRuntimePluginTrust({ env }); + const existing = store.plugins.find((plugin) => plugin.name === adapterName); + return { + action: existing ? 'replace-runtime-plugin-trust' : 'trust-runtime-plugin', + name: adapterName, + modulePath: entrypoint.path, + sha256: entrypoint.digest, + previousSha256: existing?.sha256 ?? null, + trustStorePath: storePath, + executesPluginDuringPlan: false, + trustEffect: 'The exact entrypoint module is allowed to execute in TORCH with the current user permissions.', + mutationPerformed: false, + }; +} + +export function trustRuntimePlugin({ + name, modulePath, env = process.env, approved = false, expectedSha256, now = () => new Date(), +} = {}) { + const plan = planRuntimePluginTrust({ name, modulePath, env }); + if (!approved) { + throw pluginError('Trusting a runtime plugin requires explicit user approval.', 'APPROVAL_REQUIRED', { + action: plan.action, name: plan.name, modulePath: plan.modulePath, sha256: plan.sha256, + }); + } + if (expectedSha256 !== plan.sha256) { + throw pluginError('Runtime plugin changed since review; inspect the new path and hash before trusting it.', 'RUNTIME_PLUGIN_REVIEW_STALE', { + reviewedSha256: expectedSha256 ?? null, currentSha256: plan.sha256, + }); + } + const { path, store } = readRuntimePluginTrust({ env }); + const plugins = store.plugins.filter((plugin) => plugin.name !== plan.name); + plugins.push({ name: plan.name, modulePath: plan.modulePath, sha256: plan.sha256, trustedAt: now().toISOString() }); + writeStore(path, { ...store, plugins: plugins.sort((left, right) => left.name.localeCompare(right.name)) }); + return { ...plan, mutationPerformed: true, sessionsStarted: false }; +} + +export function revokeRuntimePlugin({ name, env = process.env, approved = false, expectedSha256 } = {}) { + const adapterName = validatePluginName(name); + const { path, store } = readRuntimePluginTrust({ env }); + const existing = store.plugins.find((plugin) => plugin.name === adapterName); + const plan = planRuntimePluginRevoke({ name: adapterName, env }); + if (!existing) return { ...plan, disposition: 'not-trusted' }; + if (!approved) throw pluginError('Revoking a runtime plugin requires explicit user approval.', 'APPROVAL_REQUIRED', plan); + if (expectedSha256 !== existing.sha256) { + throw pluginError('Runtime plugin trust changed since review; inspect the current record before revoking it.', 'RUNTIME_PLUGIN_REVIEW_STALE', { + reviewedSha256: expectedSha256 ?? null, currentSha256: existing.sha256, + }); + } + writeStore(path, { ...store, plugins: store.plugins.filter((plugin) => plugin.name !== adapterName) }); + return { ...plan, mutationPerformed: true, disposition: 'revoked' }; +} + +export function planRuntimePluginRevoke({ name, env = process.env } = {}) { + const adapterName = validatePluginName(name); + const { path, store } = readRuntimePluginTrust({ env }); + const existing = store.plugins.find((plugin) => plugin.name === adapterName); + return { + action: 'revoke-runtime-plugin-trust', name: adapterName, + modulePath: existing?.modulePath ?? null, sha256: existing?.sha256 ?? null, + trustStorePath: path, mutationPerformed: false, sessionsStopped: false, + }; +} + +export function inspectRuntimePluginTrust({ env = process.env } = {}) { + const { path, store } = readRuntimePluginTrust({ env }); + const plugins = store.plugins.map((record) => { + try { + const current = inspectEntrypoint(record.modulePath); + return { + ...record, + status: current.digest === record.sha256 ? 'trusted' : 'hash-mismatch', + currentSha256: current.digest, + }; + } catch (error) { + return { ...record, status: 'unavailable', error: { code: error.code, message: error.message } }; + } + }); + return { path, schema: store.schema, plugins, mutationPerformed: false }; +} + +export function loadTrustedRuntimeAdapters({ env = process.env, names } = {}) { + const { store } = readRuntimePluginTrust({ env }); + const adapters = []; + const errors = []; + const allowedNames = names === undefined ? null : new Set(names); + const selected = allowedNames ? store.plugins.filter((record) => allowedNames.has(record.name)) : store.plugins; + for (const record of selected) { + try { + const entrypoint = inspectEntrypoint(record.modulePath); + if (entrypoint.digest !== record.sha256) { + throw pluginError(`Runtime plugin entrypoint changed after it was trusted: ${record.name}`, 'RUNTIME_PLUGIN_HASH_MISMATCH', { + name: record.name, modulePath: record.modulePath, trustedSha256: record.sha256, currentSha256: entrypoint.digest, + }); + } + const resolved = require.resolve(entrypoint.path); + delete require.cache[resolved]; + const plugin = require(entrypoint.path); + if (typeof plugin.createTorchRuntimeAdapter !== 'function') { + throw pluginError('Runtime plugin must export createTorchRuntimeAdapter({ env }).', 'RUNTIME_PLUGIN_CONTRACT_INVALID', { + name: record.name, + }); + } + const adapter = plugin.createTorchRuntimeAdapter({ env }); + if (adapter && typeof adapter.then === 'function') { + throw pluginError('Runtime plugin factories must be synchronous.', 'RUNTIME_PLUGIN_CONTRACT_INVALID', { name: record.name }); + } + if (adapter?.name !== record.name) { + throw pluginError('Runtime plugin adapter name does not match its trusted identifier.', 'RUNTIME_PLUGIN_NAME_MISMATCH', { + expected: record.name, actual: adapter?.name ?? null, + }); + } + adapters.push(adapter); + } catch (error) { + errors.push({ + name: record.name, code: error.code ?? 'RUNTIME_PLUGIN_LOAD_FAILED', message: error.message, + details: error.details, + }); + } + } + return { adapters, errors }; +} diff --git a/src/adapters/registry.mjs b/src/adapters/registry.mjs new file mode 100644 index 0000000..aead37e --- /dev/null +++ b/src/adapters/registry.mjs @@ -0,0 +1,93 @@ +import { TorchError } from '../kernel/errors.mjs'; +import { createClaudeAdapter } from './claude.mjs'; +import { createCodexAdapter } from './codex.mjs'; +import { createPiAdapter } from './pi.mjs'; +import { validateRuntimeAdapter } from './runtime.mjs'; +import { inspectRuntimePluginTrust, loadTrustedRuntimeAdapters } from './plugins.mjs'; + +export class RuntimeAdapterRegistry { + constructor({ env = process.env, adapters = [], loadPlugins = false, pluginNames } = {}) { + this.adapters = new Map([ + ['claude', createClaudeAdapter({ env })], + ['codex', createCodexAdapter({ env })], + ['pi', createPiAdapter({ env })], + ]); + this.pluginErrors = []; + this.trustedPlugins = []; + try { + const inspected = inspectRuntimePluginTrust({ env }); + this.trustedPlugins = inspected.plugins; + if (loadPlugins) { + const loaded = loadTrustedRuntimeAdapters({ env, names: pluginNames }); + this.pluginErrors.push(...loaded.errors); + for (const adapter of loaded.adapters) { + try { this.register(adapter); } catch (error) { + this.pluginErrors.push({ + name: adapter.name, code: error.code ?? 'RUNTIME_PLUGIN_LOAD_FAILED', message: error.message, + details: error.details, + }); + } + } + } else { + for (const plugin of inspected.plugins.filter((entry) => entry.status !== 'trusted')) { + this.pluginErrors.push({ + name: plugin.name, + code: plugin.status === 'hash-mismatch' ? 'RUNTIME_PLUGIN_HASH_MISMATCH' : 'RUNTIME_PLUGIN_UNAVAILABLE', + message: plugin.error?.message ?? `Runtime plugin is ${plugin.status}.`, + }); + } + } + } catch (error) { + this.pluginErrors.push({ + name: null, code: error.code ?? 'RUNTIME_PLUGIN_STORE_INVALID', message: error.message, + details: error.details, + }); + } + for (const adapter of adapters) this.register(adapter); + } + + register(adapter) { + validateRuntimeAdapter(adapter); + if (!/^[a-z][a-z0-9-]*$/.test(adapter.name)) { + throw new TorchError(`Invalid runtime adapter name: ${adapter.name}`, { + code: 'INVALID_RUNTIME_ADAPTER', details: { name: adapter.name }, + }); + } + if (this.adapters.has(adapter.name)) { + throw new TorchError(`Runtime adapter already registered: ${adapter.name}`, { + code: 'RUNTIME_ADAPTER_EXISTS', details: { name: adapter.name }, + }); + } + this.adapters.set(adapter.name, adapter); + return adapter; + } + + replace(adapter) { + validateRuntimeAdapter(adapter); + if (!this.adapters.has(adapter.name)) { + throw new TorchError(`Cannot replace an unregistered runtime adapter: ${adapter.name}`, { + code: 'RUNTIME_ADAPTER_MISSING', details: { name: adapter.name }, + }); + } + this.adapters.set(adapter.name, adapter); + return adapter; + } + + get(name) { return this.adapters.get(name); } + + has(name) { return this.adapters.has(name); } + + names() { return [...this.adapters.keys()]; } + + toMap() { return new Map(this.adapters); } + + configuration(name) { + const adapter = this.get(name); + if (!adapter) return undefined; + return structuredClone(adapter.configuration ?? {}); + } +} + +export function createRuntimeAdapterRegistry(options) { + return new RuntimeAdapterRegistry(options); +} diff --git a/src/adapters/runtime.mjs b/src/adapters/runtime.mjs index a514f6c..13a0a5b 100644 --- a/src/adapters/runtime.mjs +++ b/src/adapters/runtime.mjs @@ -12,6 +12,50 @@ export function validateRuntimeAdapter(adapter) { code: 'INVALID_RUNTIME_ADAPTER', }); } + for (const capability of ['modelSelection', 'reasoningSelection']) { + if (typeof adapter.capabilities[capability] !== 'boolean') { + throw new TorchError(`Runtime adapter ${adapter.name} must declare ${capability} as true or false`, { + code: 'INVALID_RUNTIME_ADAPTER', details: { capability }, + }); + } + } + const costCeiling = adapter.capabilities.perInvocationCostCeiling; + if (!costCeiling || typeof costCeiling !== 'object' || Array.isArray(costCeiling)) { + throw new TorchError(`Runtime adapter ${adapter.name} must declare perInvocationCostCeiling modes`, { + code: 'INVALID_RUNTIME_ADAPTER', details: { capability: 'perInvocationCostCeiling' }, + }); + } + for (const operation of ['createSession', 'resumeSession']) { + const modes = costCeiling[operation]; + if (!Array.isArray(modes) || modes.some((mode) => typeof mode !== 'string' + || !/^[a-z][a-z0-9._:-]*$/.test(mode)) || new Set(modes).size !== modes.length) { + throw new TorchError(`Runtime adapter ${adapter.name} must declare valid cost-ceiling modes for ${operation}`, { + code: 'INVALID_RUNTIME_ADAPTER', details: { capability: 'perInvocationCostCeiling', operation }, + }); + } + } + if (adapter.capabilities.modelRequired !== undefined + && typeof adapter.capabilities.modelRequired !== 'boolean') { + throw new TorchError(`Runtime adapter ${adapter.name} must declare modelRequired as a boolean`, { + code: 'INVALID_RUNTIME_ADAPTER', details: { capability: 'modelRequired' }, + }); + } + if (adapter.capabilities.launchPolicy !== undefined) { + const declaration = adapter.capabilities.launchPolicy; + if (!declaration || typeof declaration !== 'object' || Array.isArray(declaration) + || !declaration.fields || typeof declaration.fields !== 'object' || Array.isArray(declaration.fields)) { + throw new TorchError(`Runtime adapter ${adapter.name} must declare launchPolicy.fields`, { + code: 'INVALID_RUNTIME_ADAPTER', details: { capability: 'launchPolicy' }, + }); + } + for (const [field, values] of Object.entries(declaration.fields)) { + if (!field || !Array.isArray(values) || values.some((value) => typeof value !== 'string' || !value)) { + throw new TorchError(`Runtime adapter ${adapter.name} has an invalid launch-policy declaration`, { + code: 'INVALID_RUNTIME_ADAPTER', details: { capability: 'launchPolicy', field }, + }); + } + } + } for (const operation of RUNTIME_OPERATIONS) { if (!(operation in adapter.capabilities)) { throw new TorchError(`Runtime adapter ${adapter.name} does not declare ${operation}`, { @@ -27,8 +71,84 @@ export function validateRuntimeAdapter(adapter) { return adapter; } +export function costCeilingPlanIssues(adapter, operation, maxUsd, runtimePlan) { + if (!Number.isFinite(maxUsd) || maxUsd <= 0) { + return [{ code: 'RUNTIME_COST_CEILING_INVALID', operation, maxUsd }]; + } + const modes = adapter.capabilities.perInvocationCostCeiling?.[operation]; + if (!Array.isArray(modes) || modes.length === 0) { + return [{ code: 'RUNTIME_COST_CEILING_UNSUPPORTED', adapter: adapter.name, operation, maxUsd }]; + } + const receipt = runtimePlan?.costCeiling; + if (receipt?.enforced !== true || receipt.maxUsd !== maxUsd + || typeof receipt.mode !== 'string' || !modes.includes(receipt.mode)) { + return [{ + code: 'RUNTIME_COST_CEILING_UNPROVEN', adapter: adapter.name, operation, maxUsd, + declaredModes: modes, receipt: receipt ?? null, + }]; + } + return []; +} + export function unsupportedCapability(adapterName, operation) { throw new TorchError(`${adapterName} adapter does not support ${operation}`, { code: 'RUNTIME_CAPABILITY_UNSUPPORTED', details: { adapter: adapterName, operation }, }); } + +export function resolveLaunchPolicy({ runtime, adapterDefaults = {}, runtimeConfig = {}, identity = {} } = {}) { + const hasLegacyPolicy = runtime === 'codex' + && (Object.hasOwn(runtimeConfig, 'sandbox') || Object.hasOwn(runtimeConfig, 'approval')); + const legacyPolicy = hasLegacyPolicy ? { + ...(runtimeConfig.sandbox === undefined ? {} : { sandbox: runtimeConfig.sandbox }), + ...(runtimeConfig.approval === undefined ? {} : { approval: runtimeConfig.approval }), + } : {}; + // The historical generated pair described the same effective Codex mode as + // --approve-for-me (which implies workspace-write); do not emit both flags. + if (legacyPolicy.sandbox === 'workspace-write' && legacyPolicy.approval === 'approve-for-me') { + delete legacyPolicy.sandbox; + } + const nestedPolicy = runtimeConfig.launchPolicy ?? {}; + const launchPolicy = { + ...(adapterDefaults.launchPolicy ?? {}), + ...(Object.keys(nestedPolicy).length ? nestedPolicy : legacyPolicy), + ...(identity.launchPolicy ?? {}), + }; + if (Object.keys(nestedPolicy).length && hasLegacyPolicy) launchPolicy._legacyFieldsConflict = 'true'; + return launchPolicy; +} + +export function profileCapabilityIssues(adapter, { model, reasoning, launchPolicy = {} } = {}) { + const issues = []; + if (adapter.capabilities.modelRequired === true && !model) { + issues.push({ field: 'model', capability: 'modelSelection', required: true }); + } + if (model && adapter.capabilities.modelSelection !== true) { + issues.push({ field: 'model', capability: 'modelSelection' }); + } + if (reasoning && adapter.capabilities.reasoningSelection !== true) { + issues.push({ field: 'reasoning', capability: 'reasoningSelection' }); + } + if (launchPolicy && (typeof launchPolicy !== 'object' || Array.isArray(launchPolicy))) { + issues.push({ field: 'launchPolicy', capability: 'launchPolicy', reason: 'must be an object' }); + return issues; + } + const declaration = adapter.capabilities.launchPolicy; + for (const [field, value] of Object.entries(launchPolicy ?? {})) { + const allowed = declaration?.fields?.[field]; + if (!allowed) { + issues.push({ field: `launchPolicy.${field}`, capability: 'launchPolicy', value }); + } else if (typeof value !== 'string' || !allowed.includes(value)) { + issues.push({ field: `launchPolicy.${field}`, capability: 'launchPolicy', value, allowed }); + } + } + for (const conflict of declaration?.conflicts ?? []) { + if (launchPolicy?.[conflict.field] === conflict.value && Object.hasOwn(launchPolicy ?? {}, conflict.with)) { + issues.push({ + field: `launchPolicy.${conflict.field}`, capability: 'launchPolicy', value: conflict.value, + conflictsWith: `launchPolicy.${conflict.with}`, + }); + } + } + return issues; +} diff --git a/src/artifacts/service.mjs b/src/artifacts/service.mjs new file mode 100644 index 0000000..12be242 --- /dev/null +++ b/src/artifacts/service.mjs @@ -0,0 +1,420 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { + chmodSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, + realpathSync, statSync, unlinkSync, writeFileSync, +} from 'node:fs'; +import { join, relative, resolve, sep } from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { DatabaseSync } from 'node:sqlite'; +import { TorchError } from '../kernel/errors.mjs'; +import { readInstallManifest } from '../kernel/install.mjs'; +import { projectStatePath } from '../kernel/paths.mjs'; + +const IMAGE_TYPES = new Map([ + ['.png', 'image/png'], ['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'], + ['.webp', 'image/webp'], ['.gif', 'image/gif'], +]); +const MAX_ARTIFACT_BYTES = 25 * 1024 * 1024; + +function requiredText(value, field) { + if (typeof value !== 'string' || !value.trim()) { + throw new TorchError(`${field} must be a non-empty string`, { + code: 'INVALID_ARTIFACT_INPUT', details: { field }, + }); + } + return value.trim(); +} + +function taskId(value) { + const normalized = requiredText(value, 'taskId'); + if (!/^TASK-[A-Za-z0-9-]+$/.test(normalized)) { + throw new TorchError('Artifact taskId must identify an authoritative backlog item', { + code: 'INVALID_ARTIFACT_TASK', details: { taskId: normalized }, + }); + } + return normalized; +} + +function localRelativePath(value) { + const normalized = requiredText(value, 'file').replaceAll('\\', '/').replace(/^\.\//, ''); + if (normalized.startsWith('/') || normalized.split('/').some((part) => !part || part === '.' || part === '..')) { + throw new TorchError('Artifact source must be a normalized path inside the identity worktree', { + code: 'ARTIFACT_PATH_INVALID', details: { file: value }, + }); + } + return normalized; +} + +function safeSourcePath(root, relativePath) { + const source = resolve(root, ...relativePath.split('/')); + const rel = relative(root, source); + if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || resolve(source) !== source) { + throw new TorchError('Artifact source must remain inside the identity worktree', { + code: 'ARTIFACT_PATH_INVALID', details: { file: relativePath }, + }); + } + let current = root; + for (const part of relativePath.split('/')) { + current = join(current, part); + const stat = (() => { + try { return requireStat(current); } catch (error) { + throw new TorchError(`Cannot inspect artifact source: ${relativePath}`, { + code: 'ARTIFACT_SOURCE_UNAVAILABLE', details: error.message, + }); + } + })(); + if (stat.isSymbolicLink()) { + throw new TorchError('Artifact source cannot traverse symbolic links', { + code: 'ARTIFACT_PATH_INVALID', details: { file: relativePath }, + }); + } + } + const realRoot = realpathSync(root); + const realSource = realpathSync(source); + const realRel = relative(realRoot, realSource); + if (realRel === '..' || realRel.startsWith(`..${sep}`) || !realRel) { + throw new TorchError('Artifact source resolves outside the identity worktree', { + code: 'ARTIFACT_PATH_INVALID', details: { file: relativePath }, + }); + } + return source; +} + +function requireStat(path) { + // Kept behind a helper so every traversed segment gets the same failure mapping. + return statSync(path); +} + +function commitSha(repositoryRoot, value) { + const sha = requiredText(value, 'commit'); + if (!/^[0-9a-f]{40}$/i.test(sha)) { + throw new TorchError('Artifact provenance requires a full Git commit SHA', { + code: 'ARTIFACT_COMMIT_INVALID', details: { commit: sha }, + }); + } + let resolved; + try { + resolved = execFileSync('git', ['-C', repositoryRoot, 'rev-parse', '--verify', `${sha}^{commit}`], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + throw new TorchError('Artifact commit is not present in the local project history', { + code: 'ARTIFACT_COMMIT_INVALID', details: { commit: sha }, + }); + } + if (resolved.toLowerCase() !== sha.toLowerCase()) { + throw new TorchError('Artifact commit must be the exact full commit SHA', { + code: 'ARTIFACT_COMMIT_INVALID', details: { commit: sha, resolved }, + }); + } + return resolved; +} + +function artifactRecord(row) { + return { + id: row.id, projectId: row.project_id, title: row.title, alt: row.alt_text, + taskId: row.task_ref, sessionId: row.runtime_session_id, identityId: row.area_id, + commit: row.commit_sha, mediaType: row.media_type, sha256: row.sha256, + bytes: row.bytes, createdAt: row.created_at, url: `/api/artifacts/${encodeURIComponent(row.id)}`, + }; +} + +function validStoredImage(row) { + const match = /^([a-f0-9-]{36})\.(png|jpe?g|webp|gif)$/i.exec(row.stored_name ?? ''); + return Boolean(match && match[1] === row.id + && IMAGE_TYPES.get(`.${match[2].toLowerCase()}`) === row.media_type); +} + +export class ArtifactService { + constructor({ repositoryRoot, controlPlane, backlogService, clock = () => new Date(), idFactory = randomUUID } = {}) { + this.repositoryRoot = resolve(repositoryRoot ?? controlPlane?.repositoryRoot ?? process.cwd()); + if (!controlPlane || !backlogService) { + throw new TorchError('Artifact service requires the installed control plane and authoritative backlog', { + code: 'ARTIFACT_SERVICE_DEPENDENCY_MISSING', + }); + } + this.controlPlane = controlPlane; + this.backlogService = backlogService; + this.clock = clock; + this.idFactory = idFactory; + this.stateRoot = controlPlane.stateRoot; + this.artifactRoot = join(this.stateRoot, 'artifacts'); + this.database = controlPlane.database; + } + + ensureSchema() { + mkdirSync(this.artifactRoot, { recursive: true, mode: 0o700 }); + const storage = lstatSync(this.artifactRoot); + if (!storage.isDirectory() || storage.isSymbolicLink()) { + throw new TorchError('Artifact storage must be a private, real directory', { code: 'ARTIFACT_STORAGE_INVALID' }); + } + chmodSync(this.artifactRoot, 0o700); + this.database.exec(` + CREATE TABLE IF NOT EXISTS artifacts ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + title TEXT NOT NULL, + alt_text TEXT NOT NULL, + task_ref TEXT NOT NULL, + area_id TEXT NOT NULL, + runtime_session_id TEXT, + commit_sha TEXT NOT NULL, + source_path TEXT NOT NULL, + media_type TEXT NOT NULL, + sha256 TEXT NOT NULL, + bytes INTEGER NOT NULL, + stored_name TEXT NOT NULL, + created_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS artifacts_created ON artifacts(created_at DESC, id); + `); + } + + publish({ areaId, taskId: rawTaskId, title, alt, file, commit } = {}) { + const actor = this.controlPlane.assertIdentity(areaId); + const task = this.backlogService.get(taskId(rawTaskId)); + if (task.owner !== actor) { + throw new TorchError('Only the backlog owner may publish evidence for this task', { + code: 'ARTIFACT_TASK_OWNER_MISMATCH', details: { taskId: task.id, owner: task.owner, actor }, + }); + } + const sourceRelative = localRelativePath(file); + const worktree = (this.controlPlane.manifest.external ?? []).find((entry) => + entry.type === 'worktree' && entry.area === actor)?.path ?? this.repositoryRoot; + if (!existsSync(worktree)) { + throw new TorchError(`Identity worktree is unavailable: ${actor}`, { + code: 'ARTIFACT_WORKTREE_MISSING', details: { areaId: actor }, + }); + } + const extension = sourceRelative.slice(sourceRelative.lastIndexOf('.')).toLowerCase(); + const rootExtension = sourceRelative.slice(sourceRelative.lastIndexOf('.')).toLowerCase(); + const mediaType = IMAGE_TYPES.get(rootExtension); + if (!mediaType) { + throw new TorchError('Review gallery supports PNG, JPEG, WebP, and GIF images', { + code: 'ARTIFACT_MEDIA_TYPE_UNSUPPORTED', details: { extension }, + }); + } + const sourcePath = safeSourcePath(worktree, sourceRelative); + const descriptor = openSync(sourcePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); + let content; + try { + const stat = fstatSync(descriptor); + if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_ARTIFACT_BYTES) { + throw new TorchError('Artifact image must be a regular file no larger than 25 MiB', { + code: 'ARTIFACT_SIZE_INVALID', details: { bytes: stat.size }, + }); + } + content = readFileSync(descriptor); + } finally { closeSync(descriptor); } + const exactCommit = commitSha(this.repositoryRoot, commit); + const identity = this.controlPlane.identity(actor); + const id = this.idFactory(); + if (!/^[a-f0-9-]{36}$/i.test(id)) { + throw new TorchError('Artifact ID factory must return a UUID-shaped value', { code: 'ARTIFACT_ID_INVALID' }); + } + const storedName = `${id}${rootExtension}`; + const storedPath = join(this.artifactRoot, storedName); + const record = { + id, projectId: this.controlPlane.projectId, title: requiredText(title, 'title'), + alt: requiredText(alt, 'alt'), taskId: task.id, areaId: actor, + runtimeSessionId: identity.runtimeSessionId, commit: exactCommit, + sourcePath: sourceRelative, mediaType, sha256: createHash('sha256').update(content).digest('hex'), + bytes: content.byteLength, storedName, createdAt: this.clock().toISOString(), + }; + this.ensureSchema(); + writeFileSync(storedPath, content, { flag: 'wx', mode: 0o600 }); + try { + this.database.exec('BEGIN IMMEDIATE'); + this.database.prepare(` + INSERT INTO artifacts ( + id, project_id, title, alt_text, task_ref, area_id, runtime_session_id, + commit_sha, source_path, media_type, sha256, bytes, stored_name, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + record.id, record.projectId, record.title, record.alt, record.taskId, record.areaId, + record.runtimeSessionId, record.commit, record.sourcePath, record.mediaType, + record.sha256, record.bytes, record.storedName, record.createdAt, + ); + this.controlPlane.audit({ + actorId: actor, operation: 'artifact.publish', entityType: 'artifact', entityId: id, + details: { taskId: task.id, commit: exactCommit, sha256: record.sha256, bytes: record.bytes }, + }); + this.database.exec('COMMIT'); + } catch (error) { + try { this.database.exec('ROLLBACK'); } catch { /* transaction may not have begun */ } + unlinkSync(storedPath); + throw error; + } + return { + id: record.id, projectId: record.projectId, title: record.title, alt: record.alt, + taskId: record.taskId, identityId: record.areaId, sessionId: record.runtimeSessionId, + commit: record.commit, mediaType: record.mediaType, sha256: record.sha256, + bytes: record.bytes, createdAt: record.createdAt, url: `/api/artifacts/${encodeURIComponent(id)}`, + }; + } + + list({ limit = 40 } = {}) { + const bounded = Number.isInteger(limit) && limit > 0 && limit <= 100 ? limit : 40; + if (!this.database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifacts'").get()) return []; + return this.database.prepare('SELECT * FROM artifacts ORDER BY created_at DESC, id LIMIT ?') + .all(bounded).map(artifactRecord); + } + + get(id) { + if (!this.database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifacts'").get()) { + throw new TorchError(`Unknown artifact: ${id}`, { code: 'ARTIFACT_NOT_FOUND' }); + } + const row = this.database.prepare('SELECT * FROM artifacts WHERE id = ?').get(requiredText(id, 'artifactId')); + if (!row) throw new TorchError(`Unknown artifact: ${id}`, { code: 'ARTIFACT_NOT_FOUND' }); + return { ...artifactRecord(row), storedName: row.stored_name }; + } + + comment({ artifactId, body, by } = {}) { + const plan = this.planComment({ artifactId, body, by }); + const artifact = this.get(plan.artifactId); + const content = plan.body; + const createdAt = this.clock().toISOString(); + const messageId = this.idFactory(); + this.database.prepare(` + INSERT INTO messages ( + id, project_id, sender_id, recipient_id, kind, created_at, body, + task_ref, path_ref, commit_ref, handoff_ref + ) VALUES (?, ?, 'owner', ?, 'message', ?, ?, ?, ?, ?, NULL) + `).run( + messageId, artifact.projectId, artifact.identityId, createdAt, content, + artifact.taskId, `artifact:${artifact.id}`, artifact.commit, + ); + this.controlPlane.auditOwnerAction({ + actorId: by, operation: 'artifact.feedback', entityType: 'artifact', entityId: artifact.id, + details: { recipient: artifact.identityId, messageId }, + }); + return { + id: messageId, artifactId: artifact.id, sender: 'owner', recipient: artifact.identityId, + taskId: artifact.taskId, commit: artifact.commit, body: content, createdAt, + }; + } + + planComment({ artifactId, body, by } = {}) { + if (by !== 'owner') { + throw new TorchError('Only the project owner may submit artifact review feedback', { + code: 'OWNER_AUTHORITY_REQUIRED', details: { operation: 'artifact.comment' }, + }); + } + const artifact = this.get(artifactId); + const content = requiredText(body, 'body'); + return { + schema: 'torch.dev/artifact-feedback-plan/v1alpha1', mutationPerformed: false, + artifactId: artifact.id, body: content, by: 'owner', recipient: artifact.identityId, + taskId: artifact.taskId, commit: artifact.commit, + effect: 'Create one durable owner message for the responsible identity, linked to this artifact and backlog task.', + }; + } + +} + +export function observeArtifacts({ database, stateRoot, limit = 40 } = {}) { + const table = database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifacts'").get(); + if (!table) return { + available: false, items: [], + reason: 'No project-local artifact catalog with task, session, and commit provenance is connected yet.', + }; + const bounded = Number.isInteger(limit) && limit > 0 && limit <= 100 ? limit : 40; + const items = database.prepare('SELECT * FROM artifacts ORDER BY created_at DESC, id LIMIT ?').all(bounded); + const comments = database.prepare(` + SELECT id, sender_id AS sender, recipient_id AS recipient, body, created_at AS createdAt, + task_ref AS taskId, commit_ref AS "commit", path_ref AS path + FROM messages WHERE path_ref LIKE 'artifact:%' ORDER BY created_at, id + `).all(); + const messagesByArtifact = new Map(); + for (const message of comments) { + const id = message.path.slice('artifact:'.length); + if (!messagesByArtifact.has(id)) messagesByArtifact.set(id, []); + messagesByArtifact.get(id).push(message); + } + const artifactRoot = join(stateRoot, 'artifacts'); + let storageValid = false; + try { + const directory = lstatSync(artifactRoot); + storageValid = directory.isDirectory() && !directory.isSymbolicLink(); + } catch { /* absent storage is an explicit unavailable state */ } + return { + available: true, + items: items.map((row) => { + const record = artifactRecord(row); + const filePath = validStoredImage(row) ? join(artifactRoot, row.stored_name) : null; + let valid = false; + try { + const stat = storageValid && filePath ? lstatSync(filePath) : null; + valid = Boolean(stat?.isFile() && !stat.isSymbolicLink() + && stat.size === row.bytes && row.bytes <= MAX_ARTIFACT_BYTES); + } catch { /* observation reports unavailable evidence rather than inferring integrity */ } + return { + ...record, integrity: valid ? 'available' : 'unavailable', + url: valid ? record.url : null, + feedback: messagesByArtifact.get(row.id) ?? [], + }; + }), + reason: null, + }; +} + +export function readPublishedArtifact({ repositoryRoot, env = process.env, id } = {}) { + const artifactId = requiredText(id, 'artifactId'); + if (!/^[a-f0-9-]{36}$/i.test(artifactId)) { + throw new TorchError('Artifact ID is invalid', { code: 'ARTIFACT_NOT_FOUND' }); + } + const root = resolve(repositoryRoot); + const manifest = readInstallManifest(root); + const expectedStateRoot = projectStatePath(manifest.projectId, env); + const stateEntry = (manifest.external ?? []).find((entry) => entry.type === 'local-state'); + if (!stateEntry?.path || resolve(stateEntry.path) !== resolve(expectedStateRoot)) { + throw new TorchError('Artifact state does not belong to this installed project', { + code: 'LOCAL_STATE_PATH_MISMATCH', + }); + } + const metadataPath = join(expectedStateRoot, 'project.json'); + const metadata = JSON.parse(readFileSync(metadataPath, 'utf8')); + if (metadata.projectId !== manifest.projectId || resolve(metadata.root) !== root) { + throw new TorchError('Artifact state metadata does not match the current project', { + code: 'LOCAL_STATE_METADATA_MISMATCH', + }); + } + const database = new DatabaseSync(join(expectedStateRoot, 'state.db'), { readOnly: true }); + try { + const table = database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'artifacts'").get(); + if (!table) throw new TorchError(`Unknown artifact: ${artifactId}`, { code: 'ARTIFACT_NOT_FOUND' }); + const row = database.prepare('SELECT * FROM artifacts WHERE id = ? AND project_id = ?').get(artifactId, manifest.projectId); + if (!row || !validStoredImage(row)) { + throw new TorchError(`Unknown artifact: ${artifactId}`, { code: 'ARTIFACT_NOT_FOUND' }); + } + const artifactRoot = join(expectedStateRoot, 'artifacts'); + const storageStat = lstatSync(artifactRoot); + if (!storageStat.isDirectory() || storageStat.isSymbolicLink()) { + throw new TorchError('Artifact storage directory is invalid', { code: 'ARTIFACT_STORAGE_INVALID' }); + } + const realStorageRoot = realpathSync(artifactRoot); + const storageRelative = relative(realpathSync(expectedStateRoot), realStorageRoot); + if (storageRelative.startsWith(`..${sep}`) || storageRelative === '..') { + throw new TorchError('Artifact storage resolves outside project-local state', { code: 'ARTIFACT_PATH_INVALID' }); + } + const storedPath = resolve(artifactRoot, row.stored_name); + const fileRelative = relative(artifactRoot, storedPath); + if (fileRelative.startsWith(`..${sep}`) || fileRelative === '..') { + throw new TorchError('Published artifact resolves outside local artifact storage', { code: 'ARTIFACT_PATH_INVALID' }); + } + const descriptor = openSync(storedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); + let bytes; + try { + const stat = fstatSync(descriptor); + if (!stat.isFile() || stat.size !== row.bytes || stat.size > MAX_ARTIFACT_BYTES) { + throw new TorchError('Published image has invalid size or file type', { code: 'ARTIFACT_INTEGRITY_FAILED' }); + } + bytes = readFileSync(descriptor); + } finally { closeSync(descriptor); } + const digest = createHash('sha256').update(bytes).digest('hex'); + if (digest !== row.sha256) throw new TorchError('Published image failed its stored integrity check', { + code: 'ARTIFACT_INTEGRITY_FAILED', + }); + return { mediaType: row.media_type, bytes }; + } finally { database.close(); } +} diff --git a/src/backlog/activity.mjs b/src/backlog/activity.mjs new file mode 100644 index 0000000..eeb6d0a --- /dev/null +++ b/src/backlog/activity.mjs @@ -0,0 +1,90 @@ +import { execFileSync } from 'node:child_process'; +import { TorchError } from '../kernel/errors.mjs'; + +export function commitTaskReferences(message) { + const references = [...new Set(message.match(/\bTASK-[A-Za-z0-9-]+\b/g) ?? [])]; + const closes = []; + for (const line of message.split('\n')) { + const match = /^Closes:\s*(TASK-[A-Za-z0-9-]+(?:\s*,\s*TASK-[A-Za-z0-9-]+)*)\s*$/i.exec(line); + if (match) closes.push(...match[1].split(',').map((id) => id.trim())); + } + return { references, closes: [...new Set(closes)] }; +} + +/** Bounded, read-only observation. Commit messages are intent, not authority. + * A truncated scan cannot prove absence of recent activity. + */ +export function observeTaskActivity({ + repositoryRoot, tasks, branches, now = new Date(), staleDays = 3, maxCommits = 1000, +} = {}) { + if (!Number.isInteger(staleDays) || staleDays < 1 || staleDays > 365 + || !Number.isInteger(maxCommits) || maxCommits < 1 || maxCommits > 10_000 + || !Number.isFinite(now.getTime())) { + throw new TorchError('Invalid task activity observation limits', { code: 'BACKLOG_ACTIVITY_INPUT_INVALID' }); + } + const git = (args) => execFileSync('git', ['-C', repositoryRoot, ...args], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024, + }); + const refs = [...new Set(branches ?? [])]; + const heads = []; + const unavailableBranches = []; + for (const branch of refs) { + try { heads.push(git(['rev-parse', '--verify', `refs/heads/${branch}^{commit}`]).trim()); } + catch { unavailableBranches.push(branch); } + } + if (!heads.length) throw new TorchError('No managed branches available for activity observation', { code: 'BACKLOG_ACTIVITY_UNAVAILABLE' }); + let output; + try { + output = git(['log', `--max-count=${maxCommits + 1}`, '--format=%H%x00%cI%x00%B%x00', ...new Set(heads), '--']); + } catch (error) { + throw new TorchError('Task activity scan could not complete', { code: 'BACKLOG_ACTIVITY_UNAVAILABLE', details: error.message }); + } + const parts = output.split('\0'); + if (parts.pop()?.trim()) throw new TorchError('Malformed commit activity output', { code: 'BACKLOG_ACTIVITY_UNAVAILABLE' }); + if (parts.length % 3) throw new TorchError('Malformed commit activity output', { code: 'BACKLOG_ACTIVITY_UNAVAILABLE' }); + const commits = []; + for (let index = 0; index < parts.length; index += 3) { + const commit = parts[index].trim(); + const committedAt = parts[index + 1].trim(); + if (!/^[0-9a-f]{40,64}$/.test(commit) || !Number.isFinite(Date.parse(committedAt))) { + throw new TorchError('Malformed commit activity identity', { code: 'BACKLOG_ACTIVITY_UNAVAILABLE' }); + } + commits.push({ commit, committedAt, ...commitTaskReferences(parts[index + 2]) }); + } + const truncated = commits.length > maxCommits; + const scanned = commits.slice(0, maxCommits); + const complete = !truncated && !unavailableBranches.length; + const known = new Set(tasks.map((task) => task.id)); + const unknownReferences = [...new Set(scanned.flatMap((commit) => commit.references).filter((id) => !known.has(id)))]; + const futureCommits = scanned.filter((commit) => Date.parse(commit.committedAt) > now.getTime()).map((commit) => commit.commit); + const observations = tasks.map((task) => { + const linked = scanned.filter((commit) => commit.references.includes(task.id) + && Date.parse(commit.committedAt) <= now.getTime()) + .sort((left, right) => Date.parse(right.committedAt) - Date.parse(left.committedAt) || left.commit.localeCompare(right.commit)); + const lastActivity = linked[0] ?? null; + const created = Date.parse(task.createdAt); + const baseline = lastActivity + ? Math.max(Date.parse(lastActivity.committedAt), Number.isFinite(created) ? created : 0) : created; + const ageDays = Number.isFinite(baseline) && baseline <= now.getTime() + ? Math.floor((now.getTime() - baseline) / 86_400_000) : null; + const terminal = ['completed', 'cancelled'].includes(task.state); + const recent = lastActivity && ageDays !== null && ageDays < staleDays; + const status = terminal ? 'terminal' : recent ? 'recent' : !complete || ageDays === null ? 'unknown' + : ageDays >= staleDays ? 'stale' : 'new'; + return { + taskId: task.id, title: task.title, state: task.state, owner: task.owner ?? null, + ownerRequested: task.history?.[0]?.actorId === 'owner', + status, ageDays, lastActivity, linkedCommits: linked.length, + closureIntent: linked.filter((commit) => commit.closes.includes(task.id)).map((commit) => commit.commit), + expectedWaiting: task.state === 'blocked', + }; + }).sort((left, right) => Number(right.ownerRequested) - Number(left.ownerRequested) + || (right.ageDays ?? -1) - (left.ageDays ?? -1) || left.taskId.localeCompare(right.taskId)); + return { + schema: 'torch.dev/backlog-activity/v1alpha1', generatedAt: now.toISOString(), + provenance: 'git-commit-metadata', staleDays, maxCommits, scannedCommits: scanned.length, + complete, truncated, unavailableBranches, futureCommits, unknownReferences, + tasks: observations, stale: observations.filter((task) => task.status === 'stale'), + mutationPerformed: false, closurePerformed: false, + }; +} diff --git a/src/backlog/service.mjs b/src/backlog/service.mjs index dd14b4d..04c7de0 100644 --- a/src/backlog/service.mjs +++ b/src/backlog/service.mjs @@ -3,9 +3,12 @@ import { execFileSync } from 'node:child_process'; import { closeSync, existsSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, writeSync, } from 'node:fs'; -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; import { TorchError } from '../kernel/errors.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; +import { loadFleetDefinition } from '../kernel/worktrees.mjs'; +import { organizationGraphFromConfig } from '../kernel/organization.mjs'; +import { commitTaskReferences, observeTaskActivity } from './activity.mjs'; export const BACKLOG_STATES = Object.freeze([ 'proposed', 'ready', 'assigned', 'in_progress', 'blocked', @@ -27,6 +30,12 @@ const TRANSITIONS = new Map([ const PRIORITIES = new Set(['urgent', 'high', 'normal', 'low']); const WORKER_TRANSITIONS = new Set(['in_progress', 'blocked', 'verification', 'ready_to_integrate']); const ACTIVE_ASSIGNMENT_STATES = new Set(['assigned', 'in_progress', 'verification', 'ready_to_integrate']); +const PRIORITY_ORDER = { urgent: 0, high: 1, normal: 2, low: 3 }; + +function queueOrder(left, right) { + return (PRIORITY_ORDER[left.priority ?? 'normal'] - PRIORITY_ORDER[right.priority ?? 'normal']) + || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id); +} function requiredText(value, name) { if (typeof value !== 'string' || !value.trim()) { @@ -65,6 +74,41 @@ function optionalObservedAt(value) { return observedAt; } +function optionalInitiativeLabel(value, name) { + if (value === undefined || value === null) return null; + const label = requiredText(value, name); + if (label.length > 120) { + throw new TorchError(`${name} must be at most 120 characters`, { + code: 'INVALID_BACKLOG_INPUT', details: { field: name }, + }); + } + return label; +} + +function normalizeCreateFields({ + title, description, priority = 'normal', affectedDomains = [], dependencies = [], + acceptanceCriteria, observedAt, feature, milestone, +} = {}, { controlPlane, repositoryRoot, getTask }) { + if (!PRIORITIES.has(priority)) { + throw new TorchError(`Invalid backlog priority: ${priority}`, { code: 'INVALID_BACKLOG_PRIORITY' }); + } + const domains = textList(affectedDomains, 'affectedDomains'); + domains.forEach((areaId) => controlPlane.assertIdentity(areaId)); + const dependencyIds = textList(dependencies, 'dependencies'); + dependencyIds.forEach((id) => getTask(taskId(id))); + return { + title: requiredText(title, 'title'), + description: requiredText(description, 'description'), + priority, + affectedDomains: domains, + dependencies: dependencyIds, + acceptanceCriteria: textList(acceptanceCriteria, 'acceptanceCriteria', { required: true }), + observedAt: optionalObservedAt(observedAt) ?? headSha(repositoryRoot), + feature: feature === '' ? null : optionalInitiativeLabel(feature, 'feature'), + milestone: milestone === '' ? null : optionalInitiativeLabel(milestone, 'milestone'), + }; +} + function headSha(repositoryRoot) { try { return execFileSync('git', ['-C', repositoryRoot, 'rev-parse', '--short=12', 'HEAD'], { @@ -210,12 +254,13 @@ function atomicTask(path, task) { export class BacklogService { constructor({ - repositoryRoot, controlPlane, integrationLookup = null, + repositoryRoot, controlPlane, integrationLookup = null, checkService = null, clock = () => new Date(), idFactory = randomUUID, } = {}) { this.repositoryRoot = repositoryRoot; this.controlPlane = controlPlane; this.integrationLookup = integrationLookup; + this.checkService = checkService; this.clock = clock; this.idFactory = idFactory; const manifest = readInstallManifest(repositoryRoot); @@ -323,9 +368,11 @@ export class BacklogService { task: active[0], mutationPerformed: false, }; } - const ready = tasks.find((task) => task.state === 'ready' + const ready = tasks.filter((task) => task.state === 'ready' + && (!task.owner || task.owner === area) && (!task.affectedDomains.length || task.affectedDomains.includes(area)) - && task.dependencies.every((dependency) => tasks.find((item) => item.id === dependency)?.state === 'completed')); + && task.dependencies.every((dependency) => tasks.find((item) => item.id === dependency)?.state === 'completed')) + .sort(queueOrder)[0]; return { schema: 'torch.dev/backlog-next/v1alpha1', areaId: area, disposition: ready ? 'ready' : 'idle', task: ready ?? null, mutationPerformed: false, @@ -333,16 +380,202 @@ export class BacklogService { } health({ staleAfterDays = 7, staleObservedCommits = 50, now = this.clock() } = {}) { - return assessBacklogHealth({ + const result = assessBacklogHealth({ tasks: this.list(), agents: this.controlPlane.listAgents(), now, staleAfterDays, staleObservedCommits, commitDistance: (observedAt) => backlogObservedCommitDistance(this.repositoryRoot, observedAt), }); + try { + result.activity = this.activity({ now }); + result.findings.push(...result.activity.stale.map((task) => ({ + severity: 'warning', code: 'BACKLOG_TASK_NO_RECENT_COMMIT', taskId: task.taskId, + ownerRequested: task.ownerRequested, ageDays: task.ageDays, state: task.state, + recommendation: 'Review linked work and acceptance; revive, close with evidence, or record the real wait. No automatic repair.', + }))); + } catch (error) { + result.activity = { available: false, reason: error.code ?? error.message }; + result.findings.push({ severity: 'warning', code: 'BACKLOG_ACTIVITY_UNAVAILABLE', reason: result.activity.reason, + recommendation: 'Restore managed branch observation before treating absence of commits as evidence.' }); + } + result.healthy = result.findings.length === 0; + return result; + } + + activity({ staleDays, maxCommits, now = this.clock() } = {}) { + const { config } = loadFleetDefinition(this.repositoryRoot); + const manifest = readInstallManifest(this.repositoryRoot); + return observeTaskActivity({ + repositoryRoot: this.repositoryRoot, tasks: this.list(), now, + staleDays: staleDays ?? config.backlog?.activity?.stale_days ?? 3, + maxCommits: maxCommits ?? config.backlog?.activity?.max_commits ?? 1000, + branches: [config.project.main_branch, ...(manifest.external ?? []) + .filter((entry) => entry.type === 'worktree').map((entry) => entry.branch)], + }); + } + + planLandedClosure({ integrationRequest, actorId } = {}) { + const actor = this.controlPlane.assertIdentity(actorId); + if (actor !== 'session-manager') throw new TorchError('Only Fleet Operations may reconcile landed task closure', { + code: 'BACKLOG_AUTHORITY_REQUIRED', + }); + const { config } = loadFleetDefinition(this.repositoryRoot); + const request = this.integrationLookup?.(integrationRequest); + const blockers = []; + const target = config.integration?.target ?? config.project.main_branch; + if (!request || request.state !== 'landed' || request.projectId !== this.controlPlane.projectId + || request.targetBranch !== target || !/^[0-9a-f]{40,64}$/.test(request.sourceCommit ?? '')) { + blockers.push('matching-landed-integration-required'); + } + const sourceArea = config.domains.find((area) => area.id === request?.sourceArea); + if (!sourceArea) blockers.push('current-source-area-required'); + const requiredChecks = [...new Set([ + ...(config.integration?.required_checks ?? []), ...(request?.requiredChecks ?? []), + ...(sourceArea?.required_checks ?? []), + ])]; + if (!this.checkService || !request || !this.checkService.exactPasses({ commit: request.sourceCommit, requiredChecks })) { + blockers.push('current-check-evidence-required'); + } + let closes = []; + if (!blockers.length) { + try { + execFileSync('git', ['-C', this.repositoryRoot, 'merge-base', '--is-ancestor', request.sourceCommit, + `refs/heads/${target}`], { stdio: 'pipe' }); + const message = execFileSync('git', ['-C', this.repositoryRoot, 'show', '-s', '--format=%B', request.sourceCommit], { + encoding: 'utf8', maxBuffer: 1024 * 1024, + }); + closes = commitTaskReferences(message).closes; + } catch { blockers.push('canonical-ancestry-or-message-unavailable'); } + } + const tasks = this.list(); + const candidates = closes.map((taskId) => { + const task = tasks.find((item) => item.id === taskId); + const reasons = []; + const unchanged = task?.state === 'completed' && task.commit === request.sourceCommit + && task.integrationRequest === request.id; + if (!task) reasons.push('unknown-task'); + else if (!unchanged) { + if (task.state !== 'ready_to_integrate') reasons.push('task-not-ready-to-integrate'); + if (task.owner !== request.sourceArea) reasons.push('source-owner-mismatch'); + if (task.commit !== request.sourceCommit) reasons.push('task-commit-mismatch'); + if (!task.evidence.length) reasons.push('task-evidence-required'); + } + return { taskId, revision: task?.revision ?? null, disposition: unchanged ? 'unchanged' : reasons.length ? 'blocked' : 'close', reasons }; + }); + return { schema: 'torch.dev/landed-task-closure/v1alpha1', integrationRequest, + automaticEnabled: config.backlog?.activity?.auto_close_after_landing === true, + canProceed: !blockers.length, blockers, candidates, mutationPerformed: false }; + } + + reconcileLanded({ integrationRequest, actorId, approved = false, automatic = false } = {}) { + this.#assertMutable(); + const initial = this.planLandedClosure({ integrationRequest, actorId }); + if (!approved && !(automatic && initial.automaticEnabled)) throw new TorchError('Landed closure requires explicit approval or approved automatic policy', { + code: 'APPROVAL_REQUIRED', + }); + if (!initial.canProceed) throw new TorchError('Landed task closure evidence is incomplete', { + code: 'BACKLOG_CLOSURE_BLOCKED', details: initial.blockers, + }); + const results = initial.candidates.map((candidate) => this.#withTaskLock(candidate.taskId, () => { + const fresh = this.planLandedClosure({ integrationRequest, actorId }); + if (!fresh.canProceed || (automatic && !fresh.automaticEnabled)) throw new TorchError('Closure policy or evidence changed', { + code: 'BACKLOG_CLOSURE_BLOCKED', details: fresh.blockers, + }); + const current = fresh.candidates.find((item) => item.taskId === candidate.taskId); + if (!current || current.disposition !== 'close') return current ?? { ...candidate, disposition: 'blocked', reasons: ['intent-changed'] }; + const task = this.#applyTransition({ taskId: current.taskId, actorId, to: 'completed', + expectedRevision: current.revision, integrationRequest, note: 'Exact landed Closes intent reconciled with current checks.' }); + return { ...current, disposition: 'completed', revision: task.revision }; + })); + return { ...initial, results, mutationPerformed: results.some((item) => item.disposition === 'completed') }; + } + + planClaim({ areaId } = {}) { + const area = this.controlPlane.assertIdentity(areaId); + const { config } = loadFleetDefinition(this.repositoryRoot); + const graph = organizationGraphFromConfig(config); + if (!graph.roles.some((role) => role.identity_id === area && role.kind === 'specialist' + && role.authority.includes('own-implementation'))) { + throw new TorchError('Only implementation specialists may self-claim work', { code: 'BACKLOG_OWNER_INVALID' }); + } + const current = this.next({ areaId: area }); + if (current.disposition === 'resume') return { + ...current, action: 'claim-next', canProceed: true, blockers: [], policyRequired: false, + }; + const tasks = this.list(); + const candidate = tasks.filter((task) => task.state === 'ready' + && (!task.owner || task.owner === area) && task.affectedDomains.includes(area) + && task.dependencies.every((dependency) => tasks.find((item) => item.id === dependency)?.state === 'completed')) + .sort(queueOrder)[0]; + const blockers = []; + const policy = config.backlog?.self_claim; + if (!policy?.enabled || !policy.areas.includes(area)) blockers.push({ code: 'BACKLOG_SELF_CLAIM_DISABLED' }); + if (!this.managerWorktreeAvailable) blockers.push({ code: 'BACKLOG_WORKTREE_MISSING' }); + const manifest = readInstallManifest(this.repositoryRoot); + const worktree = (manifest.external ?? []).find((entry) => entry.type === 'worktree' && entry.area === area); + let commit = null; + if (!worktree?.path || !existsSync(worktree.path)) blockers.push({ code: 'WORKTREE_MISSING' }); + else { + const git = (args) => execFileSync('git', ['-C', worktree.path, ...args], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); + commit = git(['rev-parse', 'HEAD']); + if (git(['branch', '--show-current']) !== worktree.branch) blockers.push({ code: 'WORKTREE_BRANCH_CHANGED' }); + const entries = git(['status', '--porcelain=v1', '--untracked-files=all']).split('\n').filter(Boolean); + if (entries.length) blockers.push({ code: 'WORKTREE_DIRTY', entries }); + for (const operation of ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'rebase-merge', 'rebase-apply', 'BISECT_LOG']) { + if (existsSync(resolve(worktree.path, git(['rev-parse', '--git-path', operation])))) { + blockers.push({ code: 'WORKTREE_GIT_OPERATION', operation }); + } + } + } + const database = this.controlPlane.database; + const unusual = (table, condition, code) => { + if (!database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table)) return; + const pending = database.prepare(`SELECT id FROM ${table} WHERE project_id = ? AND ${condition}`) + .all(this.controlPlane.projectId, area); + if (pending.length) blockers.push({ code, ids: pending.map((row) => row.id) }); + }; + unusual('worktree_guards', 'area_id = ? AND released_at IS NULL', 'WORKTREE_GUARD_ACTIVE'); + unusual('prepared_checks', "area_id = ? AND state IN ('prepared', 'running')", 'CHECKS_PENDING'); + unusual('integration_requests', "source_area = ? AND state NOT IN ('landed', 'superseded')", 'INTEGRATION_PENDING'); + // Resource tables predate project_id columns. Their database is project-scoped. + for (const [table, condition, code] of [ + ['resource_leases', 'released_at IS NULL', 'RESOURCE_HELD'], + ['resource_requests', "state = 'waiting'", 'RESOURCE_WAITING'], + ]) { + if (database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table)) { + const pending = database.prepare(`SELECT id FROM ${table} WHERE area_id = ? AND ${condition}`).all(area); + if (pending.length) blockers.push({ code, ids: pending.map((row) => row.id) }); + } + } + const approvals = this.controlPlane.listApprovals({ actorId: area, status: 'pending' }); + if (approvals.length) blockers.push({ code: 'APPROVAL_PENDING', ids: approvals.map((approval) => approval.id) }); + return { + schema: 'torch.dev/backlog-claim-plan/v1alpha1', action: 'claim-next', areaId: area, + disposition: blockers.length ? 'blocked' : candidate ? 'ready' : 'idle', + task: candidate ?? null, commit, blockers, canProceed: blockers.length === 0, + mutationPerformed: false, policyRequired: true, + policy: { enabled: Boolean(policy?.enabled), areas: policy?.areas ?? [] }, + }; + } + + claimNext({ areaId } = {}) { + this.#assertMutable(); + return this.#withAssignmentLock(() => { + const plan = this.planClaim({ areaId }); + if (!plan.canProceed || plan.disposition !== 'ready') return plan; + return this.#withTaskLock(plan.task.id, () => { + const updated = this.#applyTransition({ + taskId: plan.task.id, actorId: plan.areaId, owner: plan.areaId, + to: 'assigned', expectedRevision: plan.task.revision, note: 'Claimed under approved self-claim policy.', + }, { selfClaim: true }); + return { ...plan, disposition: 'claimed', task: updated, mutationPerformed: true }; + }); + }); } create({ - actorId, title, description, priority = 'normal', affectedDomains = [], - dependencies = [], acceptanceCriteria, observedAt, + actorId, ...input } = {}) { this.#assertMutable(); const actor = this.controlPlane.assertIdentity(actorId); @@ -351,48 +584,179 @@ export class BacklogService { code: 'BACKLOG_AUTHORITY_REQUIRED', details: { actorId: actor }, }); } - if (!PRIORITIES.has(priority)) { - throw new TorchError(`Invalid backlog priority: ${priority}`, { code: 'INVALID_BACKLOG_PRIORITY' }); - } - const domains = textList(affectedDomains, 'affectedDomains'); - domains.forEach((areaId) => this.controlPlane.assertIdentity(areaId)); - const dependencyIds = textList(dependencies, 'dependencies'); - dependencyIds.forEach((id) => this.get(id)); + const fields = normalizeCreateFields(input, { + controlPlane: this.controlPlane, repositoryRoot: this.repositoryRoot, getTask: (id) => this.get(id), + }); + return this.#writeTask(fields, actor); + } + + planOwnerCreate({ actorId, ...input } = {}) { + this.#assertMutable(); + this.controlPlane.assertOwnerActor(actorId); + const fields = normalizeCreateFields(input, { + controlPlane: this.controlPlane, repositoryRoot: this.repositoryRoot, getTask: (id) => this.get(id), + }); + return { + schema: 'torch.dev/backlog-create-plan/v1alpha1', + task: { ...fields, state: 'proposed', owner: null }, + authority: 'Project owner may propose new work; the Session Manager retains triage and assignment authority.', + effect: 'Create one proposed, unassigned backlog task. It will not be assigned, dispatched, or start a session.', + mutationPerformed: false, + }; + } + + createOwner({ actorId, ...input } = {}) { + this.#assertMutable(); + const actor = this.controlPlane.assertOwnerActor(actorId); + const fields = normalizeCreateFields(input, { + controlPlane: this.controlPlane, repositoryRoot: this.repositoryRoot, getTask: (id) => this.get(id), + }); + return this.#writeTask(fields, actor, { ownerAction: true }); + } + + #writeTask(fields, actor, { ownerAction = false } = {}) { const now = this.clock().toISOString(); const task = { schema: 'torch.dev/backlog-item/v1alpha1', - id: taskId(`TASK-${this.idFactory()}`), title: requiredText(title, 'title'), - description: requiredText(description, 'description'), priority, - state: 'proposed', owner: null, affectedDomains: domains, dependencies: dependencyIds, - acceptanceCriteria: textList(acceptanceCriteria, 'acceptanceCriteria', { required: true }), + id: taskId(`TASK-${this.idFactory()}`), ...fields, + state: 'proposed', owner: null, evidence: [], commit: null, integrationRequest: null, blockedReason: null, - observedAt: optionalObservedAt(observedAt) ?? headSha(this.repositoryRoot), createdAt: now, updatedAt: now, revision: 1, history: [{ from: null, to: 'proposed', actorId: actor, at: now, note: 'Task created.' }], }; const path = this.#path(task.id); if (existsSync(path)) throw new TorchError(`Backlog task already exists: ${task.id}`, { code: 'BACKLOG_TASK_EXISTS' }); atomicTask(path, task); - this.controlPlane.audit({ + const audit = { actorId: actor, operation: 'backlog.create', entityType: 'backlog-task', entityId: task.id, - details: { state: task.state, priority }, - }); + details: { state: task.state, priority: task.priority, affectedDomains: task.affectedDomains }, + }; + if (ownerAction) this.controlPlane.auditOwnerAction(audit); + else this.controlPlane.audit(audit); return task; } + classify({ + taskId: id, actorId, expectedRevision, feature, milestone, + clearFeature = false, clearMilestone = false, reason, + } = {}) { + this.#assertMutable(); + return this.#withTaskLock(id, () => { + const actor = this.controlPlane.assertIdentity(actorId); + if (actor !== 'session-manager') { + throw new TorchError('Only the Session Manager may classify backlog work by feature or milestone', { + code: 'BACKLOG_AUTHORITY_REQUIRED', details: { actorId: actor }, + }); + } + if (typeof clearFeature !== 'boolean' || typeof clearMilestone !== 'boolean' + || clearFeature && feature !== undefined || clearMilestone && milestone !== undefined) { + throw new TorchError('Choose either a new initiative label or its explicit clear action', { + code: 'INVALID_BACKLOG_CLASSIFICATION', + }); + } + const hasFeature = feature !== undefined || clearFeature; + const hasMilestone = milestone !== undefined || clearMilestone; + if (!hasFeature && !hasMilestone) { + throw new TorchError('Classifying backlog work requires --feature or --milestone', { + code: 'INVALID_BACKLOG_CLASSIFICATION', + }); + } + const task = this.get(id); + if (!Number.isInteger(expectedRevision) || expectedRevision !== task.revision) { + throw new TorchError('Backlog task changed since it was read', { + code: 'BACKLOG_REVISION_CONFLICT', details: { expectedRevision, actualRevision: task.revision }, + }); + } + const nextFeature = clearFeature ? null + : (feature !== undefined ? optionalInitiativeLabel(feature, 'feature') : task.feature ?? null); + const nextMilestone = clearMilestone ? null + : (milestone !== undefined ? optionalInitiativeLabel(milestone, 'milestone') : task.milestone ?? null); + if (nextFeature === (task.feature ?? null) && nextMilestone === (task.milestone ?? null)) { + return { ...task, mutationPerformed: false, disposition: 'unchanged' }; + } + const now = this.clock().toISOString(); + const record = { + actorId: actor, at: now, reason: requiredText(reason, 'reason'), + from: { feature: task.feature ?? null, milestone: task.milestone ?? null }, + to: { feature: nextFeature, milestone: nextMilestone }, + }; + const updated = { + ...task, feature: nextFeature, milestone: nextMilestone, + updatedAt: now, revision: task.revision + 1, + classificationHistory: [...(task.classificationHistory ?? []), record], + }; + atomicTask(this.#path(task.id), updated); + this.controlPlane.audit({ + actorId: actor, operation: 'backlog.classify', entityType: 'backlog-task', entityId: task.id, + details: { revision: updated.revision, feature: nextFeature, milestone: nextMilestone }, + }); + return { ...updated, mutationPerformed: true, disposition: 'classified' }; + }); + } + + planPriorityChange({ taskId: id, actorId, expectedRevision, priority, reason } = {}) { + this.#assertMutable(); + this.controlPlane.assertOwnerActor(actorId); + if (!PRIORITIES.has(priority)) { + throw new TorchError(`Invalid backlog priority: ${priority}`, { code: 'INVALID_BACKLOG_PRIORITY' }); + } + const task = this.get(id); + if (!Number.isInteger(expectedRevision) || expectedRevision !== task.revision) { + throw new TorchError('Backlog task changed since it was read', { + code: 'BACKLOG_REVISION_CONFLICT', details: { expectedRevision, actualRevision: task.revision }, + }); + } + return { + schema: 'torch.dev/backlog-priority-plan/v1alpha1', mutationPerformed: false, + taskId: task.id, title: task.title, revision: task.revision, + from: task.priority ?? 'normal', to: priority, reason: requiredText(reason, 'reason'), + effect: 'Change only this task priority. State, owner, evidence, dependencies, and queue history stay unchanged.', + }; + } + + setPriority({ taskId: id, actorId, expectedRevision, priority, reason } = {}) { + this.#assertMutable(); + return this.#withTaskLock(id, () => { + const actor = this.controlPlane.assertOwnerActor(actorId); + if (!PRIORITIES.has(priority)) { + throw new TorchError(`Invalid backlog priority: ${priority}`, { code: 'INVALID_BACKLOG_PRIORITY' }); + } + const task = this.get(id); + if (!Number.isInteger(expectedRevision) || expectedRevision !== task.revision) { + throw new TorchError('Backlog task changed since it was read', { + code: 'BACKLOG_REVISION_CONFLICT', details: { expectedRevision, actualRevision: task.revision }, + }); + } + const note = requiredText(reason, 'reason'); + if ((task.priority ?? 'normal') === priority) { + return { ...task, mutationPerformed: false, disposition: 'unchanged' }; + } + const now = this.clock().toISOString(); + const updated = { + ...task, priority, updatedAt: now, revision: task.revision + 1, + priorityHistory: [...(task.priorityHistory ?? []), { + actorId: actor, at: now, reason: note, from: task.priority ?? 'normal', to: priority, + }], + }; + atomicTask(this.#path(task.id), updated); + this.controlPlane.auditOwnerAction({ + actorId: actor, operation: 'backlog.priority', entityType: 'backlog-task', entityId: task.id, + details: { revision: updated.revision, from: task.priority ?? 'normal', to: priority }, + }); + return { ...updated, mutationPerformed: true, disposition: 'reprioritized' }; + }); + } + transition(input = {}) { this.#assertMutable(); - return this.#withTaskLock(input.taskId, () => ( - input.to === 'assigned' - ? this.#withAssignmentLock(() => this.#applyTransition(input)) - : this.#applyTransition(input) - )); + const apply = () => this.#withTaskLock(input.taskId, () => this.#applyTransition(input)); + return ACTIVE_ASSIGNMENT_STATES.has(input.to) ? this.#withAssignmentLock(apply) : apply(); } #applyTransition({ taskId: id, actorId, to, expectedRevision, owner, evidence = [], commit, integrationRequest, blockedReason, note, - } = {}) { + } = {}, { selfClaim = false } = {}) { const actor = this.controlPlane.assertIdentity(actorId); const target = requiredText(to, 'to'); if (!BACKLOG_STATES.includes(target)) { @@ -409,7 +773,11 @@ export class BacklogService { code: 'BACKLOG_TRANSITION_INVALID', details: { from: task.state, to: target }, }); } - if (actor !== 'session-manager' && (actor !== task.owner || !WORKER_TRANSITIONS.has(target))) { + if (selfClaim && (target !== 'assigned' || actor !== owner || task.state !== 'ready' + || !task.affectedDomains.includes(actor))) { + throw new TorchError('Self-claim is restricted to routed ready work for this specialist', { code: 'BACKLOG_AUTHORITY_REQUIRED' }); + } + if (actor !== 'session-manager' && !selfClaim && (actor !== task.owner || !WORKER_TRANSITIONS.has(target))) { throw new TorchError(`${actor} may not transition ${task.id} to ${target}`, { code: 'BACKLOG_AUTHORITY_REQUIRED', details: { actorId: actor, owner: task.owner, to: target }, }); @@ -439,12 +807,14 @@ export class BacklogService { }); } } + } + if (ACTIVE_ASSIGNMENT_STATES.has(target) && nextOwner) { const existing = this.list().find((candidate) => candidate.id !== task.id - && candidate.owner === assigned && ACTIVE_ASSIGNMENT_STATES.has(candidate.state)); + && candidate.owner === nextOwner && ACTIVE_ASSIGNMENT_STATES.has(candidate.state)); if (existing) { - throw new TorchError(`${assigned} already has active backlog work`, { + throw new TorchError(`${nextOwner} already has active backlog work`, { code: 'BACKLOG_OWNER_BUSY', - details: { owner: assigned, taskId: existing.id, state: existing.state }, + details: { owner: nextOwner, taskId: existing.id, state: existing.state }, }); } } @@ -488,10 +858,10 @@ export class BacklogService { }; atomicTask(this.#path(task.id), updated); this.controlPlane.audit({ - actorId: actor, operation: 'backlog.transition', entityType: 'backlog-task', entityId: task.id, + actorId: actor, operation: selfClaim ? 'backlog.self-claim' : 'backlog.transition', entityType: 'backlog-task', entityId: task.id, details: { from: task.state, to: target, revision: updated.revision }, }); - if (target === 'assigned') { + if (target === 'assigned' && !selfClaim) { this.controlPlane.sendMessage({ sender: actor, recipient: updated.owner, body: `Assigned ${updated.id}: ${updated.title}`, diff --git a/src/canonical/fetch.mjs b/src/canonical/fetch.mjs new file mode 100644 index 0000000..966f721 --- /dev/null +++ b/src/canonical/fetch.mjs @@ -0,0 +1,100 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import { loadProjectConfig } from '../kernel/config.mjs'; +import { TorchError } from '../kernel/errors.mjs'; + +const runGit = (root, args, timeout = 10_000) => spawnSync('git', ['-C', root, ...args], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout, maxBuffer: 1_048_576, + env: { ...process.env, GIT_TERMINAL_PROMPT: '0', LC_ALL: 'C' }, +}); +const hash = (value) => createHash('sha256').update(value).digest('hex'); + +export function planCanonicalFetch({ repositoryRoot } = {}) { + const config = loadProjectConfig(repositoryRoot); + const remote = config.repository.canonical.remote; + const branch = config.project.main_branch; + if (!remote || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(remote) + || runGit(repositoryRoot, ['check-ref-format', '--branch', branch]).status !== 0) { + throw new TorchError('Canonical remote or branch is unavailable', { code: 'CANONICAL_FETCH_CONFIG_INVALID' }); + } + const url = runGit(repositoryRoot, ['remote', 'get-url', '--', remote]); + if (url.status !== 0) throw new TorchError('Configured canonical remote is missing', { code: 'CANONICAL_FETCH_CONFIG_INVALID' }); + const observed = runGit(repositoryRoot, ['ls-remote', '--heads', '--', remote, `refs/heads/${branch}`]); + const lines = (observed.stdout ?? '').trim().split('\n'); + const match = lines.length === 1 && /^([a-f0-9]{40,64})\s+(.+)$/.exec(lines[0]); + if (observed.status !== 0 || !match || match[2] !== `refs/heads/${branch}`) { + throw new TorchError('Canonical branch cannot be resolved; no fetch was attempted', { code: 'CANONICAL_FETCH_HEAD_UNAVAILABLE' }); + } + return { action: 'fetch-canonical-objects', remote, branch, commit: match[1], + destinationHash: hash(url.stdout.trim()), requiresConfirmation: true, + changesBranches: false, changesWorkingTree: false, publishesRemote: false, + mutationPerformed: false }; +} + +// Only object import is retried. Never share this classifier with pushes/deploys. +export function classifyFetchFailure(result) { + const error = String(result?.stderr ?? ''); + if (/authentication|permission denied|access denied|could not read username|repository not found|not a git repository|host key verification failed|certificate/i.test(error)) return 'permanent'; + if (result?.error?.code === 'ETIMEDOUT' + || /could not resolve host|temporary failure in name resolution|connection timed out|connection reset|failed to connect|remote end hung up unexpectedly|HTTP (?:502|503|504)\b/i.test(error)) return 'transient-network'; + return 'unclassified'; +} + +export function fetchCanonicalObjects({ repositoryRoot, controlPlane, actorId = 'owner', attempts = 3, + executor = runGit, clock = () => new Date(), idFactory = randomUUID } = {}) { + if (!controlPlane) throw new TorchError('Fetch requires owner audit', { code: 'CANONICAL_FETCH_AUDIT_REQUIRED' }); + controlPlane.assertOwnerActor(actorId); + if (!Number.isInteger(attempts) || attempts < 1 || attempts > 5) { + throw new TorchError('Fetch attempts must be between one and five', { code: 'CANONICAL_FETCH_INPUT_INVALID' }); + } + const plan = planCanonicalFetch({ repositoryRoot }); + const database = controlPlane.database; + database.exec(`CREATE TABLE IF NOT EXISTS canonical_fetches ( + id TEXT PRIMARY KEY, project_id TEXT NOT NULL, remote TEXT NOT NULL, branch TEXT NOT NULL, + commit_sha TEXT NOT NULL, destination_hash TEXT NOT NULL, state TEXT NOT NULL, + created_at TEXT NOT NULL, updated_at TEXT NOT NULL, attempt_limit INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS canonical_fetch_attempts ( + fetch_id TEXT NOT NULL, ordinal INTEGER NOT NULL, state TEXT NOT NULL, + classification TEXT, started_at TEXT NOT NULL, finished_at TEXT, + PRIMARY KEY(fetch_id, ordinal));`); + const id = idFactory(); + const started = clock().toISOString(); + database.prepare('INSERT INTO canonical_fetches VALUES (?,?,?,?,?,?,?,?,?,?)').run(id, + controlPlane.projectId, plan.remote, plan.branch, plan.commit, plan.destinationHash, 'running', started, started, attempts); + let state = 'failed'; + for (let ordinal = 1; ordinal <= attempts; ordinal += 1) { + const config = loadProjectConfig(repositoryRoot); + const currentUrl = runGit(repositoryRoot, ['remote', 'get-url', '--', plan.remote]); + if (config.repository.canonical.remote !== plan.remote || config.project.main_branch !== plan.branch + || currentUrl.status !== 0 || hash(currentUrl.stdout.trim()) !== plan.destinationHash) { + state = 'policy-changed'; + break; + } + database.prepare('INSERT INTO canonical_fetch_attempts VALUES (?,?,?,?,?,?)') + .run(id, ordinal, 'running', null, clock().toISOString(), null); + let result; + try { + result = executor(repositoryRoot, ['fetch', '--no-tags', '--no-write-fetch-head', '--no-recurse-submodules', + '--no-auto-maintenance', '--refmap=', '--', currentUrl.stdout.trim(), plan.commit], 30_000); + } catch { result = { status: null, stderr: '' }; } + const verified = result?.status === 0 && !result.error && !result.signal + && runGit(repositoryRoot, ['cat-file', '-e', `${plan.commit}^{commit}`]).status === 0; + const classification = verified ? 'verified-object-import' : classifyFetchFailure(result); + database.prepare('UPDATE canonical_fetch_attempts SET state = ?, classification = ?, finished_at = ? WHERE fetch_id = ? AND ordinal = ?') + .run(verified ? 'succeeded' : 'failed', classification, clock().toISOString(), id, ordinal); + if (verified) { state = 'succeeded'; break; } + if (classification !== 'transient-network') break; + } + database.exec('BEGIN'); + try { + database.prepare('UPDATE canonical_fetches SET state = ?, updated_at = ? WHERE id = ?') + .run(state, clock().toISOString(), id); + controlPlane.auditOwnerAction({ actorId, operation: 'canonical.fetch', entityType: 'canonical-fetch', entityId: id, + details: { remote: plan.remote, branch: plan.branch, commit: plan.commit, state } }); + database.exec('COMMIT'); + } catch (error) { database.exec('ROLLBACK'); throw error; } + return { ...plan, id, state, succeeded: state === 'succeeded', mutationPerformed: true, + attempts: database.prepare(`SELECT ordinal, state, classification, started_at AS startedAt, + finished_at AS finishedAt FROM canonical_fetch_attempts WHERE fetch_id = ? ORDER BY ordinal`).all(id), + evidence: 'Local object presence plus Git exit status; not deployment or remote publication evidence' }; +} diff --git a/src/checks/conditions.mjs b/src/checks/conditions.mjs new file mode 100644 index 0000000..02e95e8 --- /dev/null +++ b/src/checks/conditions.mjs @@ -0,0 +1,100 @@ +const MAX_REPORT_BYTES = 65_536; + +function inspectReport(policy, report, subject) { + if (!report || report.schema !== 'torch.dev/check-conditions/v1alpha1' + || report.subject?.commit !== subject.commit + || (subject.inputDigest && report.subject?.inputDigest !== subject.inputDigest) + || !report.conditions || Array.isArray(report.conditions) || typeof report.conditions !== 'object') { + return 'condition-subject-or-report-invalid'; + } + if (Object.hasOwn(report, 'runtimeId') + && (typeof report.runtimeId !== 'string' || !report.runtimeId.trim() || report.runtimeId.length > 256)) { + return 'condition-runtime-identity-invalid'; + } + for (const requirement of policy.required) { + if (!Object.hasOwn(report.conditions, requirement.id)) return `condition-missing:${requirement.id}`; + if (report.conditions[requirement.id] !== requirement.equals) return `condition-mismatch:${requirement.id}`; + } + return null; +} + +function probe({ definition, subject, cwd, executor, clock }) { + const policy = definition.conditions; + const startedAt = clock().toISOString(); + let result; + try { + result = executor(policy.command, policy.args ?? [], { + cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + timeout: (policy.timeout_seconds ?? 30) * 1000, maxBuffer: MAX_REPORT_BYTES, + env: { ...process.env, TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' }, + }); + } catch (error) { + return { valid: false, reason: 'condition-probe-executor-failed', startedAt, + finishedAt: clock().toISOString(), error: error.message, report: null }; + } + result ??= { status: null }; + const raw = result.stdout?.toString() ?? ''; + let report = null; + let reason = null; + if (result.status !== 0 || result.error || result.signal) reason = 'condition-probe-failed'; + else if (Buffer.byteLength(raw) > MAX_REPORT_BYTES) reason = 'condition-report-too-large'; + else { + try { report = JSON.parse(raw); } catch { reason = 'condition-report-malformed'; } + if (!reason) reason = inspectReport(policy, report, subject); + } + return { valid: !reason, reason, report, startedAt, finishedAt: clock().toISOString(), + exitStatus: result.status ?? null, signal: result.signal ?? null }; +} + +/** Project probes report conditions; TORCH validates their contract, not sensor truth. + * Before/after checks must observe the same test subject in the project harness. + */ +export function validConditionEvidence({ definition, conditions, commit }) { + if (!definition.conditions) return true; + if (conditions?.before?.valid !== true || conditions?.after?.valid !== true + || conditions.subject?.commit !== commit) return false; + const before = conditions.before.report; + const after = conditions.after.report; + return !inspectReport(definition.conditions, before, conditions.subject) + && !inspectReport(definition.conditions, after, conditions.subject) + && (before.runtimeId === undefined || after.runtimeId === before.runtimeId); +} + +export function executeConditionedCheck({ definition, subject, cwd, executor, clock }) { + const conditions = definition.conditions ? { + provenance: 'project-reported', subject, + required: definition.conditions.required, before: null, after: null, + } : null; + let result; + let invalidReason = null; + if (conditions) { + conditions.before = probe({ definition, subject, cwd, executor, clock }); + if (!conditions.before.valid) return { + result: { status: null, stdout: `TORCH_CHECK_CONDITIONS ${JSON.stringify(conditions.before)}\n`, + stderr: conditions.before.reason }, + conditions, invalidReason: 'test-conditions-invalid-before', measurementsExecuted: false, + }; + } + try { + result = executor(definition.command, definition.args ?? [], { + cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, TORCH_CHECK_COMMIT: subject.commit, TORCH_CHECK_INPUT_DIGEST: subject.inputDigest ?? '' }, + }); + if (result.error || result.signal) invalidReason = 'executor-failed'; + } catch (error) { + result = { status: null, stdout: '', stderr: error.message }; + invalidReason = 'executor-failed'; + } + if (conditions) { + conditions.after = probe({ definition, subject, cwd, executor, clock }); + if (conditions.after.valid && conditions.before.report.runtimeId !== undefined + && conditions.after.report.runtimeId !== conditions.before.report.runtimeId) { + conditions.after = { ...conditions.after, valid: false, reason: 'condition-runtime-changed' }; + } + if (!conditions.after.valid) invalidReason = 'test-conditions-invalid-after'; + } + // Keep the diagnostic line ahead of measurement output in the retained log. + if (conditions) result = { ...result, stdout: + `TORCH_CHECK_CONDITIONS ${JSON.stringify(conditions.before)}\n${result.stdout?.toString() ?? ''}\nTORCH_CHECK_CONDITIONS_AFTER ${JSON.stringify(conditions.after)}\n` }; + return { result, conditions, invalidReason, measurementsExecuted: true }; +} diff --git a/src/checks/runner.mjs b/src/checks/runner.mjs new file mode 100644 index 0000000..968ab73 --- /dev/null +++ b/src/checks/runner.mjs @@ -0,0 +1,30 @@ +import { readFileSync } from 'node:fs'; +import { hostname } from 'node:os'; + +function linuxIdentity(pid) { + try { + const stat = readFileSync(`/proc/${pid}/stat`, 'utf8'); + const fields = stat.slice(stat.lastIndexOf(')') + 2).trim().split(/\s+/); + return { start: fields[19], state: fields[0], + boot: readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim() }; + } catch { return null; } +} + +export function checkRunnerIdentity() { + return { pid: process.pid, host: hostname(), linux: linuxIdentity(process.pid) }; +} + +// Runner identity is not proof that its descendants have stopped. Recovery +// separately requires explicit owner evidence covering all executors. +export function observeCheckRunner(runner) { + if (!runner || runner.host !== hostname() || !Number.isInteger(runner.pid) || runner.pid < 1) { + return { state: 'unknown' }; + } + const current = linuxIdentity(runner.pid); + if (runner.linux && current) { + if (runner.linux.boot !== current.boot || runner.linux.start !== current.start) return { state: 'stopped' }; + return { state: ['Z', 'X'].includes(current.state) ? 'stopped' : 'live' }; + } + try { process.kill(runner.pid, 0); return { state: 'live' }; } + catch (error) { return { state: error.code === 'ESRCH' ? 'stopped' : 'unknown' }; } +} diff --git a/src/checks/service.mjs b/src/checks/service.mjs index 229b0fc..0b073e1 100644 --- a/src/checks/service.mjs +++ b/src/checks/service.mjs @@ -1,11 +1,14 @@ import { createHash, randomUUID } from 'node:crypto'; import { spawnSync } from 'node:child_process'; -import { mkdirSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { TorchError } from '../kernel/errors.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; import { loadFleetDefinition } from '../kernel/worktrees.mjs'; import { beginWorktreeGuard, endWorktreeGuard } from '../convergence/service.mjs'; +import { captureCheckSnapshot, disposeCheckSnapshot, verifyCheckSnapshot } from './snapshot.mjs'; +import { executeConditionedCheck, validConditionEvidence } from './conditions.mjs'; +import { checkRunnerIdentity, observeCheckRunner } from './runner.mjs'; function git(root, args) { const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); @@ -70,14 +73,42 @@ export class CheckService { ); CREATE INDEX IF NOT EXISTS check_receipts_commit ON check_receipts(commit_sha, check_id, result); + CREATE TABLE IF NOT EXISTS prepared_checks ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + area_id TEXT NOT NULL, + check_id TEXT NOT NULL, + definition_json TEXT NOT NULL, + snapshot_json TEXT NOT NULL, + state TEXT NOT NULL, + created_at TEXT NOT NULL, + receipt_id TEXT + ); `); + const columns = this.controlPlane.database.prepare('PRAGMA table_info(check_receipts)').all(); + if (!columns.some((column) => column.name === 'conditions_json')) { + this.controlPlane.database.exec('ALTER TABLE check_receipts ADD COLUMN conditions_json TEXT'); + } + const preparedColumns = this.controlPlane.database.prepare('PRAGMA table_info(prepared_checks)').all(); + for (const column of ['runner_json', 'recovery_json']) { + if (!preparedColumns.some((record) => record.name === column)) { + this.controlPlane.database.exec(`ALTER TABLE prepared_checks ADD COLUMN ${column} TEXT`); + } + } } listChecks() { + this.refreshDefinitions(); return [...this.definitions.values()]; } + refreshDefinitions() { + const { config } = loadFleetDefinition(this.repositoryRoot); + this.definitions = new Map((config.checks ?? []).map((check) => [check.id, check])); + } + plan({ checkId, areaId } = {}) { + this.refreshDefinitions(); const area = this.controlPlane.assertIdentity(areaId); const definition = this.definitions.get(checkId); if (!definition) { @@ -108,6 +139,10 @@ export class CheckService { code: 'CHECK_BLOCKED', details: plan.blockers, }); } + if (plan.definition.snapshot) { + const prepared = this.prepare({ checkId, areaId }); + return this.runPrepared({ preparedId: prepared.id, areaId }); + } const receiptId = this.idFactory(); const startedAt = this.clock().toISOString(); beginWorktreeGuard(this.controlPlane, { @@ -115,10 +150,13 @@ export class CheckService { clock: this.clock, idFactory: this.idFactory, }); let result; + let execution; try { - result = this.executor(plan.definition.command, plan.definition.args ?? [], { - cwd: plan.worktree, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + execution = executeConditionedCheck({ + definition: plan.definition, subject: { commit: plan.commit }, cwd: plan.worktree, + executor: this.executor, clock: this.clock, }); + result = execution.result; } finally { endWorktreeGuard(this.controlPlane, { areaId: plan.areaId, type: 'check', clock: this.clock, missingOk: true, @@ -127,8 +165,8 @@ export class CheckService { const finishedAt = this.clock().toISOString(); const afterCommit = git(plan.worktree, ['rev-parse', 'HEAD']); const afterDirty = git(plan.worktree, ['status', '--porcelain']).split('\n').filter(Boolean); - let status = result.status === 0 ? 'pass' : 'fail'; - let invalidReason = null; + let status = execution.invalidReason ? 'incomplete' : result.status === 0 ? 'pass' : 'fail'; + let invalidReason = execution.invalidReason; if (afterCommit !== plan.commit) { status = 'incomplete'; invalidReason = 'commit-changed-during-check'; @@ -141,23 +179,26 @@ export class CheckService { const artifactPath = join(artifactDirectory, 'output.json'); writeFileSync(artifactPath, `${JSON.stringify({ stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr), + conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted, }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); const receipt = { id: receiptId, projectId: this.controlPlane.projectId, checkId, areaId: plan.areaId, definition: plan.definition, worktree: plan.worktree, commit: plan.commit, startedAt, finishedAt, exitStatus: result.status ?? null, result: status, artifactPath, invalidReason, + conditions: execution.conditions, }; this.controlPlane.database.prepare(` INSERT INTO check_receipts ( id, project_id, check_id, definition, definition_hash, area_id, worktree, commit_sha, - started_at, finished_at, exit_status, result, artifact_path, invalid_reason - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + started_at, finished_at, exit_status, result, artifact_path, invalid_reason, conditions_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( receipt.id, receipt.projectId, receipt.checkId, JSON.stringify(receipt.definition), definitionHash(receipt.definition), receipt.areaId, receipt.worktree, receipt.commit, receipt.startedAt, receipt.finishedAt, receipt.exitStatus, receipt.result, receipt.artifactPath, receipt.invalidReason, + execution.conditions ? JSON.stringify(execution.conditions) : null, ); this.controlPlane.audit({ actorId: plan.areaId, operation: 'check.run', entityType: 'check-receipt', entityId: receipt.id, @@ -166,6 +207,210 @@ export class CheckService { return receipt; } + prepared({ areaId } = {}) { + const area = areaId ? this.controlPlane.assertIdentity(areaId) : null; + return this.controlPlane.database.prepare(`SELECT * FROM prepared_checks + WHERE project_id = ? ${area ? 'AND area_id = ?' : ''} ORDER BY created_at, id`) + .all(this.controlPlane.projectId, ...(area ? [area] : [])).map((row) => ({ + id: row.id, areaId: row.area_id, checkId: row.check_id, state: row.state, + definition: JSON.parse(row.definition_json), snapshot: JSON.parse(row.snapshot_json), + createdAt: row.created_at, receiptId: row.receipt_id, + runner: row.runner_json ? JSON.parse(row.runner_json) : null, + recovery: row.recovery_json ? JSON.parse(row.recovery_json) : null, + })); + } + + prepare({ checkId, areaId } = {}) { + const plan = this.plan({ checkId, areaId }); + const blockers = plan.blockers.filter((blocker) => blocker.code !== 'RESOURCE_LEASE_REQUIRED'); + if (blockers.length) throw new TorchError('Check inputs cannot be prepared', { + code: 'CHECK_BLOCKED', details: blockers, + }); + const snapshot = captureCheckSnapshot({ + worktree: plan.worktree, stateRoot: this.stateRoot, definition: plan.definition, + commit: plan.commit, id: this.idFactory(), + }); + try { + if (git(plan.worktree, ['rev-parse', 'HEAD']) !== plan.commit + || git(plan.worktree, ['status', '--porcelain'])) { + throw new TorchError('Source changed while preparing check inputs', { code: 'CHECK_BLOCKED' }); + } + const createdAt = this.clock().toISOString(); + this.controlPlane.database.prepare(`INSERT INTO prepared_checks + (id, project_id, area_id, check_id, definition_json, snapshot_json, state, created_at) + VALUES (?, ?, ?, ?, ?, ?, 'prepared', ?)`) + .run(snapshot.id, this.controlPlane.projectId, plan.areaId, checkId, + JSON.stringify(plan.definition), JSON.stringify(snapshot), createdAt); + this.controlPlane.audit({ + actorId: plan.areaId, operation: 'check.prepare', entityType: 'prepared-check', entityId: snapshot.id, + details: { checkId, commit: plan.commit, inputDigest: snapshot.digest }, + }); + return this.prepared({ areaId: plan.areaId }).find((item) => item.id === snapshot.id); + } catch (error) { + // If persistence succeeded, retain inputs as durable recovery evidence. + if (!this.prepared({ areaId: plan.areaId }).some((item) => item.id === snapshot.id)) { + disposeCheckSnapshot(snapshot, { stateRoot: this.stateRoot }); + } + throw error; + } + } + + runPrepared({ preparedId, areaId } = {}) { + this.refreshDefinitions(); + const area = this.controlPlane.assertIdentity(areaId); + const item = this.prepared({ areaId: area }).find((candidate) => candidate.id === preparedId); + if (!item || item.state !== 'prepared') throw new TorchError('Prepared check is unavailable for this identity', { + code: 'PREPARED_CHECK_UNAVAILABLE', + }); + const currentDefinition = this.definitions.get(item.checkId); + if (!currentDefinition || definitionHash(currentDefinition) !== definitionHash(item.definition)) { + throw new TorchError('Prepared check policy changed; prepare new inputs', { code: 'CHECK_POLICY_CHANGED' }); + } + const missing = (item.definition.resources ?? []).filter((resourceId) => + !this.resourceService?.hasActiveLease({ resourceId, areaId: area })); + if (missing.length) throw new TorchError('Prepared check requires resource leases', { + code: 'CHECK_BLOCKED', details: missing.map((resourceId) => ({ code: 'RESOURCE_LEASE_REQUIRED', resourceId })), + }); + verifyCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot }); + const claimed = this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'running', runner_json = ? + WHERE id = ? AND project_id = ? AND area_id = ? AND state = 'prepared'`) + .run(JSON.stringify(checkRunnerIdentity()), item.id, this.controlPlane.projectId, area); + if (claimed.changes !== 1) throw new TorchError('Prepared check was claimed concurrently', { + code: 'PREPARED_CHECK_UNAVAILABLE', + }); + const startedAt = this.clock().toISOString(); + const execution = executeConditionedCheck({ + definition: item.definition, + subject: { commit: item.snapshot.commit, inputDigest: item.snapshot.digest }, + cwd: item.snapshot.inputRoot, executor: this.executor, clock: this.clock, + }); + const result = execution.result; + let invalidReason = execution.invalidReason; + try { + verifyCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot }); + } catch { + invalidReason = 'snapshot-changed-during-check'; + } + if ((item.definition.resources ?? []).some((resourceId) => + !this.resourceService?.hasActiveLease({ resourceId, areaId: area }))) { + invalidReason = 'resource-lease-lost-during-check'; + } + const finishedAt = this.clock().toISOString(); + const receiptId = this.idFactory(); + const status = invalidReason ? 'incomplete' : result.status === 0 ? 'pass' : 'fail'; + const artifactDirectory = join(this.stateRoot, 'checks', receiptId); + mkdirSync(artifactDirectory, { recursive: false }); + const artifactPath = join(artifactDirectory, 'output.json'); + const snapshotEvidence = { + preparedId: item.id, inputDigest: item.snapshot.digest, commit: item.snapshot.commit, + files: item.snapshot.files, provenance: item.snapshot.provenance, + copyStrategy: item.snapshot.copyStrategy, + }; + writeFileSync(artifactPath, `${JSON.stringify({ + stdout: safeOutput(result.stdout), stderr: safeOutput(result.stderr), snapshot: snapshotEvidence, + conditions: execution.conditions, measurementsExecuted: execution.measurementsExecuted, + }, null, 2)}\n`, { mode: 0o600, flag: 'wx' }); + const receipt = { + id: receiptId, projectId: this.controlPlane.projectId, checkId: item.checkId, areaId: area, + definition: item.definition, worktree: item.snapshot.sourceWorktree, commit: item.snapshot.commit, + startedAt, finishedAt, exitStatus: result.status ?? null, result: status, + artifactPath, invalidReason, snapshot: snapshotEvidence, + conditions: execution.conditions, + }; + this.controlPlane.database.prepare(`INSERT INTO check_receipts + (id, project_id, check_id, definition, definition_hash, area_id, worktree, commit_sha, + started_at, finished_at, exit_status, result, artifact_path, invalid_reason, conditions_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + .run(receipt.id, receipt.projectId, receipt.checkId, JSON.stringify(receipt.definition), + definitionHash(receipt.definition), area, receipt.worktree, receipt.commit, + startedAt, finishedAt, receipt.exitStatus, status, artifactPath, invalidReason, + execution.conditions ? JSON.stringify(execution.conditions) : null); + this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'finished', receipt_id = ? WHERE id = ?`) + .run(receiptId, item.id); + this.controlPlane.audit({ + actorId: area, operation: 'check.run-prepared', entityType: 'check-receipt', entityId: receiptId, + details: { preparedId: item.id, checkId: item.checkId, commit: receipt.commit, result: status }, + }); + disposeCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot }); + return receipt; + } + + planPreparedRecovery({ preparedId, actorId } = {}) { + this.controlPlane.assertOwnerActor(actorId); + const item = this.prepared().find((candidate) => candidate.id === preparedId); + if (!item || !['running', 'abandoned'].includes(item.state)) { + throw new TorchError('Only interrupted or already abandoned checks can be recovered', { code: 'PREPARED_CHECK_RECOVERY_UNAVAILABLE' }); + } + const runner = observeCheckRunner(item.runner); + return { schema: 'torch.dev/prepared-check-recovery/v1alpha1', preparedId, state: item.state, + areaId: item.areaId, checkId: item.checkId, commit: item.snapshot.commit, + inputDigest: item.snapshot.digest, runner, cleanupPending: existsSync(item.snapshot.root), + requiresStoppedExecutorEvidence: true, canRecover: runner.state !== 'live', + receiptCreated: false, resourceReleasePerformed: false, mutationPerformed: false }; + } + + recoverPrepared({ preparedId, actorId, approved = false, executorsStopped = false, evidence } = {}) { + if (approved !== true) throw new TorchError('Recovery requires explicit owner confirmation', { code: 'APPROVAL_REQUIRED' }); + if (executorsStopped !== true || typeof evidence !== 'string' || !evidence.trim() || evidence.length > 4000) { + throw new TorchError('Confirm all check executors and descendants stopped with evidence', { code: 'PREPARED_CHECK_STOP_EVIDENCE_REQUIRED' }); + } + const database = this.controlPlane.database; + database.exec('BEGIN IMMEDIATE'); + let item; + let recovery; + try { + const plan = this.planPreparedRecovery({ preparedId, actorId }); + if (!plan.canRecover) throw new TorchError('Recorded check runner is still live', { code: 'PREPARED_CHECK_RUNNER_LIVE' }); + item = this.prepared().find((candidate) => candidate.id === preparedId); + if (item.state === 'running') { + recovery = { at: this.clock().toISOString(), actorId, evidence: evidence.trim(), + executorsStopped: true, observedRunner: plan.runner, receiptCreated: false, + cleanupPending: true, cleanupReason: null }; + database.prepare("UPDATE prepared_checks SET state = 'abandoned', recovery_json = ? WHERE id = ? AND project_id = ? AND state = 'running'") + .run(JSON.stringify(recovery), preparedId, this.controlPlane.projectId); + this.controlPlane.auditOwnerAction({ actorId, operation: 'check.recover-prepared', + entityType: 'prepared-check', entityId: preparedId, + details: { ...recovery, commit: item.snapshot.commit, inputDigest: item.snapshot.digest } }); + } else recovery = item.recovery; + database.exec('COMMIT'); + } catch (error) { database.exec('ROLLBACK'); throw error; } + // The terminal audit is durable before cleanup. A cleanup error leaves only + // this exact owned snapshot pending; explicit repeat recovery can retry it. + let cleanupPending = existsSync(item.snapshot.root); + let cleanupReason = null; + if (cleanupPending) { + try { disposeCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot }); cleanupPending = false; } + catch (error) { cleanupReason = error.code ?? 'CHECK_SNAPSHOT_CLEANUP_FAILED'; } + } + const cleanupChanged = recovery?.cleanupPending !== cleanupPending || recovery?.cleanupReason !== cleanupReason; + if (cleanupChanged) { + recovery = { ...recovery, cleanupPending, cleanupReason }; + database.prepare("UPDATE prepared_checks SET recovery_json = ? WHERE id = ? AND project_id = ? AND state = 'abandoned'") + .run(JSON.stringify(recovery), preparedId, this.controlPlane.projectId); + } + return { preparedId, state: 'abandoned', recovery, cleanupPending, cleanupReason, + receiptCreated: false, resourceReleasePerformed: false, executionPerformed: false, + mutationPerformed: item.state === 'running' || cleanupChanged }; + } + + cancelPrepared({ preparedId, areaId } = {}) { + const area = this.controlPlane.assertIdentity(areaId); + const item = this.prepared({ areaId: area }).find((candidate) => candidate.id === preparedId); + if (!item || item.state !== 'prepared') throw new TorchError('Only unstarted owned checks can be cancelled', { + code: 'PREPARED_CHECK_UNAVAILABLE', + }); + const changed = this.controlPlane.database.prepare(`UPDATE prepared_checks SET state = 'cancelled' + WHERE id = ? AND project_id = ? AND area_id = ? AND state = 'prepared'`) + .run(item.id, this.controlPlane.projectId, area); + if (changed.changes !== 1) throw new TorchError('Prepared check was claimed concurrently', { code: 'PREPARED_CHECK_UNAVAILABLE' }); + disposeCheckSnapshot(item.snapshot, { stateRoot: this.stateRoot }); + this.controlPlane.audit({ + actorId: area, operation: 'check.cancel-prepared', entityType: 'prepared-check', entityId: item.id, + details: { checkId: item.checkId, commit: item.snapshot.commit }, + }); + return { id: item.id, state: 'cancelled', removed: true }; + } + receipts({ commit, checkId, areaId } = {}) { const clauses = []; const values = []; @@ -182,15 +427,20 @@ export class CheckService { commit: row.commit_sha, startedAt: row.started_at, finishedAt: row.finished_at, exitStatus: row.exit_status, result: row.result, artifactPath: row.artifact_path, invalidReason: row.invalid_reason, + conditions: row.conditions_json ? JSON.parse(row.conditions_json) : null, })); } exactPasses({ commit, requiredChecks }) { + this.refreshDefinitions(); const receipts = this.receipts({ commit }); return requiredChecks.every((checkId) => { const definition = this.definitions.get(checkId); return Boolean(definition) && receipts.some((receipt) => receipt.checkId === checkId && receipt.result === 'pass' + && validConditionEvidence({ definition, conditions: receipt.conditions, commit }) + && (!definition.snapshot || this.controlPlane.database.prepare(`SELECT id FROM prepared_checks + WHERE project_id = ? AND receipt_id = ? AND state = 'finished'`).get(this.controlPlane.projectId, receipt.id)) && receipt.definitionHash === definitionHash(definition)); }); } diff --git a/src/checks/snapshot.mjs b/src/checks/snapshot.mjs new file mode 100644 index 0000000..1e1ec90 --- /dev/null +++ b/src/checks/snapshot.mjs @@ -0,0 +1,144 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { + constants, copyFileSync, chmodSync, existsSync, lstatSync, mkdirSync, + readFileSync, readdirSync, realpathSync, rmSync, writeFileSync, +} from 'node:fs'; +import { isAbsolute, join, relative, resolve } from 'node:path'; +import { TorchError } from '../kernel/errors.mjs'; + +function fail(message, details) { + throw new TorchError(message, { code: 'CHECK_SNAPSHOT_INVALID', details }); +} + +function safeRelative(value) { + if (typeof value !== 'string' || !value || isAbsolute(value) + || value.includes('\\') || value.split('/').some((part) => !part || part === '.' || part === '..') + || value.split('/').includes('.git')) fail('Snapshot paths must be explicit repository-relative paths', { path: value }); + return value; +} + +function inspectPath(root, path) { + let current = root; + for (const part of safeRelative(path).split('/')) { + current = join(current, part); + if (!existsSync(current) || lstatSync(current).isSymbolicLink()) { + fail('Snapshot inputs cannot be missing or symlinked', { path }); + } + } + return current; +} + +function inventory(root, paths) { + const records = new Map(); + function visit(path) { + const absolute = inspectPath(root, path); + const stat = lstatSync(absolute); + if (stat.isDirectory()) { + for (const name of readdirSync(absolute).sort()) visit(`${path}/${name}`); + } else if (stat.isFile()) { + const bytes = readFileSync(absolute); + records.set(path, { + path, bytes: bytes.length, executable: Boolean(stat.mode & 0o111), + sha256: createHash('sha256').update(bytes).digest('hex'), + }); + } else fail('Snapshot inputs must be regular files or directories', { path }); + } + for (const path of paths) visit(path); + return [...records.values()].sort((left, right) => left.path.localeCompare(right.path)); +} + +function hash(records) { + return createHash('sha256').update(JSON.stringify(records)).digest('hex'); +} + +function snapshotParent(stateRoot) { + const root = realpathSync(stateRoot); + const parent = join(root, 'check-inputs'); + if (existsSync(parent) && (lstatSync(parent).isSymbolicLink() || !lstatSync(parent).isDirectory())) { + fail('Snapshot state directory is not an owned regular directory'); + } + mkdirSync(parent, { recursive: true, mode: 0o700 }); + return parent; +} + +function ownedSnapshot(snapshot, stateRoot) { + if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(snapshot?.id ?? '')) fail('Invalid snapshot identity'); + const parent = join(realpathSync(stateRoot), 'check-inputs'); + const root = join(parent, snapshot.id); + if (resolve(snapshot.root ?? '') !== root || !existsSync(root) + || lstatSync(parent).isSymbolicLink() || lstatSync(root).isSymbolicLink()) fail('Snapshot ownership does not match'); + const marker = JSON.parse(readFileSync(join(root, 'snapshot.json'), 'utf8')); + if (JSON.stringify(marker) !== JSON.stringify(snapshot)) { + fail('Snapshot ownership receipt was changed'); + } + return root; +} + +/** Capture only explicitly approved inputs; ignored build output is supported. + * The project marker is provenance evidence, not independent build attestation. + * COPYFILE_FICLONE never hardlinks; unsupported filesystems fall back to copies. + */ +export function captureCheckSnapshot({ worktree, stateRoot, definition, commit, id = randomUUID() } = {}) { + const policy = definition?.snapshot; + if (!policy || !Array.isArray(policy.paths) || !policy.paths.length + || !/^[0-9a-f]{40,64}$/.test(commit ?? '') || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) { + fail('Snapshot capture requires an approved input policy, commit and identity'); + } + const source = realpathSync(worktree); + const parent = snapshotParent(stateRoot); + if (!relative(source, parent).startsWith('..') || source === parent) { + fail('Snapshot state must be outside the source worktree'); + } + const paths = [...new Set(policy.paths.map(safeRelative))].sort(); + const markerPath = safeRelative(policy.source_commit_file); + const marker = readFileSync(inspectPath(source, markerPath), 'utf8').trim(); + if (marker !== commit) throw new TorchError('Build marker does not match the queued source commit', { + code: 'CHECK_SNAPSHOT_STALE_BUILD', details: { expected: commit, observed: marker }, + }); + const before = inventory(source, paths); + if (!before.some((record) => record.path === markerPath)) fail('The build commit marker must be captured with the inputs'); + if (!before.length) fail('Snapshot has no input files'); + const root = join(parent, id); + mkdirSync(root, { mode: 0o700 }); + const inputRoot = join(root, 'inputs'); + try { + mkdirSync(inputRoot, { mode: 0o700 }); + for (const record of before) { + const target = join(inputRoot, record.path); + mkdirSync(resolve(target, '..'), { recursive: true, mode: 0o700 }); + copyFileSync(inspectPath(source, record.path), target, constants.COPYFILE_FICLONE | constants.COPYFILE_EXCL); + chmodSync(target, record.executable ? 0o500 : 0o400); + } + const frozen = inventory(inputRoot, paths); + const after = inventory(source, paths); + if (hash(before) !== hash(after) || hash(before) !== hash(frozen)) { + fail('Inputs changed during snapshot capture'); + } + const snapshot = { + id, nonce: randomUUID(), root, inputRoot, sourceWorktree: source, commit, + paths, sourceCommitFile: markerPath, digest: hash(frozen), files: frozen, + copyStrategy: 'reflink-or-independent-copy', provenance: 'project-declared-commit-marker', + }; + writeFileSync(join(root, 'snapshot.json'), `${JSON.stringify(snapshot, null, 2)}\n`, { + flag: 'wx', mode: 0o600, + }); + return snapshot; + } catch (error) { + // Only the exact directory successfully created by this invocation is removed. + rmSync(root, { recursive: true }); + throw error; + } +} + +export function verifyCheckSnapshot(snapshot, { stateRoot } = {}) { + const root = ownedSnapshot(snapshot, stateRoot); + const records = inventory(join(root, 'inputs'), snapshot.paths); + if (hash(records) !== snapshot.digest) fail('Frozen check inputs were modified'); + return { verified: true, commit: snapshot.commit, digest: snapshot.digest }; +} + +export function disposeCheckSnapshot(snapshot, { stateRoot } = {}) { + const root = ownedSnapshot(snapshot, stateRoot); + rmSync(root, { recursive: true }); + return { removed: true, id: snapshot.id }; +} diff --git a/src/cli.mjs b/src/cli.mjs index 2d6ee2a..c7b39f5 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -1,13 +1,26 @@ import { analyzeRepository } from './kernel/analyze.mjs'; import { BacklogService } from './backlog/service.mjs'; +import { ArtifactService } from './artifacts/service.mjs'; import { createClaudeAdapter } from './adapters/claude.mjs'; -import { createCodexAdapter } from './adapters/codex.mjs'; +import { createRuntimeAdapterRegistry } from './adapters/registry.mjs'; +import { profileCapabilityIssues, resolveLaunchPolicy } from './adapters/runtime.mjs'; +import { + inspectRuntimePluginTrust, planRuntimePluginRevoke, planRuntimePluginTrust, + revokeRuntimePlugin, runtimePluginTrustPath, trustRuntimePlugin, +} from './adapters/plugins.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 { fetchCanonicalObjects, planCanonicalFetch } from './canonical/fetch.mjs'; import { spawnSync } from 'node:child_process'; +import { randomUUID } from 'node:crypto'; +import { + mkdirSync, mkdtempSync, renameSync, rmSync, statSync, writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; import { diagnoseProject } from './kernel/doctor.mjs'; +import { loadProjectConfig, validateProjectConfig } from './kernel/config.mjs'; import { asErrorRecord, TorchError } from './kernel/errors.mjs'; import { inspectRepository } from './kernel/git.mjs'; import { inspectSpecifications } from './kernel/specifications.mjs'; @@ -16,8 +29,8 @@ import { IntegrationService } from './integration/service.mjs'; import { analyzeCombatrigFleet } from './importers/combatrig.mjs'; import { proposeDomains } from './kernel/domains.mjs'; import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { writeNewFile } from './kernel/files.mjs'; +import { join, resolve } from 'node:path'; +import { fileHash, writeNewFile } from './kernel/files.mjs'; import { createWorktrees, planWorktrees } from './kernel/worktrees.mjs'; import { captureFleet, createFleetBrief, detachFleet, planAreaUp, planFleetDetach, @@ -26,6 +39,7 @@ import { import { ResourceService } from './resources/service.mjs'; import { CANDIDATE_ACCEPTANCE_SCENARIOS, createVersionService } from './self-host/service.mjs'; import { observeProject } from './observability/snapshot.mjs'; +import { OwnerDigestService } from './observability/owner-digest.mjs'; import { ContextTelemetryService } from './telemetry/context.mjs'; import { ScheduleService } from './schedules/service.mjs'; import { assertLauncherDigest, ScheduleLauncherService } from './schedules/launcher.mjs'; @@ -34,9 +48,11 @@ import { createArchitectRunPlan, loadArchitectArtifact, runSessionArchitect, validateArchitectProposal, } from './design/architect.mjs'; import { FleetEvolutionService } from './evolution/service.mjs'; +import { HierarchyEvolutionService } from './evolution/hierarchy.mjs'; import { ConvergenceService } from './convergence/service.mjs'; import { attachForge, detachForge, forgeStatus, planForgeAttach, planForgeDetach, + planForgeSync, syncForgeRemote, } from './forge/service.mjs'; import { configureDelivery, DeliveryService, planDeliveryConfiguration, @@ -53,7 +69,7 @@ Usage: torch architect validate --brief --response [--json] torch architect run --brief --provider --output [--model ] [--max-budget-usd ] --yes [--json] torch domains [--repo ] [--spec ] [--output ] [--json] - torch install --proposal [--runtime ] [--dry-run] [--yes] [--json] + torch install --proposal [--runtime ] [--default-runtime ] [--dry-run] [--yes] [--json] torch worktrees [--parent ] [--dry-run] [--yes] [--json] torch up [--fresh] [--only ] [--dry-run] [--yes] [--json] torch down [--dry-run] [--yes] [--json] @@ -71,20 +87,41 @@ Usage: torch inbox --area [--unacknowledged] [--limit ] [--json] torch ack --area --message [--json] torch status --area --state [--summary ] [--runtime ] [--session ] [--task ] [--json] + torch profile show --area [--json] + torch profile set --area [--runtime ] [--model |--inherit-model] [--reasoning ] [--launch-policy ...] [--reset|--reset-launch-policy] [--dry-run|--yes] [--json] + torch profile defaults --runtime [--model ] [--reasoning ] [--launch-policy ...] [--reset|--reset-launch-policy] --yes [--json] + torch runtimes list [--json] + torch runtimes trust --name --module [--yes --sha256 ] [--json] + torch runtimes revoke --name [--yes --sha256 ] [--json] torch complete --area --summary [--task ] [--evidence ] [--commit ] [--json] torch blocked --area --summary [--task ] [--evidence ] [--path ] [--commit ] [--json] torch coordinate --from --body [--with ] [--task ] [--path ] [--json] torch handoff --from [--to ] (--path | --task ) --reason [--json] + torch approvals list [--area ] [--status ] [--json] + torch approvals decide --id --revision --by owner --decision [--note ] --yes [--json] torch backlog list [--state ] [--owner ] [--json] torch backlog next --area [--json] + torch backlog claim-next --area [--dry-run|--yes] [--json] torch backlog health [--stale-days ] [--stale-observed-commits ] [--json] + torch backlog activity [--stale-days ] [--max-commits ] [--json] + torch backlog close-landed --integration --area session-manager (--dry-run | --yes) [--json] torch backlog get --task [--json] torch backlog create --area --title --description --accept [--priority ] [--domains ] [--depends ] [--observed-at ] [--json] + torch backlog classify --task --area session-manager --revision [--feature ] [--milestone ] [--clear-feature] [--clear-milestone] --reason [--json] torch backlog transition --task --area --state --revision [--owner ] [--evidence ] [--commit ] [--integration ] [--reason ] [--note ] [--json] + torch artifacts list [--json] + torch artifacts publish --area --task --title --alt --file --commit [--json] + torch artifacts comment --artifact --body --by owner [--yes] [--json] torch checks list [--json] torch checks receipts [--commit ] [--id ] [--area ] [--json] torch checks plan --id --area [--json] torch checks run --id --area --yes [--json] + torch checks prepare --id --area --yes [--json] + torch checks prepared [--area ] [--json] + torch checks run-prepared --prepared --area --yes [--json] + torch checks cancel-prepared --prepared --area --yes [--json] + torch checks recovery-plan --prepared [--json] + torch checks recover-prepared --prepared --executors-stopped --evidence --yes [--json] torch resources list [--json] torch resources status --id [--json] torch resources acquire --id --area [--json] @@ -101,14 +138,27 @@ Usage: torch integrate authorize --request --area [--json] torch integrate plan --request --area [--json] torch integrate land --request --area --yes [--json] + torch integrate drain --area --yes [--limit ] [--json] torch canonical plan [--json] torch canonical create --yes [--json] torch recoverability [--commit ] [--json] torch forge plan --remote [--provider ] [--json] torch forge attach --remote [--provider ] --yes [--json] torch forge status [--json] + torch forge sync plan [--json] + torch forge sync --yes [--json] + torch forge fetch plan [--json] + torch forge fetch status [--json] + torch forge fetch --yes [--attempts 3] [--json] torch forge detach [--dry-run] --yes [--json] torch delivery list [--state ] [--json] + torch digest build [--json] + torch digest latest [--json] + torch digest publish --by owner --yes [--json] + torch delivery attempts [--id ] [--json] + torch delivery operations [--id ] [--json] + torch delivery recover-not-applied --operation --by owner --runtime-stopped --evidence --yes [--json] + torch delivery recover-succeeded --operation --by owner (--dry-run | --runtime-stopped --evidence --yes) [--json] torch delivery get --delivery [--json] torch delivery create --area [--commit ] --label --evidence [--json] torch delivery plan --delivery --to --by [--json] @@ -127,9 +177,11 @@ Usage: torch context record --area --source --measurement [usage fields] [--json] torch schedules list [--actor ] [--json] torch schedules runs [--id ] [--json] + torch schedules wakes [--manager ] [--json] + torch schedules recover-wake --reservation --runtime-stopped --note --yes [--json] torch schedules plan --id --actor [--json] torch schedules run --id --actor [--yes] [--json] - torch schedules launcher [--yes] [--json] + torch schedules launcher [--yes] [--json] torch fleet changes [--state ] [--json] torch fleet get --change [--json] torch fleet assess [--from session-manager] [--json] @@ -142,6 +194,17 @@ Usage: torch fleet plan --change [--json] torch fleet activate --change --by --yes [--json] torch fleet start --change [--fresh] [--dry-run] --yes [--json] + torch fleet hierarchy-changes [--state ] [--json] + torch fleet hierarchy-assess [--from session-manager] [--json] + torch fleet hierarchy-get --proposal [--json] + torch fleet hierarchy-propose --from session-manager --proposal [--json] + torch fleet hierarchy-decide --proposal --decision --by owner [--reason ] --yes [--json] + torch fleet hierarchy-plan --proposal [--json] + torch fleet hierarchy-activate --proposal --by owner --yes [--json] + torch fleet hierarchy-review-pilot --proposal --review --from session-manager --yes [--json] + torch fleet hierarchy-pilot-reviews --proposal [--json] + torch fleet hierarchy-conclusion-plan --proposal --decision --reason [--json] + torch fleet hierarchy-conclude --proposal --decision --reason --plan-hash --by owner --yes [--json] torch doctor [--json] torch uninstall [--dry-run] [--purge --yes] [--json] `; @@ -167,6 +230,61 @@ function optionValues(argv, name) { return values; } +function launchPolicyOptions(argv) { + const result = {}; + for (const entry of optionValues(argv, '--launch-policy')) { + const match = /^([a-z][A-Za-z0-9]*)=(.+)$/.exec(entry); + if (!match || Object.hasOwn(result, match[1])) { + throw new TorchError(`Invalid or duplicate --launch-policy value: ${entry}`, { + code: 'RUNTIME_PROFILE_INVALID', details: { expected: 'field=value' }, + }); + } + result[match[1]] = match[2]; + } + return result; +} + +function assertEffectiveProfileSupported(config, areaId, registry) { + const profile = effectiveProfile(config, areaId, registry); + const issues = profileCapabilityIssues(registry.get(profile.runtime), profile); + if (issues.length) { + throw new TorchError(`Runtime profile is unsupported by ${profile.runtime}`, { + code: 'RUNTIME_PROFILE_UNSUPPORTED', details: { profile, capabilities: issues }, + }); + } + return profile; +} + +function assertRuntimeDefaultsSupported(config, runtime, registry) { + const adapter = registry.get(runtime); + const settings = config.runtimes[runtime] ?? {}; + const profile = { + model: settings.model ?? adapter?.configuration?.model ?? null, + reasoning: settings.reasoning ?? adapter?.configuration?.reasoning ?? null, + launchPolicy: resolveLaunchPolicy({ + runtime, adapterDefaults: adapter?.configuration, runtimeConfig: settings, + }), + }; + const issues = profileCapabilityIssues(adapter, profile); + if (issues.length) throw new TorchError(`Runtime defaults are unsupported by ${runtime}`, { + code: 'RUNTIME_PROFILE_UNSUPPORTED', details: { profile, capabilities: issues }, + }); +} + +function migrateLegacyRuntimeLaunchPolicy(runtime, settings, adapter) { + if (runtime !== 'codex' || (!Object.hasOwn(settings, 'sandbox') && !Object.hasOwn(settings, 'approval'))) return; + const migrated = resolveLaunchPolicy({ runtime, adapterDefaults: adapter?.configuration, runtimeConfig: settings }); + if (settings.sandbox === 'workspace-write' && settings.approval === 'approve-for-me') { + // If the owner changes approval away from approve-for-me, preserve the + // implicit workspace-write boundary as an explicit sandbox field. + migrated.sandbox = 'workspace-write'; + } + delete settings.sandbox; + delete settings.approval; + if (Object.keys(migrated).length) settings.launchPolicy = migrated; + else delete settings.launchPolicy; +} + function commaList(value) { return (value ?? '').split(',').map((item) => item.trim()).filter(Boolean); } @@ -191,6 +309,38 @@ function loadJsonFile(cwd, inputPath, { label = 'JSON input', code = 'INPUT_INVA } } +function effectiveProfile(config, areaId, runtimeRegistry = createRuntimeAdapterRegistry()) { + const identity = areaId === 'session-manager' + ? config.session_manager : config.domains.find((area) => area.id === areaId); + if (!identity) throw new TorchError(`Unknown Fleet identity: ${areaId}`, { code: 'FLEET_IDENTITY_MISSING' }); + const runtime = identity.runtime ?? config.runtimes.default; + const defaults = config.runtimes[runtime] ?? {}; + const adapterDefaults = runtimeRegistry.configuration(runtime) ?? {}; + const launchPolicy = resolveLaunchPolicy({ runtime, adapterDefaults, runtimeConfig: defaults, identity }); + return { + areaId, runtime, + model: identity.model ?? defaults.model ?? adapterDefaults.model ?? null, + reasoning: identity.reasoning ?? defaults.reasoning ?? adapterDefaults.reasoning ?? null, + launchPolicy, + overrides: { + runtime: identity.runtime !== undefined, + model: identity.model != null, + reasoning: identity.reasoning != null, + launchPolicy: identity.launchPolicy !== undefined, + }, + }; +} + +function persistProfileConfig(repositoryRoot, config) { + validateProjectConfig(config); + const path = resolve(repositoryRoot, '.torch', 'torch.yaml'); + const mode = statSync(path).mode & 0o777; + const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`; + writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode }); + renameSync(temporaryPath, path); + return path; +} + function withControlPlane(repository, env, callback) { const controlPlane = openControlPlane({ repositoryRoot: repository.root, env }); try { @@ -200,22 +350,38 @@ function withControlPlane(repository, env, callback) { } } -function runtimeAdapters() { - return new Map([ - ['claude', createClaudeAdapter()], - ['codex', createCodexAdapter()], - ]); +function configuredRuntimeNames(repositoryRoot, areaIds = undefined) { + const config = loadProjectConfig(repositoryRoot); + const selected = areaIds === undefined ? null : new Set(areaIds); + const identities = [config.session_manager, ...config.domains]; + return [...new Set(identities + .filter((identity) => !selected || selected.has(identity.id)) + .map((identity) => identity.runtime ?? config.runtimes.default))]; } -function runtimeStoppers(spawn) { - return new Map([ - ['claude', createClaudeAdapter({ - runner: (executable, args) => spawn(executable, args, { - encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], - }), - })], - ['codex', createCodexAdapter()], - ]); +function activeRuntimeNames(controlPlane) { + return controlPlane.listAgents().map((identity) => identity.runtime).filter(Boolean); +} + +function proposalRuntimeNames(proposal) { + return [...new Set([ + proposal.session_manager?.runtime, + ...proposal.domains.map((domain) => domain.runtime), + ].filter((runtime) => runtime && runtime !== 'default'))]; +} + +function runtimeAdapters(env = process.env, pluginNames = undefined) { + return createRuntimeAdapterRegistry({ env, loadPlugins: true, pluginNames }).toMap(); +} + +function runtimeStoppers(spawn, env = process.env, pluginNames = undefined) { + const registry = createRuntimeAdapterRegistry({ env, loadPlugins: true, pluginNames }); + registry.replace(createClaudeAdapter({ + runner: (executable, args) => spawn(executable, args, { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + }), + })); + return registry.toMap(); } function stopThrough(adapters, action) { @@ -228,56 +394,149 @@ function stopThrough(adapters, action) { }); } -function runCandidateAcceptance(candidateRoot, spawn) { - const checks = [ - { id: 'test', args: ['test'] }, - { id: 'lint', args: ['run', 'lint'] }, - { id: 'syntax', args: ['run', 'check'] }, - ].map((check) => { - const result = spawn('npm', check.args, { - cwd: candidateRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 300_000, +function acceptanceExcerpt(value, limit = 12_000) { + const output = value?.toString() ?? ''; + return output.length > limit ? `[earlier output truncated]\n${output.slice(-limit)}` : output; +} + +export const CANDIDATE_ACCEPTANCE_EVIDENCE = Object.freeze({ + 'candidate-acceptance-isolation': ['SCN-candidate-acceptance-isolation'], + 'init-analyze': ['SCN-init-read-only'], + 'spec-aware-design': ['SCN-spec-fleet-design', 'SCN-cli-spec-design'], + 'ai-fleet-bootstrap': ['SCN-ai-fleet-bootstrap', 'SCN-bootstrap-specialist-only'], + 'ai-fleet-planning': ['SCN-ai-fleet-planning', 'SCN-architect-organization-assessment', 'SCN-bootstrap-primary-ownership'], + 'fleet-evolution': ['SCN-fleet-evolution', 'SCN-cli-fleet-evolution'], + 'fleet-boundary-evolution': ['SCN-fleet-boundary-evolution'], + 'cli-lifecycle-surface': ['SCN-cli-lifecycle-surface'], + 'forge-migration': ['SCN-forge-migration'], + 'delivery-lifecycle': ['SCN-delivery-lifecycle', 'SCN-delivery-attempts', 'SCN-delivery-retry-boundaries', 'SCN-delivery-succeeded-recovery', + 'SCN-owner-digest', 'SCN-owner-digest-schedule', 'SCN-owner-digest-receipts'], + 'review-install-roster': ['SCN-cli-domain-review', 'SCN-cli-install-provider-default', 'SCN-install-provider-inheritance'], + 'branches-worktrees': ['SCN-worktree-bootstrap'], + 'runtime-identities': ['SCN-mixed-runtime'], + 'runtime-profiles': ['SCN-runtime-profiles', 'SCN-runtime-cost-ceiling', 'SCN-runtime-profile-cli'], + 'console-runtime-profile': ['SCN-console-runtime-profile'], + 'console-schedule-launcher': ['SCN-console-schedule-launcher'], + 'runtime-adapters': [ + 'SCN-pi-adapter', 'SCN-pi-lifecycle-plan', 'SCN-pi-mcp-bridge', + 'SCN-claude-adapter', 'SCN-codex-adapter', + ], + 'runtime-plugin-trust': ['SCN-runtime-plugin-trust', 'SCN-runtime-plugin-lifecycle'], + 'organization-graph': ['SCN-organization-graph'], + 'manager-check-ins': [ + 'SCN-manager-check-in', 'SCN-manager-check-in-refresh', 'SCN-manager-check-in-specialist', 'SCN-manager-check-in-schedule', + 'SCN-manager-check-in-budgeted-wake', 'SCN-manager-check-in-cost-ceiling', 'SCN-manager-check-in-active-runtime', + 'SCN-manager-check-in-count-mode', 'SCN-manager-wake-reservation', + 'SCN-manager-wake-policy', 'SCN-manager-wake-recovery', + 'SCN-manager-schedule-coverage', 'SCN-manager-schedule-growth', + 'SCN-default-manager-checkin-config', 'SCN-manager-check-in-cadence-floor', + ], + 'manager-check-in-console': ['SCN-console-manager-check-ins'], + 'structured-approvals': ['SCN-approval-request'], + 'console-owner-approval': ['SCN-console-owner-approval'], + 'hierarchy-evolution': [ + 'SCN-hierarchy-proposal', 'SCN-hierarchy-pressure-assessment', + 'SCN-hierarchy-reconsideration-after-new-evidence', + 'SCN-console-organization-review', 'SCN-hierarchy-proposal-mcp', + 'SCN-hierarchy-activation', 'SCN-hierarchy-activation-rollback', + 'SCN-hierarchy-pilot-review', 'SCN-hierarchy-pilot-reversal', 'SCN-console-hierarchy-conclusion', 'SCN-console-hierarchy-adoption', + ], + 'hierarchy-cli-review': [ + 'SCN-hierarchy-cli', 'SCN-hierarchy-cli-activation', 'SCN-console-hierarchy-activation', + ], + 'durable-messaging': ['SCN-durable-message'], + 'ownership-query': ['SCN-ownership-handoff'], + 'unsafe-worktree-detection': ['SCN-worktree-purge-safety'], + 'backlog-integration': [ + 'SCN-backlog-lifecycle', 'SCN-native-integration', + 'SCN-backlog-commit-activity', 'SCN-backlog-activity-coverage', 'SCN-commit-closure-intent', + 'SCN-landed-task-closure', 'SCN-landed-closure-recovery', + 'SCN-backlog-self-claim', 'SCN-backlog-self-claim-readiness', + 'SCN-backlog-self-claim-race', 'SCN-cli-backlog-self-claim', + 'SCN-backlog-blocked-resume-serialization', + 'SCN-integration-fifo-drain', 'SCN-cli-integration-drain', + ], + 'forge-sync': ['SCN-forge-sync', 'SCN-canonical-fetch', 'SCN-canonical-fetch-boundaries'], + 'resource-lifecycle': [ + 'SCN-resource-fifo', 'SCN-frozen-check-inputs', 'SCN-frozen-check-rejection', + 'SCN-frozen-resource-check', 'SCN-frozen-check-mutating-executor', + 'SCN-check-conditions-contract', 'SCN-check-conditions-fail-closed', + 'SCN-check-condition-drift', 'SCN-condition-receipts', + ], + 'package-distribution': ['SCN-package-distribution'], + 'capture-stop-resume': [ + 'SCN-fleet-fresh-resume', 'SCN-hierarchy-aware-start-stop', 'SCN-hierarchy-startup-cycle', + ], + 'detach-uninstall': ['SCN-install-doctor-purge'], + 'combatrig-compatibility': ['SCN-combatrig-import', 'SCN-cli-combatrig-import'], + 'product-surface': ['SCN-product-site', 'SCN-dashboard-demo', 'SCN-console-readonly', 'SCN-console-lifecycle-blockers', + 'SCN-console-owner-digest', 'SCN-demo-owner-digest', 'SCN-console-check-evidence', 'SCN-console-task-activity', 'SCN-console-operation-outcomes', 'SCN-console-live-refresh'], + 'artifact-review': ['SCN-artifact-review', 'SCN-artifact-boundaries'], + 'operations-observability': [ + 'SCN-context-locality', 'SCN-schedule-boundaries', 'SCN-cli-schedules', + 'SCN-scheduled-integration-drain', + ], +}); + +export function runCandidateAcceptance(candidateRoot, spawn) { + const acceptanceRoot = mkdtempSync(join(tmpdir(), 'torch-candidate-acceptance-')); + const home = join(acceptanceRoot, 'home'); + const env = { + ...process.env, + HOME: home, + TMPDIR: join(acceptanceRoot, 'tmp'), + XDG_CACHE_HOME: join(home, '.cache'), + XDG_CONFIG_HOME: join(home, '.config'), + XDG_DATA_HOME: join(home, '.local', 'share'), + XDG_STATE_HOME: join(home, '.local', 'state'), + XDG_RUNTIME_DIR: join(acceptanceRoot, 'runtime'), + }; + let checks; + try { + for (const path of [home, env.TMPDIR, env.XDG_CACHE_HOME, env.XDG_CONFIG_HOME, + env.XDG_DATA_HOME, env.XDG_STATE_HOME, env.XDG_RUNTIME_DIR]) { + mkdirSync(path, { recursive: true, mode: 0o700 }); + } + checks = [ + { id: 'test', args: ['test'] }, + { id: 'lint', args: ['run', 'lint'] }, + { id: 'syntax', args: ['run', 'check'] }, + ].map((check) => { + const result = spawn('npm', check.args, { + cwd: candidateRoot, env, encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], timeout: 300_000, + }); + return { + id: check.id, status: result.status ?? (result.error ? 1 : 0), + signal: result.signal ?? null, error: result.error?.message ?? null, + stdout: result.stdout ?? '', stderr: result.stderr ?? '', + }; }); - return { - id: check.id, status: result.status ?? (result.error ? 1 : 0), - signal: result.signal ?? null, error: result.error?.message ?? null, - stdout: result.stdout ?? '', - }; - }); + } finally { + rmSync(acceptanceRoot, { recursive: true, force: true }); + } const testOutput = checks.find((check) => check.id === 'test')?.stdout ?? ''; - const evidenceByScenario = new Map([ - ['init-analyze', ['SCN-init-read-only']], - ['spec-aware-design', ['SCN-spec-fleet-design', 'SCN-cli-spec-design']], - ['ai-fleet-bootstrap', ['SCN-ai-fleet-bootstrap']], - ['ai-fleet-planning', ['SCN-ai-fleet-planning']], - ['fleet-evolution', ['SCN-fleet-evolution', 'SCN-cli-fleet-evolution']], - ['fleet-boundary-evolution', ['SCN-fleet-boundary-evolution']], - ['cli-lifecycle-surface', ['SCN-cli-lifecycle-surface']], - ['forge-migration', ['SCN-forge-migration']], - ['delivery-lifecycle', ['SCN-delivery-lifecycle']], - ['review-install-roster', ['SCN-cli-domain-review']], - ['branches-worktrees', ['SCN-worktree-bootstrap']], - ['runtime-identities', ['SCN-mixed-runtime']], - ['durable-messaging', ['SCN-durable-message']], - ['ownership-query', ['SCN-ownership-handoff']], - ['unsafe-worktree-detection', ['SCN-worktree-purge-safety']], - ['backlog-integration', ['SCN-backlog-lifecycle', 'SCN-native-integration']], - ['resource-lifecycle', ['SCN-resource-fifo']], - ['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']], - ['operations-observability', ['SCN-context-locality', 'SCN-schedule-boundaries', 'SCN-cli-schedules']], - ]); + const observedMarkers = new Set(testOutput.match(/\bSCN-[A-Za-z0-9_-]+\b/g) ?? []); + const evidenceMappingComplete = CANDIDATE_ACCEPTANCE_SCENARIOS.every((scenario) => + Array.isArray(CANDIDATE_ACCEPTANCE_EVIDENCE[scenario]) + && CANDIDATE_ACCEPTANCE_EVIDENCE[scenario].length > 0); const scenarios = CANDIDATE_ACCEPTANCE_SCENARIOS.filter((scenario) => - (evidenceByScenario.get(scenario) ?? []).every((marker) => testOutput.includes(marker))); + CANDIDATE_ACCEPTANCE_EVIDENCE[scenario]?.every((marker) => observedMarkers.has(marker)) === true); const passed = checks.every((check) => check.status === 0 && !check.signal && !check.error) - && scenarios.length === CANDIDATE_ACCEPTANCE_SCENARIOS.length; + && evidenceMappingComplete && scenarios.length === CANDIDATE_ACCEPTANCE_SCENARIOS.length; return { passed, scenarios, - evidence: checks.map(({ stdout, ...check }) => ({ - ...check, outputBytes: Buffer.byteLength(stdout), - })), + evidence: checks.map(({ stdout, stderr, ...check }) => { + const failed = check.status !== 0 || check.signal || check.error; + return { + ...check, + outputBytes: Buffer.byteLength(stdout) + Buffer.byteLength(stderr), + ...(failed ? { + stdoutExcerpt: acceptanceExcerpt(stdout), stderrExcerpt: acceptanceExcerpt(stderr), + } : {}), + }; + }), }; } @@ -291,6 +550,51 @@ export async function runCli(argv = process.argv.slice(2), { process.stdout.write(HELP); return 0; } + if (command === 'runtimes') { + const operation = argv[1] ?? 'list'; + if (operation === 'list') { + const builtIns = createRuntimeAdapterRegistry({ env, loadPlugins: false }).names(); + let trust; + try { trust = inspectRuntimePluginTrust({ env }); } catch (error) { + trust = { + path: runtimePluginTrustPath(env), status: 'invalid', + error: { code: error.code ?? 'RUNTIME_PLUGIN_STORE_INVALID', message: error.message }, + }; + } + print({ + schema: 'torch.dev/runtime-catalog/v1alpha1', builtIns, + trust, mutationPerformed: false, + executesPluginCode: false, + }, { json }); + return 0; + } + if (operation === 'trust') { + const name = optionValue(argv, '--name'); + const modulePath = optionValue(argv, '--module'); + const plan = planRuntimePluginTrust({ name, modulePath, env }); + if (!argv.includes('--yes')) { + print({ ...plan, requiresApproval: true }, { json }); + return 0; + } + print(trustRuntimePlugin({ + name, modulePath, env, approved: true, expectedSha256: optionValue(argv, '--sha256'), + }), { json }); + return 0; + } + if (operation === 'revoke') { + const name = optionValue(argv, '--name'); + const plan = planRuntimePluginRevoke({ name, env }); + if (!argv.includes('--yes') || plan.sha256 === null) { + print({ ...plan, requiresApproval: plan.sha256 !== null }, { json }); + return 0; + } + print(revokeRuntimePlugin({ + name, env, approved: true, expectedSha256: optionValue(argv, '--sha256'), + }), { json }); + return 0; + } + throw new TorchError(`Unknown runtimes operation: ${operation}`, { code: 'UNKNOWN_COMMAND' }); + } if (command === 'candidate') { const operation = argv[1] ?? 'status'; const versions = createVersionService({ env }); @@ -457,6 +761,175 @@ export async function runCli(argv = process.argv.slice(2), { const designCommand = ['init', 'analyze', 'domains', 'design', 'bootstrap'].includes(command); const repositoryRoot = designCommand ? resolve(cwd, optionValue(argv, '--repo') ?? '.') : cwd; const repository = inspectRepository(repositoryRoot); + if (command === 'profile') { + const operation = argv[1] ?? 'show'; + const areaId = optionValue(argv, '--area'); + const config = loadProjectConfig(repository.root); + if (operation === 'defaults') { + const runtime = optionValue(argv, '--runtime'); + if (!argv.includes('--yes')) { + throw new TorchError('Changing project runtime defaults affects future launch plans. Review profile show, then re-run with --yes.', { + code: 'APPROVAL_REQUIRED', + }); + } + const registry = createRuntimeAdapterRegistry({ env, loadPlugins: true, pluginNames: [runtime] }); + if (!runtime || runtime === 'default' || !registry.has(runtime)) { + throw new TorchError(`Runtime adapter is not configured: ${runtime ?? ''}`, { + code: 'RUNTIME_ADAPTER_MISSING', details: { runtime: runtime ?? null }, + }); + } + const model = optionValue(argv, '--model'); + const reasoning = optionValue(argv, '--reasoning'); + const policyOptions = launchPolicyOptions(argv); + const reset = argv.includes('--reset'); + const resetLaunchPolicy = argv.includes('--reset-launch-policy'); + const beforeSettings = structuredClone(config.runtimes[runtime] ?? registry.configuration(runtime) ?? {}); + if (reset && (model !== undefined || reasoning !== undefined || Object.keys(policyOptions).length || resetLaunchPolicy)) { + throw new TorchError('--reset cannot be combined with profile values.', { code: 'RUNTIME_PROFILE_INVALID' }); + } + if (resetLaunchPolicy && (Object.keys(policyOptions).length || reset)) { + throw new TorchError('--reset-launch-policy cannot be combined with launch-policy values or --reset.', { code: 'RUNTIME_PROFILE_INVALID' }); + } + if (!reset && model === undefined && reasoning === undefined && !Object.keys(policyOptions).length && !resetLaunchPolicy) { + throw new TorchError('Set --model, --reasoning, --launch-policy, or use a reset option.', { code: 'RUNTIME_PROFILE_INVALID' }); + } + if (!Object.hasOwn(config.runtimes, runtime)) config.runtimes[runtime] = registry.configuration(runtime) ?? {}; + if (!reset && (Object.keys(policyOptions).length || resetLaunchPolicy)) { + migrateLegacyRuntimeLaunchPolicy(runtime, config.runtimes[runtime], registry.get(runtime)); + } + if (reset) { + delete config.runtimes[runtime].model; + delete config.runtimes[runtime].reasoning; + delete config.runtimes[runtime].launchPolicy; + } else { + if (model !== undefined) config.runtimes[runtime].model = model; + if (reasoning === 'none') delete config.runtimes[runtime].reasoning; + else if (reasoning !== undefined) config.runtimes[runtime].reasoning = reasoning; + if (resetLaunchPolicy) delete config.runtimes[runtime].launchPolicy; + else if (Object.keys(policyOptions).length) { + config.runtimes[runtime].launchPolicy = { + ...(config.runtimes[runtime].launchPolicy ?? {}), ...policyOptions, + }; + } + } + assertRuntimeDefaultsSupported(config, runtime, registry); + const path = persistProfileConfig(repository.root, config); + const profile = { + model: config.runtimes[runtime].model ?? registry.configuration(runtime)?.model ?? null, + reasoning: config.runtimes[runtime].reasoning ?? registry.configuration(runtime)?.reasoning ?? null, + launchPolicy: resolveLaunchPolicy({ + runtime, adapterDefaults: registry.configuration(runtime), runtimeConfig: config.runtimes[runtime], + }), + }; + withControlPlane(repository, env, (controlPlane) => controlPlane.auditOwnerAction({ + actorId: 'owner', operation: 'profile.change', entityType: 'runtime-default', entityId: runtime, + details: { before: beforeSettings, after: config.runtimes[runtime], effectiveProfile: profile, currentHead: repository.head }, + })); + print({ + runtime, profile, + path, mutationPerformed: true, + sessionsStarted: false, existingSessionChanged: false, + }, { json }); + return 0; + } + if (!areaId) throw new TorchError('Runtime profile commands require --area .', { code: 'FLEET_IDENTITY_REQUIRED' }); + const identity = areaId === 'session-manager' + ? config.session_manager : config.domains.find((area) => area.id === areaId); + if (!identity) throw new TorchError(`Unknown Fleet identity: ${areaId}`, { code: 'FLEET_IDENTITY_MISSING' }); + if (operation === 'show') { + print(effectiveProfile(config, areaId, createRuntimeAdapterRegistry({ env })), { json }); + return 0; + } + if (operation !== 'set') throw new TorchError(`Unknown profile operation: ${operation}`, { code: 'UNKNOWN_COMMAND' }); + const dryRun = argv.includes('--dry-run'); + if (!dryRun && !argv.includes('--yes')) { + throw new TorchError('Changing a project runtime profile affects future launch plans. Review profile show, then re-run with --yes.', { + code: 'APPROVAL_REQUIRED', + }); + } + const requestedRuntime = optionValue(argv, '--runtime'); + const requestedModel = optionValue(argv, '--model'); + const inheritModel = argv.includes('--inherit-model'); + const requestedReasoning = optionValue(argv, '--reasoning'); + const policyOptions = launchPolicyOptions(argv); + const reset = argv.includes('--reset'); + const resetLaunchPolicy = argv.includes('--reset-launch-policy'); + const registry = createRuntimeAdapterRegistry({ + env, loadPlugins: true, + pluginNames: [...new Set([identity.runtime ?? config.runtimes.default, requestedRuntime].filter(Boolean))], + }); + if (inheritModel && requestedModel !== undefined) { + throw new TorchError('--inherit-model cannot be combined with --model.', { code: 'RUNTIME_PROFILE_INVALID' }); + } + if (reset && (requestedRuntime !== undefined || requestedModel !== undefined || inheritModel || requestedReasoning !== undefined + || Object.keys(policyOptions).length || resetLaunchPolicy)) { + throw new TorchError('--reset cannot be combined with profile values.', { code: 'RUNTIME_PROFILE_INVALID' }); + } + if (resetLaunchPolicy && (Object.keys(policyOptions).length || reset)) { + throw new TorchError('--reset-launch-policy cannot be combined with launch-policy values or --reset.', { code: 'RUNTIME_PROFILE_INVALID' }); + } + if (!reset && requestedRuntime === undefined && requestedModel === undefined && !inheritModel && requestedReasoning === undefined + && !Object.keys(policyOptions).length && !resetLaunchPolicy) { + throw new TorchError('Set at least one profile value or use a reset option.', { + code: 'RUNTIME_PROFILE_INVALID', + }); + } + const beforeProfile = effectiveProfile(config, areaId, registry); + if (requestedRuntime !== undefined) { + if (!registry.has(requestedRuntime) || requestedRuntime === 'default') { + throw new TorchError(`Runtime adapter is not configured: ${requestedRuntime}`, { + code: 'RUNTIME_ADAPTER_MISSING', details: { runtime: requestedRuntime }, + }); + } + if (!Object.hasOwn(config.runtimes, requestedRuntime)) { + config.runtimes[requestedRuntime] = registry.configuration(requestedRuntime) ?? {}; + } + identity.runtime = requestedRuntime; + } + if (reset) { + delete identity.model; + delete identity.reasoning; + delete identity.launchPolicy; + } else { + if (requestedModel !== undefined) identity.model = requestedModel; + else if (inheritModel) delete identity.model; + if (requestedReasoning === 'none') delete identity.reasoning; + else if (requestedReasoning !== undefined) identity.reasoning = requestedReasoning; + if (resetLaunchPolicy) delete identity.launchPolicy; + else if (Object.keys(policyOptions).length) identity.launchPolicy = { + ...(identity.launchPolicy ?? {}), ...policyOptions, + }; + } + assertEffectiveProfileSupported(config, areaId, registry); + const profile = effectiveProfile(config, areaId, registry); + const configPath = resolve(repository.root, '.torch', 'torch.yaml'); + const configSha256 = fileHash(configPath); + const expectedConfigHash = optionValue(argv, '--expect-config-sha256'); + if (expectedConfigHash && expectedConfigHash !== configSha256) { + throw new TorchError('Runtime configuration changed after review; inspect a fresh profile plan.', { + code: 'RUNTIME_PROFILE_REVIEW_STALE', + }); + } + if (dryRun) { + print({ + action: 'change-runtime-profile', beforeProfile, profile, + configSha256, + effect: 'Updates this identity’s profile for future launch plans only. It does not change or start a running session.', + mutationPerformed: false, sessionsStarted: false, existingSessionChanged: false, + }, { json }); + return 0; + } + const path = persistProfileConfig(repository.root, config); + withControlPlane(repository, env, (controlPlane) => controlPlane.auditOwnerAction({ + actorId: 'owner', operation: 'profile.change', entityType: 'runtime-profile', entityId: areaId, + details: { before: beforeProfile, after: profile, currentHead: repository.head }, + })); + print({ + profile, path, mutationPerformed: true, + sessionsStarted: false, existingSessionChanged: false, + }, { json }); + return 0; + } if (command === 'init' || command === 'analyze') { const specifications = inspectSpecifications(optionValues(argv, '--spec'), { cwd, repositoryRoot: repository.root, @@ -490,10 +963,16 @@ export async function runCli(argv = process.argv.slice(2), { if (command === 'install') { const proposalPath = optionValue(argv, '--proposal'); const runtimeOption = optionValue(argv, '--runtime'); + const defaultRuntime = optionValue(argv, '--default-runtime'); const runtimes = runtimeOption === undefined ? undefined : commaList(runtimeOption); if (argv.includes('--dry-run')) { const proposal = loadProposal(cwd, proposalPath); - print({ ...planInstall({ repository, proposal, env, runtimes }), proposal: resolve(cwd, proposalPath) }, { json }); + print({ ...planInstall({ + repository, proposal, env, runtimes, defaultRuntime, + runtimeRegistry: createRuntimeAdapterRegistry({ + env, loadPlugins: true, pluginNames: [...(runtimes ?? proposalRuntimeNames(proposal)), ...[defaultRuntime].filter(Boolean)], + }), + }), proposal: resolve(cwd, proposalPath) }, { json }); return 0; } if (!argv.includes('--yes')) { @@ -502,7 +981,12 @@ export async function runCli(argv = process.argv.slice(2), { }); } const proposal = loadProposal(cwd, proposalPath); - print(installProject({ repository, proposal, env, runtimes }), { json }); + print(installProject({ + repository, proposal, env, runtimes, defaultRuntime, + runtimeRegistry: createRuntimeAdapterRegistry({ + env, loadPlugins: true, pluginNames: [...(runtimes ?? proposalRuntimeNames(proposal)), ...[defaultRuntime].filter(Boolean)], + }), + }), { json }); return 0; } if (command === 'doctor') { @@ -528,10 +1012,11 @@ export async function runCli(argv = process.argv.slice(2), { if (command === 'up') { const control = openControlPlane({ repositoryRoot: repository.root, env }); try { - const adapters = runtimeAdapters(); + const only = optionValue(argv, '--only') === undefined ? undefined : commaList(optionValue(argv, '--only')); + const adapters = runtimeAdapters(env, configuredRuntimeNames(repository.root, only)); const plan = planFleetUp({ repositoryRoot: repository.root, controlPlane: control, adapters, fresh: argv.includes('--fresh'), - only: optionValue(argv, '--only') === undefined ? undefined : commaList(optionValue(argv, '--only')), + only, }); if (argv.includes('--dry-run')) { print(plan, { json }); @@ -556,7 +1041,10 @@ export async function runCli(argv = process.argv.slice(2), { if (command === 'down') { const control = openControlPlane({ repositoryRoot: repository.root, env }); try { - const adapters = runtimeAdapters(); + const pluginNames = [...new Set([ + ...configuredRuntimeNames(repository.root), ...activeRuntimeNames(control), + ])]; + const adapters = runtimeAdapters(env, pluginNames); const plan = planFleetDown({ repositoryRoot: repository.root, controlPlane: control, adapters }); if (argv.includes('--dry-run')) { print(plan, { json }); @@ -567,7 +1055,7 @@ export async function runCli(argv = process.argv.slice(2), { code: 'APPROVAL_REQUIRED', }); } - const stoppers = runtimeStoppers(spawn); + const stoppers = runtimeStoppers(spawn, env, pluginNames); print(stopFleet({ plan, controlPlane: control, stopRuntime: (action) => stopThrough(stoppers, action), @@ -586,7 +1074,7 @@ export async function runCli(argv = process.argv.slice(2), { if (command === 'detach') { const control = openControlPlane({ repositoryRoot: repository.root, env }); try { - const stoppers = runtimeStoppers(spawn); + const stoppers = runtimeStoppers(spawn, env, activeRuntimeNames(control)); const plan = planFleetDetach({ repositoryRoot: repository.root, controlPlane: control, adapters: stoppers, }); @@ -699,13 +1187,98 @@ export async function runCli(argv = process.argv.slice(2), { })), { json }); return 0; } + if (command === 'approvals') { + const operation = argv[1] ?? 'list'; + const control = openControlPlane({ repositoryRoot: repository.root, env }); + try { + if (operation === 'list') { + const actorId = optionValue(argv, '--area') ?? 'owner'; + print({ approvals: control.listApprovals({ actorId, status: optionValue(argv, '--status') }) }, { json }); + return 0; + } + if (operation === 'decide') { + const decidedBy = optionValue(argv, '--by'); + if (decidedBy !== 'owner') { + throw new TorchError('AI approvals must be decided through the bound TORCH MCP identity.', { + code: 'APPROVER_AUTHORITY_REQUIRED', + }); + } + if (!argv.includes('--yes')) { + throw new TorchError('Owner approval decisions require explicit --yes after reviewing the request.', { + code: 'APPROVAL_REQUIRED', + }); + } + const result = control.decideApproval({ + approvalId: optionValue(argv, '--id'), decidedBy, + decision: optionValue(argv, '--decision'), note: optionValue(argv, '--note'), + expectedRevision: Number(optionValue(argv, '--revision')) || undefined, + }); + print(result, { json }); + return 0; + } + throw new TorchError(`Unknown approvals operation: ${operation}`, { code: 'UNKNOWN_COMMAND' }); + } finally { + control.close(); + } + } if (command === 'fleet') { const operation = argv[1] ?? 'changes'; const control = openControlPlane({ repositoryRoot: repository.root, env }); try { const evolution = new FleetEvolutionService({ repositoryRoot: repository.root, controlPlane: control }); + const hierarchy = new HierarchyEvolutionService({ repositoryRoot: repository.root, controlPlane: control }); const changeId = optionValue(argv, '--change'); - if (operation === 'changes') print({ changes: evolution.list({ state: optionValue(argv, '--state') }) }, { json }); + const hierarchyProposalId = optionValue(argv, '--proposal'); + if (operation === 'hierarchy-assess') print(hierarchy.assess({ + assessor: optionValue(argv, '--from') ?? 'session-manager', + }), { json }); + else if (operation === 'hierarchy-changes') print({ proposals: hierarchy.list({ state: optionValue(argv, '--state') }) }, { json }); + else if (operation === 'hierarchy-get') print(hierarchy.get(hierarchyProposalId), { json }); + else if (operation === 'hierarchy-propose') { + const input = loadJsonFile(cwd, optionValue(argv, '--proposal'), { + label: 'Hierarchy change proposal', code: 'HIERARCHY_PROPOSAL_INVALID', + }); + print(hierarchy.propose({ + proposer: optionValue(argv, '--from') ?? input.proposer, + proposal: input.proposal ?? input, + }), { json }); + } else if (operation === 'hierarchy-decide') { + if (!argv.includes('--yes')) { + throw new TorchError('Owner review of a hierarchy proposal requires explicit --yes.', { code: 'APPROVAL_REQUIRED' }); + } + print(hierarchy.decide({ + proposalId: hierarchyProposalId, decision: optionValue(argv, '--decision'), + decidedBy: optionValue(argv, '--by'), reason: optionValue(argv, '--reason'), + }), { json }); + } else if (operation === 'hierarchy-plan') print(hierarchy.planPilot(hierarchyProposalId), { json }); + else if (operation === 'hierarchy-conclusion-plan') print(hierarchy.planConclusion({ + proposalId: hierarchyProposalId, decision: optionValue(argv, '--decision'), reason: optionValue(argv, '--reason'), + }), { json }); + else if (operation === 'hierarchy-conclude') print(hierarchy.concludePilot({ + proposalId: hierarchyProposalId, decision: optionValue(argv, '--decision'), reason: optionValue(argv, '--reason'), + planHash: optionValue(argv, '--plan-hash'), decidedBy: optionValue(argv, '--by'), approved: argv.includes('--yes'), + }), { json }); + else if (operation === 'hierarchy-pilot-reviews') print({ reviews: hierarchy.pilotReviews(hierarchyProposalId) }, { json }); + else if (operation === 'hierarchy-review-pilot') { + if (!argv.includes('--yes')) throw new TorchError('Recording pilot review evidence requires explicit --yes.', { code: 'APPROVAL_REQUIRED' }); + print(hierarchy.recordPilotReview({ proposalId: hierarchyProposalId, + reviewer: optionValue(argv, '--from') ?? 'session-manager', + review: loadJsonFile(cwd, optionValue(argv, '--review'), { + label: 'Hierarchy pilot review', code: 'HIERARCHY_REVIEW_INVALID', + }), + }), { json }); + } + else if (operation === 'hierarchy-activate') { + if (!argv.includes('--yes')) { + throw new TorchError('Hierarchy activation writes and commits tracked organization state. Review hierarchy-plan, then use --yes; no runtime is started.', { + code: 'APPROVAL_REQUIRED', + }); + } + print(hierarchy.activatePilot({ + proposalId: hierarchyProposalId, activatedBy: optionValue(argv, '--by'), + }), { json }); + } + else if (operation === 'changes') print({ changes: evolution.list({ state: optionValue(argv, '--state') }) }, { json }); else if (operation === 'get') print(evolution.get(changeId), { json }); else if (operation === 'assess') print(evolution.assessDomainNeeds({ assessor: optionValue(argv, '--from') ?? 'session-manager', @@ -771,7 +1344,9 @@ export async function runCli(argv = process.argv.slice(2), { code: 'FLEET_CHANGE_STATE_CONFLICT', details: { state: change.state }, }); } - const adapters = runtimeAdapters(); + const adapters = runtimeAdapters(env, [ + ...configuredRuntimeNames(repository.root), change.domain.runtime, + ]); const plan = planAreaUp({ repositoryRoot: repository.root, controlPlane: control, areaId: change.domain.id, adapters, fresh: argv.includes('--fresh'), @@ -809,7 +1384,7 @@ export async function runCli(argv = process.argv.slice(2), { repositoryRoot: repository.root, controlPlane: control, checkService: checks, }); const backlog = new BacklogService({ - repositoryRoot: repository.root, controlPlane: control, + repositoryRoot: repository.root, controlPlane: control, checkService: checks, integrationLookup: (requestId) => integration.get(requestId), }); if (operation === 'list') print({ tasks: backlog.list({ @@ -818,12 +1393,34 @@ export async function runCli(argv = process.argv.slice(2), { else if (operation === 'next') print(backlog.next({ areaId: optionValue(argv, '--area'), }), { json }); + else if (operation === 'claim-next') { + const areaId = optionValue(argv, '--area'); + if (argv.includes('--dry-run')) print(backlog.planClaim({ areaId }), { json }); + else { + if (!argv.includes('--yes')) throw new TorchError('Self-claim requires explicit --yes and approved project policy.', { + code: 'APPROVAL_REQUIRED', + }); + print(backlog.claimNext({ areaId }), { json }); + } + } else if (operation === 'health') print(backlog.health({ staleAfterDays: optionValue(argv, '--stale-days') === undefined ? 7 : Number(optionValue(argv, '--stale-days')), staleObservedCommits: optionValue(argv, '--stale-observed-commits') === undefined ? 50 : Number(optionValue(argv, '--stale-observed-commits')), }), { json }); + else if (operation === 'activity') print(backlog.activity({ + staleDays: optionValue(argv, '--stale-days') === undefined ? undefined : Number(optionValue(argv, '--stale-days')), + maxCommits: optionValue(argv, '--max-commits') === undefined ? undefined : Number(optionValue(argv, '--max-commits')), + }), { json }); + else if (operation === 'close-landed') { + const input = { integrationRequest: optionValue(argv, '--integration'), actorId: optionValue(argv, '--area') }; + if (argv.includes('--dry-run')) print(backlog.planLandedClosure(input), { json }); + else { + if (!argv.includes('--yes')) throw new TorchError('Landed closure requires explicit --yes.', { code: 'APPROVAL_REQUIRED' }); + print(backlog.reconcileLanded({ ...input, approved: true }), { json }); + } + } else if (operation === 'get') print(backlog.get(optionValue(argv, '--task')), { json }); else if (operation === 'create') print(backlog.create({ actorId: optionValue(argv, '--area'), title: optionValue(argv, '--title'), @@ -832,6 +1429,14 @@ export async function runCli(argv = process.argv.slice(2), { affectedDomains: commaList(optionValue(argv, '--domains')), dependencies: commaList(optionValue(argv, '--depends')), observedAt: optionValue(argv, '--observed-at'), + feature: optionValue(argv, '--feature'), milestone: optionValue(argv, '--milestone'), + }), { json }); + else if (operation === 'classify') print(backlog.classify({ + taskId: optionValue(argv, '--task'), actorId: optionValue(argv, '--area'), + expectedRevision: Number(optionValue(argv, '--revision')), + feature: optionValue(argv, '--feature'), milestone: optionValue(argv, '--milestone'), + clearFeature: argv.includes('--clear-feature'), clearMilestone: argv.includes('--clear-milestone'), + reason: optionValue(argv, '--reason'), }), { json }); else if (operation === 'transition') print(backlog.transition({ taskId: optionValue(argv, '--task'), actorId: optionValue(argv, '--area'), @@ -884,11 +1489,16 @@ export async function runCli(argv = process.argv.slice(2), { print(plan, { json }); return plan.canProceed ? 0 : 1; } + if (launcherOperation === 'plan-reconcile') { + const plan = launcher.planReconcile(); + print(plan, { json }); + return plan.canProceed ? 0 : 1; + } if (launcherOperation === 'status') { print(launcher.status(), { json }); return 0; } - if (!['install', 'remove'].includes(launcherOperation)) { + if (!['install', 'reconcile', 'remove'].includes(launcherOperation)) { throw new TorchError(`Unknown schedule launcher operation: ${launcherOperation}`, { code: 'UNKNOWN_COMMAND' }); } if (!argv.includes('--yes')) { @@ -896,12 +1506,44 @@ export async function runCli(argv = process.argv.slice(2), { code: 'APPROVAL_REQUIRED', }); } - print(launcherOperation === 'install' ? launcher.install() : launcher.remove(), { json }); + const result = launcherOperation === 'install' ? launcher.install() + : launcherOperation === 'reconcile' ? launcher.reconcile() : launcher.remove(); + print(result, { json }); return 0; } const control = openControlPlane({ repositoryRoot: repository.root, env }); try { - const schedules = new ScheduleService({ repositoryRoot: repository.root, controlPlane: control }); + const schedules = new ScheduleService({ + repositoryRoot: repository.root, controlPlane: control, + wakeManager: { + plan: ({ managerId, maxUsd }) => { + const adapters = runtimeAdapters(env, configuredRuntimeNames(repository.root, [managerId])); + const plan = planAreaUp({ + repositoryRoot: repository.root, controlPlane: control, areaId: managerId, + adapters, maxCostUsd: maxUsd, + }); + return { plan, adapters }; + }, + invoke: ({ managerId, maxUsd, prepared }) => { + const { plan, adapters } = prepared; + const plannedAction = plan.actions?.find((action) => action.areaId === managerId); + if (!plan.canProceed || !plannedAction || plan.actions.length !== 1 + || (maxUsd !== undefined && (plannedAction.costCeiling?.enforced !== true + || plannedAction.costCeiling.maxUsd !== maxUsd))) { + throw new TorchError(`Manager runtime wake is blocked for ${managerId}`, { + code: 'MANAGER_WAKE_BLOCKED', details: plan.blockers, + }); + } + return startFleet({ + plan, controlPlane: control, adapters, + executor: (launch) => spawn(launch.command, launch.args, { + cwd: launch.cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + timeout: 300_000, maxBuffer: 8 * 1024 * 1024, killSignal: 'SIGTERM', + }), + }); + }, + }, + }); const scheduleId = optionValue(argv, '--id'); const actorId = optionValue(argv, '--actor') ?? 'owner'; if (operation === 'dispatch-system') { @@ -913,6 +1555,14 @@ export async function runCli(argv = process.argv.slice(2), { } if (operation === 'list') print({ schedules: schedules.list({ actorId }) }, { json }); else if (operation === 'runs') print({ runs: schedules.runs({ scheduleId }) }, { json }); + else if (operation === 'wakes') print({ reservations: schedules.wakeReservations({ + managerId: optionValue(argv, '--manager'), + }) }, { json }); + else if (operation === 'recover-wake') print(schedules.recoverWake({ + reservationId: optionValue(argv, '--reservation'), actorId, + approved: argv.includes('--yes'), runtimeStopped: argv.includes('--runtime-stopped'), + note: optionValue(argv, '--note'), + }), { json }); else if (operation === 'plan') print(schedules.plan({ scheduleId, actorId }), { json }); else if (operation === 'run') { const result = schedules.run({ scheduleId, actorId, approved: argv.includes('--yes') }); @@ -940,11 +1590,24 @@ export async function runCli(argv = process.argv.slice(2), { else if (operation === 'plan') print(checks.plan({ checkId: optionValue(argv, '--id'), areaId: optionValue(argv, '--area'), }), { json }); - else if (operation === 'run') { + else if (operation === 'prepared') print({ prepared: checks.prepared({ + areaId: optionValue(argv, '--area'), + }) }, { json }); + else if (operation === 'recovery-plan') print(checks.planPreparedRecovery({ + preparedId: optionValue(argv, '--prepared'), actorId: 'owner', + }), { json }); + else if (operation === 'recover-prepared') print(checks.recoverPrepared({ + preparedId: optionValue(argv, '--prepared'), actorId: 'owner', approved: argv.includes('--yes'), + executorsStopped: argv.includes('--executors-stopped'), evidence: optionValue(argv, '--evidence'), + }), { json }); + else if (['run', 'prepare', 'run-prepared', 'cancel-prepared'].includes(operation)) { if (!argv.includes('--yes')) { throw new TorchError('Configured check execution requires an explicit --yes.', { code: 'APPROVAL_REQUIRED' }); } - print(checks.run({ checkId: optionValue(argv, '--id'), areaId: optionValue(argv, '--area') }), { json }); + const input = { checkId: optionValue(argv, '--id'), areaId: optionValue(argv, '--area'), + preparedId: optionValue(argv, '--prepared') }; + const action = { run: 'run', prepare: 'prepare', 'run-prepared': 'runPrepared', 'cancel-prepared': 'cancelPrepared' }[operation]; + print(checks[action](input), { json }); } else throw new TorchError(`Unknown checks operation: ${operation}`, { code: 'UNKNOWN_COMMAND' }); return 0; } finally { @@ -1033,6 +1696,14 @@ export async function runCli(argv = process.argv.slice(2), { }); } print(integration.land({ requestId, actorId }), { json }); + } else if (operation === 'drain') { + if (!argv.includes('--yes')) { + throw new TorchError('Draining the authorized FIFO integration queue changes canonical main. Review integrate list, then re-run with --yes.', { + code: 'APPROVAL_REQUIRED', + }); + } + const limit = optionValue(argv, '--limit') === undefined ? 50 : Number(optionValue(argv, '--limit')); + print(integration.drain({ actorId, limit }), { json }); } else throw new TorchError(`Unknown integrate operation: ${operation}`, { code: 'UNKNOWN_COMMAND' }); return 0; } finally { @@ -1063,8 +1734,87 @@ export async function runCli(argv = process.argv.slice(2), { }), { json }); return 0; } + if (command === 'artifacts') { + const operation = argv[1] ?? 'list'; + if (operation === 'list') { + const snapshot = observeProject({ repositoryRoot: repository.root, env }); + print(snapshot.artifacts ?? { available: false, items: [] }, { json }); + return 0; + } + if (operation === 'comment' && !argv.includes('--yes')) { + const by = optionValue(argv, '--by'); + if (by !== 'owner') throw new TorchError('Artifact feedback preview requires --by owner.', { + code: 'OWNER_AUTHORITY_REQUIRED', + }); + const artifactId = optionValue(argv, '--artifact'); + const artifact = (observeProject({ repositoryRoot: repository.root, env }).artifacts?.items ?? []) + .find((item) => item.id === artifactId); + if (!artifact) throw new TorchError(`Unknown artifact: ${artifactId}`, { code: 'ARTIFACT_NOT_FOUND' }); + const body = optionValue(argv, '--body'); + if (!body?.trim()) throw new TorchError('Artifact feedback body is required.', { code: 'INVALID_ARTIFACT_INPUT' }); + print({ + schema: 'torch.dev/artifact-feedback-plan/v1alpha1', mutationPerformed: false, + artifactId, body: body.trim(), by, recipient: artifact.identityId, + taskId: artifact.taskId, commit: artifact.commit, + effect: 'Create one durable owner message for the responsible identity, linked to this artifact and backlog task.', + }, { json }); + return 0; + } + const control = openControlPlane({ repositoryRoot: repository.root, env }); + try { + const backlog = new BacklogService({ repositoryRoot: repository.root, controlPlane: control }); + const artifacts = new ArtifactService({ repositoryRoot: repository.root, controlPlane: control, backlogService: backlog }); + if (operation === 'publish') { + print(artifacts.publish({ + areaId: optionValue(argv, '--area'), taskId: optionValue(argv, '--task'), + title: optionValue(argv, '--title'), alt: optionValue(argv, '--alt'), + file: optionValue(argv, '--file'), commit: optionValue(argv, '--commit'), + }), { json }); + } else if (operation === 'comment') { + print(artifacts.comment({ + artifactId: optionValue(argv, '--artifact'), body: optionValue(argv, '--body'), + by: optionValue(argv, '--by'), + }), { json }); + } else throw new TorchError(`Unsupported artifacts operation: ${operation}`, { code: 'CLI_USAGE' }); + } finally { control.close(); } + return 0; + } if (command === 'forge') { const operation = argv[1] ?? 'status'; + if (operation === 'fetch') { + if (argv[2] === 'status') { + print({ fetches: observeProject({ repositoryRoot: repository.root, env }).canonicalFetches ?? [], + evidence: 'Recorded operations; running records do not establish live executor state' }, { json }); + return 0; + } + if (!argv.includes('--yes')) { + if (argv[2] !== 'plan') throw new TorchError('Review forge fetch plan, then confirm with --yes.', { code: 'APPROVAL_REQUIRED' }); + print(planCanonicalFetch({ repositoryRoot: repository.root }), { json }); + return 0; + } + const result = withControlPlane(repository, env, (controlPlane) => fetchCanonicalObjects({ + repositoryRoot: repository.root, controlPlane, attempts: Number(optionValue(argv, '--attempts') ?? 3), + })); + print(result, { json }); + return result.succeeded ? 0 : 1; + } + if (operation === 'sync') { + if ((argv[2] ?? 'plan') === 'plan') { + const plan = planForgeSync({ repositoryRoot: repository.root }); + print(plan, { json }); + return plan.canProceed ? 0 : 1; + } + if (!argv.includes('--yes')) { + throw new TorchError('Forge sync publishes the current canonical commit to the configured remote. Review forge sync plan, then use --yes.', { + code: 'APPROVAL_REQUIRED', + }); + } + const control = openControlPlane({ repositoryRoot: repository.root, env }); + try { + print(syncForgeRemote({ repositoryRoot: repository.root, controlPlane: control, actorId: 'owner' }), { json }); + } finally { control.close(); } + return 0; + } if (operation === 'status') { const status = forgeStatus({ repositoryRoot: repository.root }); print(status, { json }); @@ -1106,6 +1856,20 @@ export async function runCli(argv = process.argv.slice(2), { } throw new TorchError(`Unknown forge operation: ${operation}`, { code: 'UNKNOWN_COMMAND' }); } + if (command === 'digest') { + const operation = argv[1] ?? 'build'; + const control = openControlPlane({ repositoryRoot: repository.root, env }); + try { + const service = new OwnerDigestService({ repositoryRoot: repository.root, controlPlane: control }); + if (operation === 'build') { + const report = service.build(); + if (json) print(report, { json }); else process.stdout.write(report.markdown); + } else if (operation === 'latest') print(service.latest(), { json }); + else if (operation === 'publish') print(service.publish({ actorId: optionValue(argv, '--by'), approved: argv.includes('--yes') }), { json }); + else throw new TorchError('Unknown digest operation', { code: 'UNKNOWN_COMMAND' }); + return 0; + } finally { control.close(); } + } if (command === 'delivery') { const operation = argv[1] ?? 'list'; if (operation === 'configure') { @@ -1138,6 +1902,20 @@ export async function runCli(argv = process.argv.slice(2), { }); const deliveryId = optionValue(argv, '--delivery'); if (operation === 'list') print({ deliveries: delivery.list({ state: optionValue(argv, '--state') }) }, { json }); + else if (operation === 'attempts') print({ attempts: delivery.attempts({ deliveryId }) }, { json }); + else if (operation === 'operations') print({ operations: delivery.operations({ deliveryId }) }, { json }); + else if (operation === 'recover-not-applied') print(delivery.recoverNotApplied({ + operationId: optionValue(argv, '--operation'), actor: optionValue(argv, '--by'), + approved: argv.includes('--yes'), runtimeStopped: argv.includes('--runtime-stopped'), + evidence: commaList(optionValue(argv, '--evidence')), + }), { json }); + else if (operation === 'recover-succeeded') { + const input = { operationId: optionValue(argv, '--operation'), actor: optionValue(argv, '--by') }; + if (argv.includes('--dry-run')) print(delivery.planSucceededRecovery(input), { json }); + else print(delivery.reconcileSucceeded({ ...input, approved: argv.includes('--yes'), + runtimeStopped: argv.includes('--runtime-stopped'), evidence: commaList(optionValue(argv, '--evidence')), + }), { json }); + } else if (operation === 'get') print(delivery.get(deliveryId), { json }); else if (operation === 'create') print(delivery.create({ sourceArea: optionValue(argv, '--area'), commit: optionValue(argv, '--commit') ?? 'HEAD', @@ -1173,7 +1951,7 @@ export async function runCli(argv = process.argv.slice(2), { if (!purge) { const control = openControlPlane({ repositoryRoot: repository.root, env }); try { - const stoppers = runtimeStoppers(spawn); + const stoppers = runtimeStoppers(spawn, env, activeRuntimeNames(control)); const fleet = planFleetDetach({ repositoryRoot: repository.root, controlPlane: control, adapters: stoppers, }); diff --git a/src/console/server.mjs b/src/console/server.mjs index 108aadd..bfaf17c 100644 --- a/src/console/server.mjs +++ b/src/console/server.mjs @@ -1,19 +1,457 @@ import { createServer } from 'node:http'; +import { spawnSync } from 'node:child_process'; +import { createHash, randomUUID } from 'node:crypto'; import { readFileSync } from 'node:fs'; -import { dirname, extname, join } from 'node:path'; +import { dirname, extname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { observeProject } from '../observability/snapshot.mjs'; +import { ArtifactService, readPublishedArtifact } from '../artifacts/service.mjs'; +import { BacklogService } from '../backlog/service.mjs'; +import { openControlPlane } from '../control-plane/service.mjs'; +import { FleetEvolutionService } from '../evolution/service.mjs'; +import { HierarchyEvolutionService } from '../evolution/hierarchy.mjs'; +import { loadProjectConfig } from '../kernel/config.mjs'; +import { inspectRepository } from '../kernel/git.mjs'; +import { organizationGraphFromConfig } from '../kernel/organization.mjs'; +import { fileHash } from '../kernel/files.mjs'; +import { ScheduleLauncherService } from '../schedules/launcher.mjs'; const SITE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'site'); +const TORCH_CLI = fileURLToPath(new URL('../../bin/torch.mjs', import.meta.url)); 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'], + ['/demo.html', 'demo.html'], ['/demo.js', 'demo.js'], ['/demo-artifact.svg', 'demo-artifact.svg'], + ['/console.js', 'console.js'], ['/work-views.js', 'work-views.js'], + ['/work-progress.js', 'work-progress.js'], ['/torch-mark.svg', 'torch-mark.svg'], + ['/owner-digest.js', 'owner-digest.js'], + ['/check-evidence.js', 'check-evidence.js'], + ['/task-activity.js', 'task-activity.js'], + ['/operation-outcomes.js', 'operation-outcomes.js'], + ['/live-refresh.js', 'live-refresh.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'], ]); +const MAX_FEEDBACK_BYTES = 8 * 1024; +const FEEDBACK_PREVIEW_TTL_MS = 5 * 60 * 1000; +const MAX_FEEDBACK_PREVIEWS = 200; +const ORGANIZATION_REVIEW_PREVIEW_TTL_MS = 5 * 60 * 1000; +const HIERARCHY_ACTIVATION_PREVIEW_TTL_MS = 5 * 60 * 1000; +const OWNER_REQUEST_PREVIEW_TTL_MS = 5 * 60 * 1000; +const APPROVAL_DECISION_PREVIEW_TTL_MS = 5 * 60 * 1000; +const RUNTIME_PROFILE_PREVIEW_TTL_MS = 5 * 60 * 1000; +const SCHEDULE_LAUNCHER_PREVIEW_TTL_MS = 5 * 60 * 1000; + +function feedbackPlanHash(plan) { + return createHash('sha256').update(JSON.stringify(plan)).digest('hex'); +} + +function priorityPlanHash(plan) { + return createHash('sha256').update(JSON.stringify(plan)).digest('hex'); +} + +function sameOriginLoopback(request) { + const origin = request.headers.origin; + const host = request.headers.host; + const peerAddress = String(request.socket.remoteAddress ?? '').toLowerCase().replace(/^::ffff:/, ''); + const loopbackPeer = peerAddress === '127.0.0.1' || peerAddress === '::1'; + if (!loopbackPeer || typeof origin !== 'string' || typeof host !== 'string') return false; + try { + const parsed = new URL(origin); + const requestOrigin = new URL(`http://${host}`).origin; + return parsed.protocol === 'http:' + && ['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname.toLowerCase()) + && parsed.origin === requestOrigin + && (request.headers['sec-fetch-site'] === undefined + || request.headers['sec-fetch-site'] === 'same-origin'); + } catch { return false; } +} + +async function readJsonRequest(request) { + const mediaType = String(request.headers['content-type'] ?? '').split(';', 1)[0].trim().toLowerCase(); + if (mediaType !== 'application/json') { + const error = new Error('Console actions must use application/json'); + error.code = 'REQUEST_CONTENT_TYPE_INVALID'; + throw error; + } + const declaredSize = Number(request.headers['content-length'] ?? 0); + if (Number.isFinite(declaredSize) && declaredSize > MAX_FEEDBACK_BYTES) { + const error = new Error('Console action request is too large'); + error.code = 'REQUEST_TOO_LARGE'; + throw error; + } + const chunks = []; + let size = 0; + for await (const chunk of request) { + size += chunk.length; + if (size > MAX_FEEDBACK_BYTES) { + const error = new Error('Console action request is too large'); + error.code = 'REQUEST_TOO_LARGE'; + throw error; + } + chunks.push(chunk); + } + try { + const value = JSON.parse(Buffer.concat(chunks).toString('utf8')); + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected a JSON object'); + return value; + } catch { + const invalid = new Error('Console action request must contain a valid JSON object'); + invalid.code = 'REQUEST_JSON_INVALID'; + throw invalid; + } +} + +function withArtifactService({ repositoryRoot, env }, callback) { + const controlPlane = openControlPlane({ repositoryRoot, env }); + try { + const backlogService = new BacklogService({ repositoryRoot, controlPlane }); + const artifactService = new ArtifactService({ repositoryRoot, controlPlane, backlogService }); + return callback(artifactService); + } finally { controlPlane.close(); } +} + +function withOrganizationServices({ repositoryRoot, env, clock }, callback) { + const controlPlane = openControlPlane({ repositoryRoot, env }); + try { + return callback({ + fleet: new FleetEvolutionService({ repositoryRoot, controlPlane }), + hierarchy: new HierarchyEvolutionService({ repositoryRoot, controlPlane, clock }), + }); + } finally { controlPlane.close(); } +} + +function organizationReviewPlan({ repositoryRoot, env, type, id, action, reason, clock }) { + const currentHead = inspectRepository(repositoryRoot).head; + return withOrganizationServices({ repositoryRoot, env, clock }, ({ fleet, hierarchy }) => { + const config = loadProjectConfig(repositoryRoot); + const graph = organizationGraphFromConfig(config); + const owner = graph.roles.find((role) => role.kind === 'owner')?.identity_id; + const activeGraphDigest = createHash('sha256').update(JSON.stringify(graph)).digest('hex'); + if (owner !== 'owner') { + const error = new Error('The active organization does not identify the project owner as owner.'); + error.code = 'OWNER_AUTHORITY_REQUIRED'; + throw error; + } + if (type === 'hierarchy-conclusion') { + const proposal = hierarchy.get(id); + const conclusionPlan = hierarchy.planConclusion({ proposalId: id, decision: action, reason }); + return { type, id, action, reason, ownerIdentity: owner, title: proposal.proposal.title, + currentState: proposal.state, currentHead, activeGraphRevision: graph.revision, activeGraphDigest, + canProceed: conclusionPlan.canProceed, scope: conclusionPlan, + conclusionPlanHash: conclusionPlan.planHash, + confirmationLabel: action === 'adopt' ? 'Commit pilot adoption' : 'Commit pilot reversal', + effect: action === 'adopt' + ? 'Record owner adoption in one scoped local commit. The active graph is retained. No runtime or timer is started or stopped.' + : 'Restore prior reporting relationships at a new graph revision in one scoped local commit. Identities, worktrees, implementation owners and active work are preserved. Schedules remain for explicit review. No runtime or timer is started or stopped.', + mutationPerformed: false }; + } + if (type === 'fleet') { + const change = fleet.get(id); + if (change.state !== 'proposed') { + const error = new Error(`Fleet proposal ${change.id} is ${change.state}, not proposed.`); + error.code = 'FLEET_CHANGE_STATE_CONFLICT'; + throw error; + } + if (!['approve', 'reject'].includes(action)) { + const error = new Error('Fleet proposals support approve or reject review decisions.'); + error.code = 'FLEET_CHANGE_DECISION_INVALID'; + throw error; + } + if (action === 'reject' && !reason) { + const error = new Error('Add a reason before rejecting a Fleet proposal.'); + error.code = 'FLEET_CHANGE_REASON_REQUIRED'; + throw error; + } + return { + type, id: change.id, action, reason: reason || null, currentState: change.state, + ownerIdentity: owner, baseCommit: change.baseCommit, currentHead, + activeGraphRevision: graph.revision, activeGraphDigest, + title: change.domain?.title ?? change.proposal?.title ?? change.id, + summary: change.rationale ?? change.proposal?.rationale ?? '', + scope: change.domain ?? change.proposal ?? null, + expectedBenefit: change.expectedBenefit ?? null, evidence: change.evidence ?? [], + effect: action === 'approve' + ? 'Approves this Fleet change for a separate activation review. It does not edit the active roster, create a worktree, or start an agent.' + : 'Rejects this Fleet change and records the supplied reason. The active Fleet is unchanged.', + confirmationLabel: action === 'approve' ? 'Confirm approval for later activation review' : 'Confirm rejection', + mutationPerformed: false, + }; + } + if (type !== 'hierarchy') { + const error = new Error('Unsupported organization proposal type.'); + error.code = 'ORGANIZATION_PROPOSAL_TYPE_INVALID'; + throw error; + } + const proposal = hierarchy.get(id); + if (proposal.state !== 'proposed') { + const error = new Error(`Hierarchy proposal ${proposal.id} is ${proposal.state}, not proposed.`); + error.code = 'HIERARCHY_PROPOSAL_STATE_CONFLICT'; + throw error; + } + if (!['approve-pilot', 'defer', 'reject'].includes(action)) { + const error = new Error('Hierarchy proposals support approve-pilot, defer, or reject.'); + error.code = 'HIERARCHY_DECISION_INVALID'; + throw error; + } + if (action !== 'approve-pilot' && !reason) { + const error = new Error('Add a reason before deferring or rejecting a hierarchy proposal.'); + error.code = 'HIERARCHY_DECISION_REASON_REQUIRED'; + throw error; + } + const details = proposal.proposal; + const pilotPlan = hierarchy.planPilot(id); + const activeRoles = new Set(graph.roles.map((role) => role.id)); + const proposedRoles = details.graph.roles.map((role) => role.id); + const roleChanges = { + added: proposedRoles.filter((roleId) => !activeRoles.has(roleId)), + removed: [...activeRoles].filter((roleId) => !proposedRoles.includes(roleId)), + totalProposedRoles: proposedRoles.length, + }; + const effects = { + 'approve-pilot': 'Records owner approval for bounded pilot planning only. Activation remains a separate fresh-plan CLI action; it updates and commits the approved organization graph and derived manager schedules, but does not start/create an identity, launch a runtime, or change user systemd state.', + defer: 'Defers this proposal without changing the active organization graph.', + reject: 'Rejects this proposal and records the supplied reason. The active organization graph is unchanged.', + }; + return { + type, id: proposal.id, proposalRevision: proposal.revision, action, reason: reason || null, currentState: proposal.state, + ownerIdentity: owner, baseCommit: proposal.baseCommit, currentHead, + activeGraphRevision: graph.revision, activeGraphDigest, + title: details.title, summary: details.rationale, + impact: details.impact, observationWindow: details.observation_window, + signals: details.signals, alternatives: details.alternatives, + pilot: details.pilot, roleChanges, + managerCheckInSchedules: pilotPlan.managerCheckInSchedules, + timerReconciliation: { + timerState: 'not-verified', + requiredAfterScheduleConfigChange: pilotPlan.timerReconciliation.requiredAfterActivation, + command: pilotPlan.timerReconciliation.commands, + note: 'A proposed manager cadence is not an installed timer. If an exact-config launcher is installed, hierarchy activation requires the owner to reconcile it separately before its new schedule is trusted.', + }, + effect: effects[action], + confirmationLabel: action === 'approve-pilot' ? 'Confirm pilot-plan approval' : `Confirm ${action}`, + mutationPerformed: false, + }; + }); +} + +function hierarchyActivationPlan({ repositoryRoot, env, id }) { + const currentHead = inspectRepository(repositoryRoot).head; + return withOrganizationServices({ repositoryRoot, env }, ({ hierarchy }) => { + const proposal = hierarchy.get(id); + if (proposal.state !== 'pilot-approved') { + const error = new Error(`Hierarchy proposal ${proposal.id} is ${proposal.state}, not pilot-approved.`); + error.code = 'HIERARCHY_PROPOSAL_STATE_CONFLICT'; + throw error; + } + const graph = organizationGraphFromConfig(loadProjectConfig(repositoryRoot)); + const ownerIdentity = graph.roles.find((role) => role.kind === 'owner')?.identity_id; + if (ownerIdentity !== 'owner' || proposal.decidedBy !== ownerIdentity) { + const error = new Error('Only the project owner who approved this pilot can activate it.'); + error.code = 'HIERARCHY_OWNER_AUTHORITY_REQUIRED'; + throw error; + } + const pilotPlan = hierarchy.planPilot(id); + return { + type: 'hierarchy-activation', id: proposal.id, title: proposal.proposal.title, + state: proposal.state, ownerIdentity, currentHead, + pilotPlan, + effect: 'This writes the approved organization graph and manager schedules, records the pilot, and creates one scoped local Git commit. It does not create or start identities, launch an AI runtime, or change user systemd state. If a timer is installed, reconcile it separately after activation.', + confirmationLabel: 'Commit pilot activation', + mutationPerformed: false, + }; + }); +} + +function ownerRequestPlan({ repositoryRoot, env, recipient, body, taskId }) { + const currentHead = inspectRepository(repositoryRoot).head; + const controlPlane = openControlPlane({ repositoryRoot, env }); + try { + const owner = controlPlane.assertOwnerActor('owner'); + const recipientId = controlPlane.assertIdentity(recipient); + const identity = controlPlane.agents.get(recipientId); + let task = null; + if (taskId) { + task = new BacklogService({ repositoryRoot, controlPlane }).list().find((candidate) => candidate.id === taskId) ?? null; + if (!task || ['completed', 'cancelled'].includes(task.state)) { + const error = new Error(`Owner request task ${taskId} is missing or no longer active.`); + error.code = 'OWNER_REQUEST_TASK_NOT_FOUND'; + throw error; + } + task = { id: task.id, title: task.title, state: task.state, owner: task.owner ?? null }; + } + const graph = organizationGraphFromConfig(loadProjectConfig(repositoryRoot)); + const activeGraphDigest = createHash('sha256').update(JSON.stringify(graph)).digest('hex'); + return { + owner, recipient: recipientId, recipientTitle: identity?.title ?? recipientId, + body, task, currentHead, activeGraphRevision: graph.revision, activeGraphDigest, + effect: 'Stores one durable owner request in the selected identity inbox. It does not create or change a task, reassign ownership, wake or start a session, or expand the recipient’s authority.', + mutationPerformed: false, + }; + } finally { controlPlane.close(); } +} + +function approvalDecisionPlan({ repositoryRoot, env, approvalId, decision, note }) { + const controlPlane = openControlPlane({ repositoryRoot, env }); + try { + controlPlane.assertOwnerActor('owner'); + if (!['approved', 'rejected'].includes(decision)) { + const error = new Error('Choose approve or reject for this request.'); + error.code = 'INVALID_APPROVAL_DECISION'; + throw error; + } + const normalizedNote = typeof note === 'string' ? note.trim() : ''; + if (normalizedNote.length > 2000) { + const error = new Error('Decision note is limited to 2,000 characters.'); + error.code = 'APPROVAL_NOTE_TOO_LONG'; + throw error; + } + const approval = controlPlane.listApprovals({ actorId: 'owner' }) + .find((candidate) => candidate.id === approvalId); + if (!approval) { + const error = new Error(`Pending approval request not found: ${approvalId}`); + error.code = 'APPROVAL_NOT_FOUND'; + throw error; + } + if (approval.approver !== 'owner') { + const error = new Error(`Only the named approver (${approval.approver}) may decide this request.`); + error.code = 'APPROVER_AUTHORITY_REQUIRED'; + throw error; + } + if (approval.status !== 'pending') { + const error = new Error(`Approval request ${approvalId} is already ${approval.status}.`); + error.code = 'APPROVAL_NOT_PENDING'; + throw error; + } + return { + approvalId: approval.id, requester: approval.requester, approver: approval.approver, + revision: approval.revision, title: approval.title, summary: approval.summary, + task: approval.task, evidence: approval.evidence, decision, + note: normalizedNote || null, + effect: `Record this ${decision} decision against approval revision ${approval.revision}, audit it, and notify ${approval.requester}. This does not change task status, assignment, or agent state.`, + mutationPerformed: false, + }; + } finally { controlPlane.close(); } +} + +function normalizeRuntimeProfileRequest(input) { + const areaId = typeof input.areaId === 'string' ? input.areaId.trim() : ''; + const runtime = typeof input.runtime === 'string' ? input.runtime.trim() : ''; + const model = typeof input.model === 'string' ? input.model.trim() : ''; + const reasoning = typeof input.reasoning === 'string' ? input.reasoning.trim() : ''; + if (!areaId || !runtime || !/^[a-z][a-z0-9-]*$/.test(runtime)) { + const error = new Error('Choose a Fleet identity and an available runtime.'); + error.code = 'RUNTIME_PROFILE_INPUT_INVALID'; + throw error; + } + if (areaId.length > 120 || model.length > 180 || reasoning.length > 40) { + const error = new Error('Profile fields exceed their allowed length.'); + error.code = 'RUNTIME_PROFILE_INPUT_INVALID'; + throw error; + } + return { areaId, runtime, model, reasoning }; +} + +function runProfileCli({ repositoryRoot, env, args }) { + const result = spawnSync(process.execPath, [TORCH_CLI, ...args, '--json'], { + cwd: repositoryRoot, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + timeout: 60_000, maxBuffer: 1024 * 1024, + }); + let output; + try { output = JSON.parse(result.stdout ?? ''); } catch { + const error = new Error(result.error?.message ?? result.stderr?.trim() ?? 'Profile command returned invalid output.'); + error.code = 'RUNTIME_PROFILE_COMMAND_FAILED'; + throw error; + } + if (result.status !== 0) { + const error = new Error(output.message ?? result.stderr?.trim() ?? 'Runtime profile change was refused.'); + error.code = output.error ?? 'RUNTIME_PROFILE_COMMAND_FAILED'; + throw error; + } + return output; +} + +function runtimeProfileCliArgs(request, { dryRun = false, yes = false, configSha256 } = {}) { + const args = ['profile', 'set', '--area', request.areaId, '--runtime', request.runtime]; + if (request.model) args.push('--model', request.model); + else args.push('--inherit-model'); + if (request.reasoning) args.push('--reasoning', request.reasoning); + else args.push('--reasoning', 'none'); + if (request.resetLaunchPolicy) args.push('--reset-launch-policy'); + if (dryRun) args.push('--dry-run'); + if (yes) args.push('--yes'); + if (configSha256) args.push('--expect-config-sha256', configSha256); + return args; +} + +function runtimeProfilePlan({ repositoryRoot, env, input }) { + const request = normalizeRuntimeProfileRequest(input); + const controlPlane = openControlPlane({ repositoryRoot, env }); + try { controlPlane.assertOwnerActor('owner'); } finally { controlPlane.close(); } + const current = runProfileCli({ + repositoryRoot, env, args: ['profile', 'show', '--area', request.areaId], + }); + request.resetLaunchPolicy = current.runtime !== request.runtime; + const plan = runProfileCli({ + repositoryRoot, env, args: runtimeProfileCliArgs(request, { dryRun: true }), + }); + return { + ...plan, areaId: request.areaId, currentHead: inspectRepository(repositoryRoot).head, + resetLaunchPolicy: request.resetLaunchPolicy, + currentConfigSha256: fileHash(resolve(repositoryRoot, '.torch', 'torch.yaml')), + mutationPerformed: false, + }; +} + +function scheduleLauncherPlan({ repositoryRoot, env, action, executor }) { + if (!['install', 'reconcile'].includes(action)) { + const error = new Error('Choose whether to install or refresh the TORCH schedule timer.'); + error.code = 'SCHEDULE_LAUNCHER_ACTION_INVALID'; + throw error; + } + const controlPlane = openControlPlane({ repositoryRoot, env }); + try { controlPlane.assertOwnerActor('owner'); } finally { controlPlane.close(); } + const launcher = new ScheduleLauncherService({ repositoryRoot, env, executor }); + const fullPlan = action === 'install' ? launcher.plan() : launcher.planReconcile(); + const config = loadProjectConfig(repositoryRoot); + const status = launcher.status(); + const systemSchedules = config.schedules.filter((schedule) => schedule.lifetime === 'system').map((schedule) => ({ + id: schedule.id, title: schedule.title, owner: schedule.owner, trigger: schedule.trigger, + behavior: schedule.behavior, action: schedule.action, + })); + const files = (fullPlan.files ?? []).map((file) => ({ + name: file.name, path: file.path, + sha256: createHash('sha256').update(file.content).digest('hex'), + })); + return { + action, projectId: fullPlan.projectId, digest: fullPlan.digest, + currentHead: inspectRepository(repositoryRoot).head, + canProceed: fullPlan.canProceed, blockers: fullPlan.blockers, + timerName: fullPlan.timerName, serviceName: fullPlan.serviceName, + files, updates: fullPlan.updates ?? [], stale: fullPlan.stale ?? false, + installed: status.installed, installedStale: status.stale, + installedUnits: status.units.map(({ name, path, exists, configDigest }) => ({ name, path, exists, configDigest })), + systemSchedules, + runtimeWakeBudget: config.runtime_wake_budget ?? null, + effect: action === 'install' + ? 'Writes the two reviewed user-systemd unit files and ownership manifest, reloads the user systemd manager, then enables and starts the timer. Due system schedules may run; a manager runtime wakes only when its schedule explicitly enables wake and the daily invocation budget allows it.' + : 'Refreshes only unchanged TORCH-owned unit files, updates the ownership manifest, reloads the user systemd manager, and enables/starts the timer. Due system schedules may run; a manager runtime wakes only when its schedule explicitly enables wake and the daily invocation budget allows it.', + mutationPerformed: false, + }; +} + +function publicScheduleLauncherResult(result) { + return { + action: result.action, projectId: result.projectId, digest: result.digest, + timerName: result.timerName, serviceName: result.serviceName, + files: (result.files ?? []).map(({ name, path, sha256 }) => ({ name, path, sha256 })), + systemSchedules: result.systemSchedules, effect: result.effect, + mutationPerformed: result.mutationPerformed, changed: result.changed ?? null, + requiresCommit: result.requiresCommit ?? [], + }; +} function respond(response, status, body, type = 'application/json; charset=utf-8') { response.writeHead(status, { @@ -25,13 +463,768 @@ function respond(response, status, body, type = 'application/json; charset=utf-8 response.end(body); } -export function createConsoleServer({ repositoryRoot = process.cwd(), env = process.env } = {}) { - return createServer((request, response) => { +export function createConsoleServer({ repositoryRoot = process.cwd(), env = process.env, scheduleLauncherExecutor, clock } = {}) { + const feedbackPreviews = new Map(); + const priorityPreviews = new Map(); + const taskCreatePreviews = new Map(); + const organizationPreviews = new Map(); + const hierarchyActivationPreviews = new Map(); + const ownerRequestPreviews = new Map(); + const approvalDecisionPreviews = new Map(); + const runtimeProfilePreviews = new Map(); + const scheduleLauncherPreviews = new Map(); + return createServer(async (request, response) => { const url = new URL(request.url ?? '/', 'http://torch.local'); + if (['/api/schedule-launcher/preview', '/api/schedule-launcher'].includes(url.pathname)) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + try { + const action = input.action; + if (url.pathname.endsWith('/preview')) { + const plan = scheduleLauncherPlan({ + repositoryRoot, env, action, executor: scheduleLauncherExecutor, + }); + const planHash = priorityPlanHash(plan); + const now = Date.now(); + for (const [previewToken, record] of scheduleLauncherPreviews) { + if (record.expiresAt <= now) scheduleLauncherPreviews.delete(previewToken); + } + while (scheduleLauncherPreviews.size >= MAX_FEEDBACK_PREVIEWS) { + scheduleLauncherPreviews.delete(scheduleLauncherPreviews.keys().next().value); + } + const token = randomUUID(); + const expiresAt = new Date(now + SCHEDULE_LAUNCHER_PREVIEW_TTL_MS).toISOString(); + scheduleLauncherPreviews.set(token, { + action, planHash, digest: plan.digest, expiresAt: Date.parse(expiresAt), + consumed: false, result: null, + }); + respond(response, 200, `${JSON.stringify({ plan, planHash, token, expiresAt })}\n`); + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const preview = scheduleLauncherPreviews.get(token); + if (!preview || preview.expiresAt <= Date.now()) { + respond(response, 409, JSON.stringify({ error: 'SCHEDULE_LAUNCHER_PREVIEW_EXPIRED', message: 'Review the timer setup again before applying it.' })); + return; + } + if (preview.consumed) { + if (preview.result && input.planHash === preview.planHash && action === preview.action) { + respond(response, 200, `${JSON.stringify(preview.result)}\n`); + } else { + respond(response, 409, JSON.stringify({ error: 'SCHEDULE_LAUNCHER_PREVIEW_EXPIRED', message: 'This timer review has already been used. Review it again.' })); + } + return; + } + if (action !== preview.action || input.planHash !== preview.planHash) { + respond(response, 409, JSON.stringify({ error: 'SCHEDULE_LAUNCHER_PREVIEW_STALE', message: 'The requested timer action differs from the reviewed action. Review it again.' })); + return; + } + const current = scheduleLauncherPlan({ + repositoryRoot, env, action, executor: scheduleLauncherExecutor, + }); + if (priorityPlanHash(current) !== preview.planHash) { + respond(response, 409, JSON.stringify({ error: 'SCHEDULE_LAUNCHER_PREVIEW_STALE', message: 'Project schedules or owned unit files changed after review. Review the new plan.' })); + return; + } + if (!current.canProceed) { + respond(response, 409, JSON.stringify({ error: 'SCHEDULE_LAUNCHER_BLOCKED', message: 'The timer action is blocked by current ownership or configuration checks.', blockers: current.blockers })); + return; + } + const launcher = new ScheduleLauncherService({ + repositoryRoot, env, executor: scheduleLauncherExecutor, + }); + const rawResult = action === 'install' + ? launcher.install({ expectedDigest: preview.digest }) + : launcher.reconcile({ expectedDigest: preview.digest }); + const controlPlane = openControlPlane({ repositoryRoot, env }); + let audit; + try { + audit = controlPlane.auditOwnerAction({ + actorId: 'owner', operation: `schedule.launcher.${action}`, + entityType: 'system-schedule-launcher', entityId: rawResult.projectId, + details: { + digest: rawResult.digest, timerName: rawResult.timerName, + systemSchedules: current.systemSchedules.map((schedule) => schedule.id), + files: (rawResult.files ?? []).map(({ name, path }) => ({ name, path })), + mutationPerformed: rawResult.mutationPerformed, changed: rawResult.changed ?? null, + }, + }); + } finally { controlPlane.close(); } + preview.consumed = true; + preview.result = { + ...publicScheduleLauncherResult({ + ...rawResult, action, files: current.files, + systemSchedules: current.systemSchedules, effect: current.effect, + }), auditEventId: audit.id, + }; + respond(response, 200, `${JSON.stringify(preview.result)}\n`); + } catch (error) { + const status = error.code === 'OWNER_AUTHORITY_REQUIRED' ? 403 + : ['SCHEDULE_LAUNCHER_PLAN_STALE', 'SCHEDULE_LAUNCHER_BLOCKED', + 'SCHEDULE_LAUNCHER_RECONCILE_BLOCKED', 'SCHEDULE_LAUNCHER_SYSTEMD_FAILED'].includes(error.code) ? 409 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'SCHEDULE_LAUNCHER_FAILED', message: error.message, details: error.details ?? null })}\n`); + } + return; + } + if (['/api/runtime-profiles/preview', '/api/runtime-profiles'].includes(url.pathname)) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + let normalized; + try { normalized = normalizeRuntimeProfileRequest(input); } catch (error) { + respond(response, 400, JSON.stringify({ error: error.code, message: error.message })); + return; + } + if (url.pathname.endsWith('/preview')) { + try { + const plan = runtimeProfilePlan({ repositoryRoot, env, input: normalized }); + const planHash = priorityPlanHash(plan); + const now = Date.now(); + for (const [previewToken, record] of runtimeProfilePreviews) { + if (record.expiresAt <= now) runtimeProfilePreviews.delete(previewToken); + } + while (runtimeProfilePreviews.size >= MAX_FEEDBACK_PREVIEWS) { + runtimeProfilePreviews.delete(runtimeProfilePreviews.keys().next().value); + } + const token = randomUUID(); + const expiresAt = new Date(now + RUNTIME_PROFILE_PREVIEW_TTL_MS).toISOString(); + runtimeProfilePreviews.set(token, { + input: normalized, planHash, expiresAt: Date.parse(expiresAt), consumed: false, result: null, + }); + respond(response, 200, `${JSON.stringify({ plan, planHash, token, expiresAt })}\n`); + } catch (error) { + const status = error.code === 'OWNER_AUTHORITY_REQUIRED' ? 403 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'RUNTIME_PROFILE_PLAN_FAILED', message: error.message })}\n`); + } + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const preview = runtimeProfilePreviews.get(token); + if (!preview || preview.expiresAt <= Date.now()) { + respond(response, 409, JSON.stringify({ error: 'RUNTIME_PROFILE_PREVIEW_EXPIRED', message: 'Review a fresh identity profile before changing it.' })); + return; + } + if (preview.consumed) { + if (preview.result && input.planHash === preview.planHash) { + respond(response, 200, `${JSON.stringify(preview.result)}\n`); + } else { + respond(response, 409, JSON.stringify({ error: 'RUNTIME_PROFILE_PREVIEW_EXPIRED', message: 'This profile review has already been used. Review the change again.' })); + } + return; + } + if (JSON.stringify(normalized) !== JSON.stringify(preview.input) + || input.planHash !== preview.planHash) { + respond(response, 409, JSON.stringify({ error: 'RUNTIME_PROFILE_PREVIEW_STALE', message: 'The requested profile differs from the reviewed change. Review it again.' })); + return; + } + try { + const currentPlan = runtimeProfilePlan({ repositoryRoot, env, input: normalized }); + if (priorityPlanHash(currentPlan) !== preview.planHash) { + respond(response, 409, JSON.stringify({ error: 'RUNTIME_PROFILE_PREVIEW_STALE', message: 'The project, identity profile, or runtime configuration changed after preview. Review the new state.' })); + return; + } + preview.consumed = true; + const profileInput = { ...normalized, resetLaunchPolicy: currentPlan.resetLaunchPolicy }; + const result = runProfileCli({ + repositoryRoot, env, + args: runtimeProfileCliArgs(profileInput, { + yes: true, configSha256: currentPlan.currentConfigSha256, + }), + }); + preview.result = result; + respond(response, 200, `${JSON.stringify(result)}\n`); + } catch (error) { + const status = error.code === 'OWNER_AUTHORITY_REQUIRED' ? 403 + : error.code === 'RUNTIME_PROFILE_REVIEW_STALE' ? 409 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'RUNTIME_PROFILE_CHANGE_FAILED', message: error.message })}\n`); + } + return; + } + if (['/api/backlog/tasks', '/api/backlog/tasks/preview'].includes(url.pathname)) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + try { + const withBacklog = (callback) => { + const controlPlane = openControlPlane({ repositoryRoot, env }); + try { + const service = new BacklogService({ repositoryRoot, controlPlane }); + return callback(service); + } finally { controlPlane.close(); } + }; + const taskInput = { ...input, actorId: 'owner' }; + if (url.pathname.endsWith('/preview')) { + const plan = withBacklog((service) => service.planOwnerCreate(taskInput)); + const planHash = priorityPlanHash(plan); + const now = Date.now(); + for (const [token, record] of taskCreatePreviews) { + if (record.expiresAt <= now) taskCreatePreviews.delete(token); + } + while (taskCreatePreviews.size >= MAX_FEEDBACK_PREVIEWS) { + taskCreatePreviews.delete(taskCreatePreviews.keys().next().value); + } + const token = randomUUID(); + const expiresAt = new Date(now + FEEDBACK_PREVIEW_TTL_MS).toISOString(); + taskCreatePreviews.set(token, { planHash, expiresAt: Date.parse(expiresAt), consumed: false, result: null }); + respond(response, 200, `${JSON.stringify({ plan, planHash, token, expiresAt })}\n`); + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const approvedPreview = taskCreatePreviews.get(token); + if (!approvedPreview || approvedPreview.expiresAt <= Date.now()) { + respond(response, 409, JSON.stringify({ error: 'TASK_CREATE_PREVIEW_EXPIRED', message: 'Review a fresh task preview before creating work.' })); + return; + } + if (approvedPreview.consumed) { + if (approvedPreview.result && input.planHash === approvedPreview.planHash) { + respond(response, 201, `${JSON.stringify(approvedPreview.result)}\n`); + } else { + respond(response, 409, JSON.stringify({ error: 'TASK_CREATE_PREVIEW_EXPIRED', message: 'This task preview has already been used. Review a fresh proposal.' })); + } + return; + } + approvedPreview.consumed = true; + const result = withBacklog((service) => { + const current = service.planOwnerCreate(taskInput); + const planHash = priorityPlanHash(current); + if (planHash !== approvedPreview.planHash || planHash !== input.planHash) return { stale: true }; + return { task: service.createOwner(taskInput), mutationPerformed: true }; + }); + if (result.stale) { + respond(response, 409, JSON.stringify({ error: 'TASK_CREATE_PREVIEW_STALE', message: 'Task proposal inputs or repository evidence changed. Review a fresh preview.' })); + return; + } + approvedPreview.result = result; + respond(response, 201, `${JSON.stringify(result)}\n`); + } catch (error) { + const status = ['OWNER_AUTHORITY_REQUIRED', 'BACKLOG_AUTHORITY_REQUIRED'].includes(error.code) ? 403 + : ['BACKLOG_WORKTREE_MISSING', 'BACKLOG_TASK_NOT_FOUND'].includes(error.code) ? 409 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'TASK_CREATE_FAILED', message: error.message })}\n`); + } + return; + } + if (['/api/owner-requests', '/api/owner-requests/preview'].includes(url.pathname)) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + const recipient = typeof input.recipient === 'string' ? input.recipient.trim() : ''; + const body = typeof input.body === 'string' ? input.body.trim() : ''; + const taskId = typeof input.taskId === 'string' ? input.taskId.trim() : ''; + if (!recipient || !body) { + respond(response, 400, JSON.stringify({ error: 'OWNER_REQUEST_INPUT_REQUIRED', message: 'Choose an active identity and write a request.' })); + return; + } + if (body.length > 4000) { + respond(response, 400, JSON.stringify({ error: 'OWNER_REQUEST_TOO_LONG', message: 'Keep the request under 4,000 characters.' })); + return; + } + try { + const plan = ownerRequestPlan({ repositoryRoot, env, recipient, body, taskId: taskId || null }); + if (url.pathname.endsWith('/preview')) { + const planHash = priorityPlanHash(plan); + const now = Date.now(); + for (const [token, record] of ownerRequestPreviews) { + if (record.expiresAt <= now) ownerRequestPreviews.delete(token); + } + while (ownerRequestPreviews.size >= MAX_FEEDBACK_PREVIEWS) { + ownerRequestPreviews.delete(ownerRequestPreviews.keys().next().value); + } + const token = randomUUID(); + const expiresAt = new Date(now + OWNER_REQUEST_PREVIEW_TTL_MS).toISOString(); + ownerRequestPreviews.set(token, { planHash, expiresAt: Date.parse(expiresAt), consumed: false, result: null }); + respond(response, 200, `${JSON.stringify({ plan, planHash, token, expiresAt })}\n`); + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const preview = ownerRequestPreviews.get(token); + if (!preview || preview.expiresAt <= Date.now()) { + respond(response, 409, JSON.stringify({ error: 'OWNER_REQUEST_PREVIEW_EXPIRED', message: 'Review the recipient and exact request before sending.' })); + return; + } + if (preview.consumed) { + if (preview.result && input.planHash === preview.planHash) { + respond(response, 201, `${JSON.stringify(preview.result)}\n`); + } else { + respond(response, 409, JSON.stringify({ error: 'OWNER_REQUEST_PREVIEW_EXPIRED', message: 'This request preview has already been used. Review a fresh request.' })); + } + return; + } + const currentPlanHash = priorityPlanHash(plan); + if (currentPlanHash !== preview.planHash || currentPlanHash !== input.planHash) { + respond(response, 409, JSON.stringify({ error: 'OWNER_REQUEST_PREVIEW_STALE', message: 'The request, recipient, task, active organization, or repository changed after preview. Review it again.' })); + return; + } + preview.consumed = true; + const controlPlane = openControlPlane({ repositoryRoot, env }); + let message; + try { + message = controlPlane.sendOwnerRequest({ + actorId: plan.owner, recipient, body, + references: { task: taskId || null }, + }); + } finally { controlPlane.close(); } + const result = { message, mutationPerformed: true, effect: plan.effect }; + preview.result = result; + respond(response, 201, `${JSON.stringify(result)}\n`); + } catch (error) { + const status = error.code === 'OWNER_AUTHORITY_REQUIRED' ? 403 + : error.code?.includes('NOT_FOUND') ? 404 + : error.code?.includes('STATE_CONFLICT') ? 409 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'OWNER_REQUEST_FAILED', message: error.message })}\n`); + } + return; + } + const hierarchyActivation = url.pathname.match(/^\/api\/organization-proposals\/hierarchy\/([A-Za-z0-9._-]+)\/activate(\/preview)?$/); + if (hierarchyActivation) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + const [, id, previewPath] = hierarchyActivation; + try { + if (previewPath) { + const plan = hierarchyActivationPlan({ repositoryRoot, env, id }); + const planHash = priorityPlanHash(plan); + let token = null; + let expiresAt = null; + if (plan.pilotPlan.canProceed) { + const now = Date.now(); + for (const [candidate, record] of hierarchyActivationPreviews) { + if (record.expiresAt <= now) hierarchyActivationPreviews.delete(candidate); + } + while (hierarchyActivationPreviews.size >= MAX_FEEDBACK_PREVIEWS) { + hierarchyActivationPreviews.delete(hierarchyActivationPreviews.keys().next().value); + } + token = randomUUID(); + expiresAt = new Date(now + HIERARCHY_ACTIVATION_PREVIEW_TTL_MS).toISOString(); + hierarchyActivationPreviews.set(token, { + id, planHash, expiresAt: Date.parse(expiresAt), consumed: false, result: null, + }); + } + respond(response, 200, `${JSON.stringify({ plan, planHash, token, expiresAt })}\n`); + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const preview = hierarchyActivationPreviews.get(token); + if (!preview || preview.id !== id || preview.expiresAt <= Date.now()) { + respond(response, 409, JSON.stringify({ error: 'HIERARCHY_ACTIVATION_PREVIEW_EXPIRED', message: 'Review a fresh, owner-approved pilot plan before activating it.' })); + return; + } + if (preview.consumed) { + if (preview.result && input.planHash === preview.planHash) { + respond(response, 200, `${JSON.stringify(preview.result)}\n`); + } else { + respond(response, 409, JSON.stringify({ error: 'HIERARCHY_ACTIVATION_PREVIEW_EXPIRED', message: 'This activation review has already been used. Preview the current pilot again.' })); + } + return; + } + const plan = hierarchyActivationPlan({ repositoryRoot, env, id }); + const planHash = priorityPlanHash(plan); + if (!plan.pilotPlan.canProceed || planHash !== preview.planHash || planHash !== input.planHash) { + respond(response, 409, JSON.stringify({ error: 'HIERARCHY_ACTIVATION_PREVIEW_STALE', message: 'The pilot, active organization, repository, or work state changed after preview. Review a fresh plan.' })); + return; + } + preview.consumed = true; + const result = withOrganizationServices({ repositoryRoot, env }, ({ hierarchy }) => ({ + proposal: hierarchy.activatePilot({ proposalId: id, activatedBy: plan.ownerIdentity }), + plan, mutationPerformed: true, + })); + preview.result = result; + respond(response, 200, `${JSON.stringify(result)}\n`); + } catch (error) { + const status = ['HIERARCHY_OWNER_AUTHORITY_REQUIRED'].includes(error.code) ? 403 + : ['HIERARCHY_PILOT_BLOCKED', 'HIERARCHY_PROPOSAL_STATE_CONFLICT', 'HIERARCHY_ACTIVATION_COMMIT_FAILED'].includes(error.code) ? 409 + : error.code?.includes('NOT_FOUND') ? 404 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'HIERARCHY_ACTIVATION_FAILED', message: error.message, details: error.details ?? null })}\n`); + } + return; + } + const organizationReview = url.pathname.match(/^\/api\/organization-proposals\/(fleet|hierarchy|hierarchy-conclusion)\/([A-Za-z0-9._-]+)(\/preview)?$/); + if (organizationReview) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + const [, type, id, previewPath] = organizationReview; + const action = typeof input.action === 'string' ? input.action : ''; + const reason = typeof input.reason === 'string' ? input.reason.trim() : ''; + if (reason.length > 2000) { + respond(response, 400, JSON.stringify({ error: 'ORGANIZATION_REVIEW_REASON_TOO_LONG', message: 'Keep the decision reason under 2,000 characters.' })); + return; + } + try { + if (previewPath) { + const plan = organizationReviewPlan({ repositoryRoot, env, type, id, action, reason, clock }); + const planHash = priorityPlanHash(plan); + if (plan.canProceed === false) { + respond(response, 200, `${JSON.stringify({ plan, planHash, token: null, expiresAt: null })}\n`); + return; + } + const now = Date.now(); + for (const [token, record] of organizationPreviews) { + if (record.expiresAt <= now) organizationPreviews.delete(token); + } + while (organizationPreviews.size >= MAX_FEEDBACK_PREVIEWS) { + organizationPreviews.delete(organizationPreviews.keys().next().value); + } + const token = randomUUID(); + const expiresAt = new Date(now + ORGANIZATION_REVIEW_PREVIEW_TTL_MS).toISOString(); + organizationPreviews.set(token, { type, id, action, reason, planHash, expiresAt: Date.parse(expiresAt), consumed: false, result: null }); + respond(response, 200, `${JSON.stringify({ plan, planHash, token, expiresAt })}\n`); + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const preview = organizationPreviews.get(token); + if (!preview || preview.expiresAt <= Date.now()) { + respond(response, 409, JSON.stringify({ error: 'ORGANIZATION_REVIEW_PREVIEW_EXPIRED', message: 'Review a fresh organization proposal before deciding.' })); + return; + } + if (preview.type !== type || preview.id !== id || preview.action !== action || preview.reason !== reason) { + respond(response, 409, JSON.stringify({ error: 'ORGANIZATION_REVIEW_PREVIEW_STALE', message: 'The target, operation or decision differs from the reviewed plan. Preview the exact decision again.' })); + return; + } + if (preview.consumed) { + if (preview.result && input.planHash === preview.planHash) { + respond(response, 200, `${JSON.stringify(preview.result)}\n`); + } else { + respond(response, 409, JSON.stringify({ error: 'ORGANIZATION_REVIEW_PREVIEW_EXPIRED', message: 'This review has already been used. Preview the current proposal again.' })); + } + return; + } + const plan = organizationReviewPlan({ repositoryRoot, env, type, id, action, reason, clock }); + const planHash = priorityPlanHash(plan); + if (plan.canProceed === false || planHash !== preview.planHash || planHash !== input.planHash) { + respond(response, 409, JSON.stringify({ error: 'ORGANIZATION_REVIEW_PREVIEW_STALE', message: 'The proposal, active organization, or repository changed after preview. Review a fresh plan.' })); + return; + } + preview.consumed = true; + const result = withOrganizationServices({ repositoryRoot, env, clock }, ({ fleet, hierarchy }) => { + const decided = type === 'fleet' + ? action === 'approve' + ? fleet.approve({ changeId: id, approvedBy: plan.ownerIdentity }) + : fleet.reject({ changeId: id, rejectedBy: plan.ownerIdentity, reason }) + : type === 'hierarchy-conclusion' + ? hierarchy.concludePilot({ proposalId: id, decision: action, reason, decidedBy: plan.ownerIdentity, + approved: true, planHash: plan.conclusionPlanHash }) + : hierarchy.decide({ proposalId: id, decision: action, decidedBy: plan.ownerIdentity, reason }); + return { type, decision: action, proposal: decided, plan, mutationPerformed: true }; + }); + preview.result = result; + respond(response, 200, `${JSON.stringify(result)}\n`); + } catch (error) { + const status = error.code?.includes('AUTHORITY_REQUIRED') ? 403 + : error.code?.includes('NOT_FOUND') ? 404 + : error.code?.includes('STATE_CONFLICT') || error.code?.includes('CONCLUSION_') ? 409 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'ORGANIZATION_REVIEW_FAILED', message: error.message })}\n`); + } + return; + } + const priority = url.pathname.match(/^\/api\/backlog\/tasks\/(TASK-[A-Za-z0-9-]+)\/priority(\/preview)?$/); + if (priority) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + try { + const withBacklog = (callback) => { + const controlPlane = openControlPlane({ repositoryRoot, env }); + try { + const service = new BacklogService({ repositoryRoot, controlPlane }); + return callback(service); + } finally { controlPlane.close(); } + }; + const taskId = priority[1]; + if (priority[2]) { + const plan = withBacklog((service) => service.planPriorityChange({ + taskId, actorId: 'owner', expectedRevision: input.expectedRevision, + priority: input.priority, reason: input.reason, + })); + const planHash = priorityPlanHash(plan); + const now = Date.now(); + for (const [token, record] of priorityPreviews) { + if (record.expiresAt <= now) priorityPreviews.delete(token); + } + while (priorityPreviews.size >= MAX_FEEDBACK_PREVIEWS) { + priorityPreviews.delete(priorityPreviews.keys().next().value); + } + const token = randomUUID(); + const expiresAt = new Date(now + FEEDBACK_PREVIEW_TTL_MS).toISOString(); + priorityPreviews.set(token, { taskId, planHash, expiresAt: Date.parse(expiresAt) }); + respond(response, 200, `${JSON.stringify({ plan, planHash, token, expiresAt })}\n`); + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const approvedPreview = priorityPreviews.get(token); + if (approvedPreview) priorityPreviews.delete(token); + if (!approvedPreview || approvedPreview.expiresAt <= Date.now() || approvedPreview.taskId !== taskId) { + respond(response, 409, JSON.stringify({ error: 'PRIORITY_PREVIEW_EXPIRED', message: 'Preview the priority change again before applying it.' })); + return; + } + const result = withBacklog((service) => { + const current = service.planPriorityChange({ + taskId, actorId: 'owner', expectedRevision: input.expectedRevision, + priority: input.priority, reason: input.reason, + }); + const planHash = priorityPlanHash(current); + if (planHash !== approvedPreview.planHash || planHash !== input.planHash) return { stale: true }; + return { task: service.setPriority({ + taskId, actorId: 'owner', expectedRevision: input.expectedRevision, + priority: input.priority, reason: input.reason, + }) }; + }); + if (result.stale) { + respond(response, 409, JSON.stringify({ error: 'PRIORITY_PREVIEW_STALE', message: 'Task changed since preview. Review a fresh priority change.' })); + return; + } + respond(response, 200, `${JSON.stringify(result)}\n`); + } catch (error) { + const status = error.code === 'BACKLOG_TASK_NOT_FOUND' ? 404 + : ['OWNER_AUTHORITY_REQUIRED', 'SAME_ORIGIN_REQUIRED'].includes(error.code) ? 403 + : ['BACKLOG_REVISION_CONFLICT', 'PRIORITY_PREVIEW_EXPIRED'].includes(error.code) ? 409 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'PRIORITY_CHANGE_FAILED', message: error.message })}\n`); + } + return; + } + const approvalDecision = url.pathname.match(/^\/api\/approvals\/([a-f0-9-]{36})\/decision(\/preview)?$/i); + if (approvalDecision) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + const approvalId = approvalDecision[1]; + try { + if (approvalDecision[2]) { + const plan = approvalDecisionPlan({ repositoryRoot, env, approvalId, decision: input.decision, note: input.note }); + const planHash = priorityPlanHash(plan); + const now = Date.now(); + for (const [token, record] of approvalDecisionPreviews) { + if (record.expiresAt <= now) approvalDecisionPreviews.delete(token); + } + while (approvalDecisionPreviews.size >= MAX_FEEDBACK_PREVIEWS) { + approvalDecisionPreviews.delete(approvalDecisionPreviews.keys().next().value); + } + const token = randomUUID(); + const expiresAt = new Date(now + APPROVAL_DECISION_PREVIEW_TTL_MS).toISOString(); + approvalDecisionPreviews.set(token, { approvalId, planHash, expiresAt: Date.parse(expiresAt), consumed: false, result: null }); + respond(response, 200, `${JSON.stringify({ plan, planHash, token, expiresAt })}\n`); + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const approvedPreview = approvalDecisionPreviews.get(token); + if (!approvedPreview || approvedPreview.expiresAt <= Date.now() || approvedPreview.approvalId !== approvalId) { + respond(response, 409, JSON.stringify({ error: 'APPROVAL_DECISION_PREVIEW_EXPIRED', message: 'Preview the approval decision again before confirming.' })); + return; + } + if (approvedPreview.consumed) { + if (input.planHash === approvedPreview.planHash) { + respond(response, 200, `${JSON.stringify({ ...approvedPreview.result, replayed: true })}\n`); + } else { + respond(response, 409, JSON.stringify({ error: 'APPROVAL_DECISION_PREVIEW_STALE', message: 'This preview was already used for a different decision.' })); + } + return; + } + const current = approvalDecisionPlan({ repositoryRoot, env, approvalId, decision: input.decision, note: input.note }); + const currentHash = priorityPlanHash(current); + if (currentHash !== approvedPreview.planHash || currentHash !== input.planHash) { + respond(response, 409, JSON.stringify({ error: 'APPROVAL_DECISION_PREVIEW_STALE', message: 'The request or decision changed since preview. Review a fresh decision.' })); + return; + } + const controlPlane = openControlPlane({ repositoryRoot, env }); + let decided; + try { + decided = controlPlane.decideApproval({ + approvalId, decidedBy: 'owner', decision: current.decision, + note: current.note, expectedRevision: current.revision, + }); + } finally { controlPlane.close(); } + const result = { approval: decided, plan: current, mutationPerformed: true }; + approvedPreview.consumed = true; + approvedPreview.result = result; + respond(response, 200, `${JSON.stringify(result)}\n`); + } catch (error) { + const status = error.code === 'APPROVAL_NOT_FOUND' ? 404 + : ['OWNER_AUTHORITY_REQUIRED', 'APPROVER_AUTHORITY_REQUIRED', 'SAME_ORIGIN_REQUIRED'].includes(error.code) ? 403 + : ['APPROVAL_NOT_PENDING', 'APPROVAL_REVISION_CONFLICT', 'APPROVAL_DECISION_PREVIEW_STALE', 'APPROVAL_DECISION_PREVIEW_EXPIRED'].includes(error.code) ? 409 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'APPROVAL_DECISION_FAILED', message: error.message })}\n`); + } + return; + } + const feedback = url.pathname.match(/^\/api\/artifacts\/([a-f0-9-]{36})\/feedback(\/preview)?$/i); + if (feedback) { + if (request.method !== 'POST') { + respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); + return; + } + if (!sameOriginLoopback(request)) { + respond(response, 403, JSON.stringify({ error: 'SAME_ORIGIN_REQUIRED' })); + return; + } + let input; + try { input = await readJsonRequest(request); } catch (error) { + const status = error.code === 'REQUEST_TOO_LARGE' ? 413 + : error.code === 'REQUEST_CONTENT_TYPE_INVALID' ? 415 : 400; + respond(response, status, JSON.stringify({ error: error.code, message: error.message })); + return; + } + try { + const artifactId = feedback[1]; + if (feedback[2]) { + const preview = withArtifactService({ repositoryRoot, env }, (service) => + service.planComment({ artifactId, body: input.body, by: 'owner' })); + const planHash = feedbackPlanHash(preview); + const now = Date.now(); + for (const [token, record] of feedbackPreviews) { + if (record.expiresAt <= now) feedbackPreviews.delete(token); + } + while (feedbackPreviews.size >= MAX_FEEDBACK_PREVIEWS) { + feedbackPreviews.delete(feedbackPreviews.keys().next().value); + } + const token = randomUUID(); + const expiresAt = new Date(now + FEEDBACK_PREVIEW_TTL_MS).toISOString(); + feedbackPreviews.set(token, { artifactId, planHash, expiresAt: Date.parse(expiresAt) }); + respond(response, 200, `${JSON.stringify({ preview, planHash, token, expiresAt })}\n`); + return; + } + + const token = typeof input.token === 'string' ? input.token : ''; + const approvedPreview = feedbackPreviews.get(token); + if (approvedPreview) feedbackPreviews.delete(token); + if (!approvedPreview || approvedPreview.expiresAt <= Date.now() + || approvedPreview.artifactId !== artifactId) { + respond(response, 409, JSON.stringify({ error: 'FEEDBACK_PREVIEW_EXPIRED', message: 'Preview again before sending feedback.' })); + return; + } + const result = withArtifactService({ repositoryRoot, env }, (service) => { + const current = service.planComment({ artifactId, body: input.body, by: 'owner' }); + const planHash = feedbackPlanHash(current); + if (planHash !== approvedPreview.planHash || planHash !== input.planHash) { + return { stale: true }; + } + return { feedback: service.comment({ artifactId, body: input.body, by: 'owner' }) }; + }); + if (result.stale) { + respond(response, 409, JSON.stringify({ error: 'FEEDBACK_PREVIEW_STALE', message: 'Feedback or artifact details changed. Review a fresh preview before sending.' })); + return; + } + respond(response, 200, `${JSON.stringify(result)}\n`); + } catch (error) { + const status = error.code === 'ARTIFACT_NOT_FOUND' ? 404 + : error.code === 'OWNER_AUTHORITY_REQUIRED' ? 403 : 400; + respond(response, status, `${JSON.stringify({ error: error.code ?? 'FEEDBACK_FAILED', message: error.message })}\n`); + } + return; + } if (request.method !== 'GET') { respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); return; } + if (url.pathname === '/api/digest') { + if (request.method !== 'GET') { respond(response, 405, JSON.stringify({ error: 'METHOD_NOT_ALLOWED' })); return; } + try { respond(response, 200, `${JSON.stringify(observeProject({ repositoryRoot, env }).ownerDigest)}\n`); } + catch (error) { respond(response, 503, JSON.stringify({ error: error.code ?? 'OBSERVATION_FAILED' })); } + return; + } if (url.pathname === '/api/snapshot') { try { respond(response, 200, `${JSON.stringify(observeProject({ repositoryRoot, env }))}\n`); @@ -40,6 +1233,17 @@ export function createConsoleServer({ repositoryRoot = process.cwd(), env = proc } return; } + const artifact = url.pathname.match(/^\/api\/artifacts\/([a-f0-9-]{36})$/i); + if (artifact) { + try { + const image = readPublishedArtifact({ repositoryRoot, env, id: artifact[1] }); + respond(response, 200, image.bytes, image.mediaType); + } catch (error) { + respond(response, error.code === 'ARTIFACT_NOT_FOUND' ? 404 : 410, + JSON.stringify({ error: error.code ?? 'ARTIFACT_UNAVAILABLE', message: error.message })); + } + return; + } const file = ROUTES.get(url.pathname); if (!file) { respond(response, 404, 'Not found\n', 'text/plain; charset=utf-8'); diff --git a/src/control-plane/service.mjs b/src/control-plane/service.mjs index 7e58329..ba978bc 100644 --- a/src/control-plane/service.mjs +++ b/src/control-plane/service.mjs @@ -6,12 +6,16 @@ import { TorchError } from '../kernel/errors.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; import { projectStatePath } from '../kernel/paths.mjs'; import { loadProjectConfig } from '../kernel/config.mjs'; +import { organizationGraphFromConfig } from '../kernel/organization.mjs'; export const PRESENCE_STATES = Object.freeze([ 'starting', 'working', 'waiting', 'idle', 'stale', 'stopping', 'offline', ]); -const MESSAGE_KINDS = new Set(['message', 'coordination-request', 'handoff-request', 'completion', 'blocker']); +const MESSAGE_KINDS = new Set([ + 'message', 'coordination-request', 'handoff-request', 'completion', 'blocker', 'manager-check-in', 'manager-stale-work-review', + 'approval-request', 'approval-decision', +]); function readJson(path, code) { try { @@ -80,8 +84,9 @@ function rowToAgent(row, rosterAgent) { neighbours: rosterAgent.neighbours ?? [], requiredChecks: rosterAgent.required_checks ?? [], resources: rosterAgent.resources ?? [], - runtime: row?.runtime ?? null, + runtime: row?.runtime ?? rosterAgent.runtime ?? null, model: rosterAgent.model ?? null, + reasoning: rosterAgent.reasoning ?? null, runtimeSessionId: row?.runtime_session_id ?? null, state: row?.state ?? 'offline', summary: row?.summary ?? null, @@ -91,6 +96,21 @@ function rowToAgent(row, rosterAgent) { }; } +function effectiveRoster(roster, config) { + return (roster.areas ?? []).map((area) => { + const configured = area.id === 'session-manager' + ? config.session_manager : config.domains.find((domain) => domain.id === area.id); + const runtime = configured?.runtime ?? area.runtime ?? config.runtimes.default; + const runtimeConfig = config.runtimes[runtime] ?? {}; + return { + ...area, + runtime, + model: configured?.model ?? runtimeConfig.model ?? null, + reasoning: configured?.reasoning ?? runtimeConfig.reasoning ?? null, + }; + }); +} + function rowToMessage(row) { return { id: row.id, @@ -177,6 +197,26 @@ function initializeSchema(database) { details TEXT, created_at TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS approval_requests ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + requester_id TEXT NOT NULL, + approver_id TEXT NOT NULL, + task_ref TEXT, + title TEXT NOT NULL, + summary TEXT NOT NULL, + evidence TEXT, + status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled')), + revision INTEGER NOT NULL, + created_at TEXT NOT NULL, + decided_at TEXT, + decided_by TEXT, + decision_note TEXT + ); + CREATE INDEX IF NOT EXISTS approval_requests_pending_approver + ON approval_requests(project_id, approver_id, status, created_at, id); + CREATE INDEX IF NOT EXISTS approval_requests_pending_requester + ON approval_requests(project_id, requester_id, status, created_at, id); CREATE INDEX IF NOT EXISTS messages_recipient_created ON messages(recipient_id, created_at, id); PRAGMA user_version = 2; @@ -215,7 +255,7 @@ export class ControlPlane { } this.stateRoot = expectedStateRoot; this.projectId = this.manifest.projectId; - this.agents = new Map((this.roster.areas ?? []).map((area) => [area.id, area])); + this.agents = new Map(effectiveRoster(this.roster, this.config).map((area) => [area.id, area])); if (!this.agents.has('session-manager')) { throw new TorchError('Roster does not contain the required session-manager identity', { code: 'INVALID_TORCH_ROSTER', @@ -229,7 +269,8 @@ export class ControlPlane { refreshRoster() { const roster = readJson(join(this.repositoryRoot, '.torch', 'roster.yaml'), 'INVALID_TORCH_ROSTER'); - const agents = new Map((roster.areas ?? []).map((area) => [area.id, area])); + const config = loadProjectConfig(this.repositoryRoot); + const agents = new Map(effectiveRoster(roster, config).map((area) => [area.id, area])); if (!agents.has('session-manager')) { throw new TorchError('Roster does not contain the required session-manager identity', { code: 'INVALID_TORCH_ROSTER', @@ -353,6 +394,37 @@ export class ControlPlane { return record; } + sendOwnerRequest({ actorId, recipient, body, references = {} } = {}) { + const ownerId = this.assertOwnerActor(actorId); + const recipientId = this.assertIdentity(recipient); + const normalizedBody = requiredText(body, 'body'); + if (normalizedBody.length > 4000) { + throw new TorchError('Owner requests are limited to 4,000 characters', { + code: 'OWNER_REQUEST_TOO_LONG', details: { maximum: 4000 }, + }); + } + const refs = normalizeReferences(references); + const record = { + id: this.idFactory(), projectId: this.projectId, sender: ownerId, recipient: recipientId, + kind: 'message', createdAt: this.clock().toISOString(), body: normalizedBody, + acknowledgedAt: null, deliveredAt: null, references: refs, + }; + this.database.prepare(` + INSERT INTO messages ( + id, project_id, sender_id, recipient_id, kind, created_at, body, + task_ref, path_ref, commit_ref, handoff_ref + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + record.id, record.projectId, record.sender, record.recipient, record.kind, + record.createdAt, record.body, refs.task, refs.path, refs.commit, refs.handoff, + ); + this.auditOwnerAction({ + actorId: ownerId, operation: 'agent.request', entityType: 'message', entityId: record.id, + details: { recipient: recipientId, task: refs.task }, + }); + return record; + } + readMessages({ recipient, unacknowledgedOnly = false, limit = 100 } = {}) { const recipientId = this.assertIdentity(recipient); const normalizedLimit = Number.isInteger(limit) && limit > 0 && limit <= 1000 ? limit : 100; @@ -503,6 +575,162 @@ export class ControlPlane { } } + requestApproval({ requester, approver, task, title, summary, evidence } = {}) { + const requesterId = this.assertIdentity(requester); + const approverId = requiredText(approver, 'approver'); + if (approverId === 'owner') this.assertOwnerActor(approverId); + else this.assertIdentity(approverId); + if (requesterId === approverId) { + throw new TorchError('An identity cannot request its own approval', { code: 'INVALID_APPROVAL_REQUEST' }); + } + const normalizedTitle = requiredText(title, 'title'); + const normalizedSummary = requiredText(summary, 'summary'); + const normalizedEvidence = optionalText(evidence, 'evidence'); + if (normalizedTitle.length > 240 || normalizedSummary.length > 4000 || (normalizedEvidence?.length ?? 0) > 4000) { + throw new TorchError('Approval title, summary, or evidence exceeds its size limit', { + code: 'APPROVAL_REQUEST_TOO_LONG', details: { title: 240, summary: 4000, evidence: 4000 }, + }); + } + const taskRef = optionalText(task, 'task'); + const record = { + id: this.idFactory(), projectId: this.projectId, requester: requesterId, approver: approverId, + task: taskRef, title: normalizedTitle, summary: normalizedSummary, evidence: normalizedEvidence, + status: 'pending', revision: 1, createdAt: this.clock().toISOString(), + decidedAt: null, decidedBy: null, decisionNote: null, + }; + this.database.exec('BEGIN IMMEDIATE'); + try { + this.database.prepare(` + INSERT INTO approval_requests ( + id, project_id, requester_id, approver_id, task_ref, title, summary, evidence, + status, revision, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + record.id, record.projectId, record.requester, record.approver, record.task, record.title, + record.summary, record.evidence, record.status, record.revision, record.createdAt, + ); + if (approverId !== 'owner') { + this.sendMessage({ + sender: requesterId, recipient: approverId, kind: 'approval-request', + body: `${record.title}: ${record.summary}`, references: { task: taskRef }, + }); + } + this.audit({ actorId: requesterId, operation: 'approval.request', entityType: 'approval', entityId: record.id }); + this.database.exec('COMMIT'); + return record; + } catch (error) { + this.database.exec('ROLLBACK'); + throw error; + } + } + + listApprovals({ actorId, status } = {}) { + const actor = requiredText(actorId, 'actorId'); + const isOwner = actor === 'owner'; + if (isOwner) this.assertOwnerActor(actor); + else this.assertIdentity(actor); + const statusClause = status ? 'AND status = ?' : ''; + const params = isOwner + ? (status ? [this.projectId, status] : [this.projectId]) + : (status ? [this.projectId, actor, actor, status] : [this.projectId, actor, actor]); + const actorClause = isOwner ? '' : 'AND (requester_id = ? OR approver_id = ?)'; + return this.database.prepare(` + SELECT * FROM approval_requests WHERE project_id = ? ${actorClause} ${statusClause} + ORDER BY CASE status WHEN 'pending' THEN 0 ELSE 1 END, created_at DESC, id DESC + `).all(...params).map((row) => this.rowToApproval(row)); + } + + pendingApprovalsFor(requesterIds = []) { + const ids = [...new Set(requesterIds.map((id) => this.assertIdentity(id)))]; + if (!ids.length) return []; + const placeholders = ids.map(() => '?').join(','); + return this.database.prepare(` + SELECT * FROM approval_requests WHERE project_id = ? AND status = 'pending' + AND requester_id IN (${placeholders}) ORDER BY created_at ASC, id ASC + `).all(this.projectId, ...ids).map((row) => this.rowToApproval(row)); + } + + decideApproval({ approvalId, decidedBy, decision, note, expectedRevision } = {}) { + const id = requiredText(approvalId, 'approvalId'); + const actor = requiredText(decidedBy, 'decidedBy'); + const owner = actor === 'owner'; + if (owner) this.assertOwnerActor(actor); + else this.assertIdentity(actor); + if (!['approved', 'rejected'].includes(decision)) { + throw new TorchError('Approval decision must be approved or rejected', { code: 'INVALID_APPROVAL_DECISION' }); + } + const normalizedNote = optionalText(note, 'note'); + if (normalizedNote && normalizedNote.length > 2000) { + throw new TorchError('Approval decision note is limited to 2,000 characters', { code: 'APPROVAL_NOTE_TOO_LONG' }); + } + this.database.exec('BEGIN IMMEDIATE'); + try { + const current = this.database.prepare('SELECT * FROM approval_requests WHERE id = ? AND project_id = ?').get(id, this.projectId); + if (!current) throw new TorchError(`Unknown approval request: ${id}`, { code: 'APPROVAL_NOT_FOUND' }); + if (current.status !== 'pending') throw new TorchError(`Approval request ${id} is already ${current.status}`, { code: 'APPROVAL_NOT_PENDING' }); + if (current.approver_id !== actor) throw new TorchError('Only the named approver may decide this request', { code: 'APPROVER_AUTHORITY_REQUIRED' }); + if (!Number.isInteger(expectedRevision) || expectedRevision < 1) { + throw new TorchError('Approval decisions require the current request revision', { code: 'APPROVAL_REVISION_REQUIRED' }); + } + if (current.revision !== expectedRevision) { + throw new TorchError('Approval request changed since it was read', { code: 'APPROVAL_REVISION_CONFLICT' }); + } + const decidedAt = this.clock().toISOString(); + const nextRevision = current.revision + 1; + this.database.prepare(` + UPDATE approval_requests SET status = ?, revision = ?, decided_at = ?, decided_by = ?, decision_note = ? + WHERE id = ? AND status = 'pending' AND revision = ? + `).run(decision, nextRevision, decidedAt, actor, normalizedNote, id, current.revision); + const details = { status: decision, requester: current.requester_id, approver: actor, note: normalizedNote }; + if (owner) { + this.auditOwnerAction({ actorId: actor, operation: 'approval.decide', entityType: 'approval', entityId: id, details }); + } else { + this.audit({ actorId: actor, operation: 'approval.decide', entityType: 'approval', entityId: id, details }); + } + if (owner) { + this.insertOwnerApprovalDecisionMessage({ + sender: actor, recipient: current.requester_id, approvalId: id, + task: current.task_ref, decision, note: normalizedNote, + }); + } else { + this.sendMessage({ + sender: actor, recipient: current.requester_id, kind: 'approval-decision', + body: `Approval ${decision}: ${normalizedNote ?? 'No additional note.'}`, + references: { task: current.task_ref }, + }); + } + this.database.exec('COMMIT'); + return this.rowToApproval(this.database.prepare('SELECT * FROM approval_requests WHERE id = ?').get(id)); + } catch (error) { + this.database.exec('ROLLBACK'); + throw error; + } + } + + insertOwnerApprovalDecisionMessage({ sender, recipient, approvalId, task, decision, note }) { + const id = this.idFactory(); + const createdAt = this.clock().toISOString(); + const body = `Approval ${decision}: ${note ?? 'No additional note.'}`; + this.database.prepare(` + INSERT INTO messages (id, project_id, sender_id, recipient_id, kind, created_at, body, task_ref) + VALUES (?, ?, ?, ?, 'approval-decision', ?, ?, ?) + `).run(id, this.projectId, sender, recipient, createdAt, body, task ?? null); + this.auditOwnerAction({ + actorId: sender, operation: 'approval.notify', entityType: 'message', entityId: id, + details: { approvalId, recipient }, + }); + } + + rowToApproval(row) { + return { + id: row.id, projectId: row.project_id, requester: row.requester_id, approver: row.approver_id, + task: row.task_ref ?? null, title: row.title, summary: row.summary, evidence: row.evidence ?? null, + status: row.status, revision: row.revision, createdAt: row.created_at, + decidedAt: row.decided_at ?? null, decidedBy: row.decided_by ?? null, + decisionNote: row.decision_note ?? null, + }; + } + audit({ actorId, operation, entityType, entityId = null, details = null } = {}) { const actor = this.assertIdentity(actorId); const event = { @@ -521,6 +749,40 @@ export class ControlPlane { return event; } + auditOwnerAction({ actorId, operation, entityType, entityId = null, details = null } = {}) { + const actor = this.assertOwnerActor(actorId); + if (!['artifact.feedback', 'backlog.priority', 'backlog.create', 'agent.request', 'approval.decide', 'approval.notify', 'forge.sync', 'canonical.fetch', 'profile.change', 'schedule.launcher.install', 'schedule.launcher.reconcile', 'schedule.wake.recover', 'hierarchy.conclude-pilot', 'check.recover-prepared'].includes(operation)) { + throw new TorchError('This owner audit surface does not authorize the requested operation', { + code: 'OWNER_OPERATION_UNSUPPORTED', details: { operation }, + }); + } + const event = { + id: this.idFactory(), projectId: this.projectId, actorId: actor, + operation: requiredText(operation, 'operation'), entityType: requiredText(entityType, 'entityType'), + entityId: optionalText(entityId, 'entityId'), details, createdAt: this.clock().toISOString(), + }; + this.database.prepare(` + INSERT INTO audit_events ( + id, project_id, actor_id, operation, entity_type, entity_id, details, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).run( + event.id, event.projectId, event.actorId, event.operation, event.entityType, + event.entityId, details ? JSON.stringify(details) : null, event.createdAt, + ); + return event; + } + + assertOwnerActor(actorId) { + const ownerRole = organizationGraphFromConfig(this.config).roles.find((role) => role.kind === 'owner'); + const actor = requiredText(actorId, 'actorId'); + if (!ownerRole || actor !== ownerRole.identity_id) { + throw new TorchError('Only the configured project owner may record this action', { + code: 'OWNER_AUTHORITY_REQUIRED', details: { actorId: actor }, + }); + } + return actor; + } + readAudit({ actorId, limit = 100 } = {}) { const normalizedLimit = Number.isInteger(limit) && limit > 0 && limit <= 1000 ? limit : 100; const rows = actorId diff --git a/src/delivery/attempts.mjs b/src/delivery/attempts.mjs new file mode 100644 index 0000000..4ac2b5a --- /dev/null +++ b/src/delivery/attempts.mjs @@ -0,0 +1,117 @@ +import { TorchError } from '../kernel/errors.mjs'; +import { fileHash } from '../kernel/files.mjs'; +import { join } from 'node:path'; +import { createHash } from 'node:crypto'; + +export function deliveryAdapterConfigHash(value) { + const ordered = (item) => Array.isArray(item) ? item.map(ordered) + : item && typeof item === 'object' ? Object.fromEntries(Object.entries(item) + .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, nested]) => [key, ordered(nested)])) : item; + return createHash('sha256').update(JSON.stringify(ordered(value))).digest('hex'); +} + +export function initializeDeliveryAttempts(database) { + database.exec(` + CREATE TABLE IF NOT EXISTS delivery_operations ( + id TEXT PRIMARY KEY, delivery_id TEXT NOT NULL REFERENCES deliveries(id), + commit_sha TEXT NOT NULL, from_state TEXT NOT NULL, target_state TEXT NOT NULL, + actor TEXT NOT NULL, provider TEXT NOT NULL, operation TEXT NOT NULL, + policy_hash TEXT NOT NULL, adapter_config_hash TEXT, state TEXT NOT NULL, created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, evidence_json TEXT NOT NULL + ); + CREATE UNIQUE INDEX IF NOT EXISTS delivery_unresolved_operation + ON delivery_operations(delivery_id) WHERE state IN ('running', 'unknown', 'succeeded'); + CREATE TABLE IF NOT EXISTS delivery_attempts ( + id TEXT PRIMARY KEY, operation_id TEXT NOT NULL REFERENCES delivery_operations(id), + ordinal INTEGER NOT NULL, state TEXT NOT NULL, started_at TEXT NOT NULL, + finished_at TEXT, receipt_json TEXT, UNIQUE(operation_id, ordinal) + ); + `); + const columns = database.prepare('PRAGMA table_info(delivery_operations)').all(); + if (!columns.some((column) => column.name === 'adapter_config_hash')) database.exec('ALTER TABLE delivery_operations ADD COLUMN adapter_config_hash TEXT'); +} + +export function unresolvedDeliveryOperation(database, deliveryId) { + return database.prepare("SELECT id, state FROM delivery_operations WHERE delivery_id = ? AND state IN ('running', 'unknown', 'succeeded')").get(deliveryId); +} + +export function deliveryOperations(database, deliveryId) { + return database.prepare(`SELECT id, delivery_id AS deliveryId, commit_sha AS "commit", from_state AS fromState, + target_state AS targetState, actor, provider, operation, policy_hash AS policyHash, state, + created_at AS createdAt, updated_at AS updatedAt FROM delivery_operations + ${deliveryId ? 'WHERE delivery_id = ?' : ''} ORDER BY rowid`).all(...(deliveryId ? [deliveryId] : [])); +} + +export function deliveryAttempts(database, deliveryId) { + return database.prepare(`SELECT a.id, a.operation_id AS operationId, o.delivery_id AS deliveryId, + o.commit_sha AS "commit", o.provider, o.operation, a.ordinal, a.state, + a.started_at AS startedAt, a.finished_at AS finishedAt, a.receipt_json AS receiptJson + FROM delivery_attempts a JOIN delivery_operations o ON o.id = a.operation_id + ${deliveryId ? 'WHERE o.delivery_id = ?' : ''} ORDER BY a.rowid`).all(...(deliveryId ? [deliveryId] : [])) + .map(({ receiptJson, ...row }) => ({ ...row, receipt: receiptJson ? JSON.parse(receiptJson) : null })); +} + +// Persist the reservation and each attempt before external execution. A crash +// leaves an unresolved row, preventing another caller from repeating effects. +export function executeDeliveryOperation({ database, repositoryRoot, plan, delivery, adapter, + evidence, clock, idFactory, maxAttempts = 1 }) { + const path = join(repositoryRoot, '.torch', 'torch.yaml'); + const policyHash = plan.policyHash; + if (fileHash(path) !== policyHash) throw new TorchError('Delivery policy changed since approval planning', { code: 'DELIVERY_POLICY_CHANGED' }); + const operationId = idFactory(); + const now = clock().toISOString(); + database.exec('BEGIN IMMEDIATE'); + try { + const current = database.prepare('SELECT state FROM deliveries WHERE id = ?').get(delivery.id); + if (current?.state !== delivery.state || unresolvedDeliveryOperation(database, delivery.id)) { + throw new TorchError('Another delivery operation or changed state blocks execution', { code: 'DELIVERY_OPERATION_UNRESOLVED' }); + } + database.prepare(`INSERT INTO delivery_operations + (id, delivery_id, commit_sha, from_state, target_state, actor, provider, operation, + policy_hash, adapter_config_hash, state, created_at, updated_at, evidence_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?)`).run(operationId, delivery.id, + delivery.commit, delivery.state, plan.to, plan.actor, plan.adapter.provider, + plan.adapter.operation, policyHash, plan.adapterConfigHash, now, now, JSON.stringify(evidence)); + database.exec('COMMIT'); + } catch (error) { database.exec('ROLLBACK'); throw error; } + const finish = (state) => database.prepare('UPDATE delivery_operations SET state = ?, updated_at = ? WHERE id = ?') + .run(state, clock().toISOString(), operationId); + const safety = adapter.retrySafety?.[plan.adapter.operation] ?? 'never'; + for (let ordinal = 1; ordinal <= maxAttempts; ordinal += 1) { + if (fileHash(path) !== policyHash) { + finish('failed'); + throw new TorchError('Approved delivery policy changed before another attempt', { code: 'DELIVERY_POLICY_CHANGED', details: { operationId } }); + } + const attemptId = idFactory(); + database.prepare(`INSERT INTO delivery_attempts (id, operation_id, ordinal, state, started_at) + VALUES (?, ?, ?, 'running', ?)`).run(attemptId, operationId, ordinal, clock().toISOString()); + let receipt; + let state = 'unknown'; + try { + receipt = adapter[plan.adapter.operation]({ delivery, evidence, idempotencyKey: operationId, attempt: ordinal }); + // Async adapters are not supported by this synchronous boundary. Never + // infer success from a Promise or unbounded/malformed response. + if (typeof receipt?.then === 'function') { + Promise.resolve(receipt).catch(() => {}); + throw new Error('Async receipt unsupported'); + } + const serialized = JSON.stringify(receipt); + if (!serialized || serialized.length > 65_536 || typeof receipt?.then === 'function') throw new Error('Invalid receipt'); + if (receipt.status === 'succeeded' && typeof receipt.reference === 'string' && receipt.reference.trim()) state = 'succeeded'; + else if (receipt.status === 'failed' && ['transient', 'permanent'].includes(receipt.failure?.classification) + && ['not-applied', 'unknown'].includes(receipt.failure?.effects)) { + state = receipt.failure.effects === 'unknown' ? 'unknown' : 'failed'; + } else receipt = { status: 'unknown', reason: 'invalid-adapter-receipt' }; + } catch { receipt = { status: 'unknown', reason: 'adapter-threw-or-invalid-receipt' }; } + database.prepare('UPDATE delivery_attempts SET state = ?, finished_at = ?, receipt_json = ? WHERE id = ?') + .run(state, clock().toISOString(), JSON.stringify(receipt), attemptId); + if (state === 'succeeded') { finish('succeeded'); return { operationId, receipt }; } + const retry = state === 'failed' && receipt.failure.classification === 'transient' + && ['idempotent', 'read-only'].includes(safety) && ordinal < maxAttempts; + if (retry) continue; + finish(state); + throw new TorchError('Delivery adapter failed; inspect durable attempts before retrying', { + code: 'DELIVERY_ADAPTER_FAILED', details: { operationId, attemptId, state, retryable: false }, + }); + } +} diff --git a/src/delivery/service.mjs b/src/delivery/service.mjs index f8f9751..6a2f737 100644 --- a/src/delivery/service.mjs +++ b/src/delivery/service.mjs @@ -7,6 +7,8 @@ import { loadProjectConfig } from '../kernel/config.mjs'; import { readInstallManifest, writeInstallManifest } from '../kernel/install.mjs'; import { fileHash } from '../kernel/files.mjs'; import { validateDeliveryAdapter } from '../adapters/delivery.mjs'; +import { initializeDeliveryAttempts, unresolvedDeliveryOperation, deliveryAttempts, + deliveryOperations, executeDeliveryOperation, deliveryAdapterConfigHash } from './attempts.mjs'; export const DELIVERY_STATES = Object.freeze([ 'implemented', 'verified', 'integrated', 'release-ready', 'released', 'deployed', 'live-verified', @@ -163,6 +165,89 @@ export class DeliveryService { this.worktrees = new Map((this.manifest.external ?? []) .filter((entry) => entry.type === 'worktree').map((entry) => [entry.area, entry])); initialize(this.database); + initializeDeliveryAttempts(this.database); + } + + attempts({ deliveryId } = {}) { return deliveryAttempts(this.database, deliveryId); } + operations({ deliveryId } = {}) { return deliveryOperations(this.database, deliveryId); } + + planSucceededRecovery({ operationId, actor } = {}) { + if (actor !== 'owner') throw new TorchError('Only the owner may reconcile external success', { code: 'DELIVERY_AUTHORITY_REQUIRED' }); + const operation = this.database.prepare('SELECT * FROM delivery_operations WHERE id = ?').get(text(operationId, 'operationId')); + if (!operation) throw new TorchError('Unknown delivery operation', { code: 'DELIVERY_RECOVERY_BLOCKED' }); + const delivery = this.get(operation.delivery_id); + const config = loadProjectConfig(this.repositoryRoot); + const blockers = []; + const unchanged = operation.state === 'applied' && delivery.commit === operation.commit_sha + && DELIVERY_STATES.indexOf(delivery.state) >= DELIVERY_STATES.indexOf(operation.target_state); + const requirement = adapterRequirement(config, operation.target_state); + const attempt = this.database.prepare('SELECT * FROM delivery_attempts WHERE operation_id = ? ORDER BY ordinal DESC LIMIT 1').get(operation.id); + let receipt = null; + try { receipt = JSON.parse(attempt?.receipt_json ?? 'null'); } catch { /* Invalid persisted evidence fails closed below. */ } + if (!unchanged) { + if (!['running', 'succeeded'].includes(operation.state) || !attempt || attempt.state !== 'succeeded' + || receipt?.status !== 'succeeded' || typeof receipt.reference !== 'string' || !receipt.reference.trim() + || (receipt.commit && receipt.commit !== delivery.commit)) blockers.push({ code: 'DELIVERY_SUCCESS_RECEIPT_REQUIRED' }); + if (delivery.commit !== operation.commit_sha || delivery.state !== operation.from_state + || DELIVERY_STATES[DELIVERY_STATES.indexOf(delivery.state) + 1] !== operation.target_state) { + blockers.push({ code: 'DELIVERY_STATE_CONFLICT' }); + } + const authority = config.delivery.authority[operation.target_state.replaceAll('-', '_')] ?? []; + if (!authority.includes('owner')) blockers.push({ code: 'DELIVERY_AUTHORITY_REQUIRED' }); + const sameDestination = requirement && requirement.provider === operation.provider && requirement.operation === operation.operation + && (operation.adapter_config_hash ? deliveryAdapterConfigHash(config.delivery.adapters[requirement.slot]) === operation.adapter_config_hash + : fileHash(configurationPaths(this.repositoryRoot).config) === operation.policy_hash); + if (!sameDestination) blockers.push({ code: 'DELIVERY_RECOVERY_DESTINATION_CHANGED' }); + } + return { action: 'reconcile-succeeded-delivery', operationId: operation.id, deliveryId: delivery.id, + commit: delivery.commit, from: delivery.state, to: operation.target_state, actor: 'owner', + adapter: requirement, receipt, unchanged, blockers, canProceed: !blockers.length, + mutationPerformed: false, executorInvoked: false }; + } + + reconcileSucceeded({ operationId, actor, approved = false, runtimeStopped = false, evidence } = {}) { + if (actor !== 'owner') throw new TorchError('Only the owner may reconcile external success', { code: 'DELIVERY_AUTHORITY_REQUIRED' }); + if (!approved || !runtimeStopped) throw new TorchError('Success recovery requires approval and a stopped executor', { code: 'APPROVAL_REQUIRED' }); + const proof = textList(evidence, 'evidence'); + this.database.exec('BEGIN IMMEDIATE'); + try { + const plan = this.planSucceededRecovery({ operationId, actor }); + if (!plan.canProceed) throw new TorchError('Saved external success cannot be reconciled safely', { code: 'DELIVERY_RECOVERY_BLOCKED', details: plan.blockers }); + if (!plan.unchanged) { + const operation = this.database.prepare('SELECT evidence_json FROM delivery_operations WHERE id = ?').get(operationId); + this.#applyTransition({ plan, delivery: this.get(plan.deliveryId), + evidenceList: [...new Set([...JSON.parse(operation.evidence_json), ...proof])], + receipt: plan.receipt, operationId, approved }); + this.controlPlane.audit({ actorId: 'session-manager', operation: 'delivery.owner-reconciled-success', + entityType: 'delivery-operation', entityId: operationId, + details: { approvedBy: 'owner', runtimeStopped, evidence: proof, provenance: 'saved-adapter-receipt-owner-reviewed' } }); + } + this.database.exec('COMMIT'); + return { ...plan, delivery: this.get(plan.deliveryId), mutationPerformed: !plan.unchanged, executorInvoked: false }; + } catch (error) { this.database.exec('ROLLBACK'); throw error; } + } + + recoverNotApplied({ operationId, actor, approved = false, runtimeStopped = false, evidence } = {}) { + if (actor !== 'owner') throw new TorchError('Only the owner can attest an uncertain delivery outcome', { code: 'DELIVERY_AUTHORITY_REQUIRED' }); + if (!approved || !runtimeStopped) throw new TorchError('Recovery requires approval and confirmation that the executor is stopped', { + code: 'APPROVAL_REQUIRED', + }); + const proof = textList(evidence, 'evidence'); + this.database.exec('BEGIN IMMEDIATE'); + try { + const operation = this.database.prepare('SELECT * FROM delivery_operations WHERE id = ?').get(text(operationId, 'operationId')); + if (!operation || !['unknown', 'running'].includes(operation.state)) throw new TorchError('Only uncertain operations may be attested not applied', { + code: 'DELIVERY_RECOVERY_BLOCKED', + }); + this.database.prepare("UPDATE delivery_operations SET state = 'reviewed-not-applied', updated_at = ?, evidence_json = ? WHERE id = ?") + .run(this.clock().toISOString(), JSON.stringify([...JSON.parse(operation.evidence_json), ...proof]), operation.id); + this.controlPlane.audit({ actorId: 'session-manager', operation: 'delivery.owner-reviewed-not-applied', + entityType: 'delivery-operation', entityId: operation.id, + details: { approvedBy: 'owner', runtimeStopped, evidence: proof, provenance: 'owner-attested-not-applied' } }); + this.database.exec('COMMIT'); + return { operationId: operation.id, state: 'reviewed-not-applied', provenance: 'owner-attested-not-applied', + deliveryStateChanged: false, executorInvoked: false }; + } catch (error) { this.database.exec('ROLLBACK'); throw error; } } list({ state } = {}) { @@ -221,6 +306,8 @@ export class DeliveryService { } planTransition({ deliveryId, targetState, actor } = {}) { + const policyHash = fileHash(configurationPaths(this.repositoryRoot).config); + this.config = loadProjectConfig(this.repositoryRoot); const delivery = this.get(deliveryId); const target = text(targetState, 'targetState'); const currentIndex = DELIVERY_STATES.indexOf(delivery.state); @@ -235,6 +322,8 @@ export class DeliveryService { const configuredAuthority = this.config.delivery.authority[authorityKey] ?? []; const authorizedActors = configuredAuthority.map((entry) => entry === '$source' ? delivery.sourceArea : entry); const blockers = []; + const pending = unresolvedDeliveryOperation(this.database, delivery.id); + if (pending) blockers.push({ code: 'DELIVERY_OPERATION_UNRESOLVED', operationId: pending.id, state: pending.state }); if (!authorizedActors.includes(principal)) blockers.push({ code: 'DELIVERY_AUTHORITY_REQUIRED', actor: principal, allowed: authorizedActors }); if (target === 'verified') { const requiredChecks = this.config.integration.required_checks; @@ -265,12 +354,14 @@ export class DeliveryService { } } } + if (fileHash(configurationPaths(this.repositoryRoot).config) !== policyHash) blockers.push({ code: 'DELIVERY_POLICY_CHANGED' }); return { action: 'delivery-transition', deliveryId: delivery.id, from: delivery.state, to: target, actor: principal, authority: authorizedActors, adapter: requirement ? { slot: requirement.slot, provider: requirement.provider, operation: requirement.operation, } : null, - requiresApproval: HIGH_IMPACT_STATES.has(target), blockers, + requiresApproval: HIGH_IMPACT_STATES.has(target), policyHash, + adapterConfigHash: requirement ? deliveryAdapterConfigHash(this.config.delivery.adapters[requirement.slot]) : null, blockers, canProceed: blockers.length === 0, mutationPerformed: false, }; } @@ -286,13 +377,22 @@ export class DeliveryService { const evidenceList = textList(evidence, 'evidence'); const delivery = this.get(deliveryId); let receipt = null; + let operationId = null; if (plan.adapter) { const adapter = this.adapters.get(plan.adapter.provider); - receipt = adapter[plan.adapter.operation]({ delivery, evidence: evidenceList }); - if (!receipt || receipt.status !== 'succeeded') throw new TorchError('Delivery adapter did not return a successful receipt', { - code: 'DELIVERY_ADAPTER_FAILED', details: { adapter: plan.adapter, receipt }, - }); + ({ receipt, operationId } = executeDeliveryOperation({ database: this.database, + repositoryRoot: this.repositoryRoot, plan, delivery, adapter, evidence: evidenceList, + clock: this.clock, idFactory: this.idFactory, maxAttempts: this.config.delivery.retry?.max_attempts ?? 1 })); } + this.database.exec('BEGIN IMMEDIATE'); + try { + this.#applyTransition({ plan, delivery, evidenceList, receipt, operationId, approved }); + this.database.exec('COMMIT'); + } catch (error) { this.database.exec('ROLLBACK'); throw error; } + return this.get(delivery.id); + } + + #applyTransition({ plan, delivery, evidenceList, receipt, operationId, approved }) { const now = this.clock().toISOString(); const receipts = [...delivery.adapterReceipts, ...(receipt ? [{ ...receipt, operation: plan.adapter.operation }] : [])]; const allEvidence = [...new Set([...delivery.evidence, ...evidenceList])]; @@ -312,6 +412,7 @@ export class DeliveryService { operation: `delivery.${plan.to}`, entityType: 'delivery', entityId: delivery.id, details: { commit: delivery.commit, approved, adapter: plan.adapter }, }); - return this.get(delivery.id); + if (operationId) this.database.prepare("UPDATE delivery_operations SET state = 'applied', updated_at = ? WHERE id = ?") + .run(now, operationId); } } diff --git a/src/design/architect.mjs b/src/design/architect.mjs index 1dee2ad..2728234 100644 --- a/src/design/architect.mjs +++ b/src/design/architect.mjs @@ -1,6 +1,9 @@ import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { TorchError } from '../kernel/errors.mjs'; +import { workingTreeFingerprint } from '../kernel/git.mjs'; +import { validateSpecificationEvidence } from '../kernel/specifications.mjs'; +import { primaryOwnershipProblems } from '../kernel/domains.mjs'; const PROVIDERS = new Set(['claude', 'codex']); @@ -101,6 +104,13 @@ function requireStringArray(value, label, problems, { nonempty = false } = {}) { return value; } +function rejectUnknownFields(value, allowed, label, problems) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return; + for (const field of Object.keys(value)) { + if (!allowed.includes(field)) problems.push(`${label} has unsupported field: ${field}`); + } +} + export function createArchitectPrompt(brief) { if (brief?.schema !== 'torch.dev/fleet-design-brief/v1alpha1') { throw new TorchError('Unsupported Fleet design brief', { code: 'INVALID_ARCHITECT_BRIEF' }); @@ -161,8 +171,16 @@ export function validateArchitectProposal({ brief, proposal } = {}) { } const expectedRepository = { root: brief?.project?.root, initialCommit: brief?.project?.initialCommit, head: brief?.project?.head, + workingTreeFingerprint: brief?.project?.workingTreeFingerprint, }; if (!same(proposal?.repository, expectedRepository)) problems.push('proposal changed its repository binding'); + if (brief?.project?.root && brief?.project?.workingTreeFingerprint + && workingTreeFingerprint(brief.project.root) !== brief.project.workingTreeFingerprint) { + problems.push('repository working tree changed after the design brief was generated'); + } + problems.push(...validateSpecificationEvidence( + brief?.reconnaissance?.specifications ?? [], brief?.project?.root, + )); if (!same(proposal?.specifications ?? [], brief?.baseline?.specifications ?? [])) { problems.push('proposal changed specification provenance'); } @@ -173,6 +191,7 @@ export function validateArchitectProposal({ brief, proposal } = {}) { const knownResources = new Set((brief?.baseline?.resources ?? []).map((resource) => resource.id)); const ids = new Set(); if (!Array.isArray(proposal?.domains) || proposal.domains.length === 0) problems.push('proposal has no domains'); + problems.push(...primaryOwnershipProblems(proposal?.domains)); for (const [index, domain] of (proposal?.domains ?? []).entries()) { const label = `domains[${index}]`; if (typeof domain.id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(domain.id) || ids.has(domain.id)) { @@ -181,7 +200,11 @@ export function validateArchitectProposal({ brief, proposal } = {}) { ids.add(domain.id); if (typeof domain.title !== 'string' || !domain.title.trim()) problems.push(`${label} has no title`); if (typeof domain.kind !== 'string' || !domain.kind.trim()) problems.push(`${label} has no kind`); - if (!['claude', 'codex'].includes(domain.runtime)) problems.push(`${label} has an unsupported runtime`); + // A pending design may name an adapter; approved installation checks its + // availability and plugin trust. Designing a role never executes it. + if (typeof domain.runtime !== 'string' || !/^[a-z][a-z0-9-]*$/.test(domain.runtime)) { + problems.push(`${label} has an invalid runtime identifier`); + } requireStringArray(domain.scope, `${label}.scope`, problems, { nonempty: true }); requireStringArray(domain.not_scope, `${label}.not_scope`, problems); const owned = requireStringArray(domain.owned_paths, `${label}.owned_paths`, problems); @@ -204,6 +227,104 @@ export function validateArchitectProposal({ brief, proposal } = {}) { if (!ids.has(neighbour)) problems.push(`${domain.id} names unknown neighbour: ${neighbour}`); } } + const organizationAssessment = proposal?.organization_assessment; + const assessmentShapes = new Set(['flat', 'coordination_roles']); + if (!organizationAssessment || typeof organizationAssessment !== 'object' || Array.isArray(organizationAssessment)) { + problems.push('proposal has no organization_assessment'); + } else { + rejectUnknownFields(organizationAssessment, [ + 'shape', 'rationale', 'evidence', 'authority_boundaries', + 'proposed_coordination_roles', 'direct_peer_communication', + ], 'organization_assessment', problems); + if (!assessmentShapes.has(organizationAssessment.shape)) { + problems.push('organization_assessment must choose flat or coordination_roles'); + } + if (typeof organizationAssessment.rationale !== 'string' || !organizationAssessment.rationale.trim()) { + problems.push('organization_assessment has no rationale'); + } + const authorityBoundaries = organizationAssessment.authority_boundaries; + const authorityFields = [ + 'implementation_ownership', 'coordination_responsibility', 'fleet_operations_authority', + 'project_priority_authority', 'independent_review_authority', + ]; + if (!authorityBoundaries || typeof authorityBoundaries !== 'object' || Array.isArray(authorityBoundaries)) { + problems.push('organization_assessment has no authority_boundaries'); + } else { + rejectUnknownFields(authorityBoundaries, [ + ...authorityFields, 'owner_only_decisions', + ], 'organization_assessment.authority_boundaries', problems); + for (const field of authorityFields) { + if (typeof authorityBoundaries[field] !== 'string' || !authorityBoundaries[field].trim()) { + problems.push(`organization_assessment.authority_boundaries.${field} is required`); + } + } + requireStringArray( + authorityBoundaries.owner_only_decisions, + 'organization_assessment.authority_boundaries.owner_only_decisions', + problems, + { nonempty: true }, + ); + } + const assessmentEvidence = requireStringArray( + organizationAssessment.evidence, 'organization_assessment.evidence', problems, { nonempty: true }, + ); + for (const citation of assessmentEvidence) { + if (!evidence.has(citation)) problems.push(`organization_assessment cited unknown evidence: ${citation}`); + } + if (organizationAssessment.direct_peer_communication !== 'preserved') { + problems.push('organization_assessment must preserve direct peer communication'); + } + const roles = organizationAssessment.proposed_coordination_roles; + if (!Array.isArray(roles)) { + problems.push('organization_assessment.proposed_coordination_roles must be an array'); + } else { + if (organizationAssessment.shape === 'flat' && roles.length !== 0) { + problems.push('flat organization_assessment cannot propose coordination roles'); + } + if (organizationAssessment.shape === 'coordination_roles' && roles.length === 0) { + problems.push('coordination_roles assessment must name at least one justified lead'); + } + const roleIds = new Set(); + for (const [index, role] of roles.entries()) { + const label = `organization_assessment.proposed_coordination_roles[${index}]`; + rejectUnknownFields(role, [ + 'id', 'title', 'coordinates_domains', 'integrated_outcome', 'decision_scope', + 'owner_escalations', 'evidence', 'owns_implementation_paths', + 'has_owner_approval_authority', + ], label, problems); + if (typeof role?.id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(role.id) || roleIds.has(role.id)) { + problems.push(`${label} has an invalid or duplicate id`); + } + if (role?.id) roleIds.add(role.id); + if (typeof role?.title !== 'string' || !role.title.trim()) problems.push(`${label} has no title`); + const coordinated = requireStringArray( + role?.coordinates_domains, `${label}.coordinates_domains`, problems, { nonempty: true }, + ); + if (coordinated.length < 2) problems.push(`${label} must coordinate at least two existing domains`); + if (new Set(coordinated).size !== coordinated.length) { + problems.push(`${label} has duplicate domain references`); + } + for (const domainId of coordinated) { + if (!ids.has(domainId)) problems.push(`${label} names unknown domain: ${domainId}`); + } + if (typeof role?.integrated_outcome !== 'string' || !role.integrated_outcome.trim()) { + problems.push(`${label} has no integrated outcome`); + } + requireStringArray(role?.decision_scope, `${label}.decision_scope`, problems, { nonempty: true }); + requireStringArray(role?.owner_escalations, `${label}.owner_escalations`, problems, { nonempty: true }); + const roleEvidence = requireStringArray(role?.evidence, `${label}.evidence`, problems, { nonempty: true }); + for (const citation of roleEvidence) { + if (!evidence.has(citation)) problems.push(`${label} cited unknown evidence: ${citation}`); + } + if (role?.owns_implementation_paths !== false) { + problems.push(`${label} must not receive implementation ownership`); + } + if (role?.has_owner_approval_authority !== false) { + problems.push(`${label} must not receive owner-approval authority`); + } + } + } + } for (const collision of proposal?.collisions ?? []) { if (!Array.isArray(collision.domains) || collision.domains.some((id) => !ids.has(id))) { problems.push('collision references an unknown domain'); diff --git a/src/design/brief.mjs b/src/design/brief.mjs index 9a89619..9172172 100644 --- a/src/design/brief.mjs +++ b/src/design/brief.mjs @@ -37,17 +37,21 @@ export function createFleetDesignBrief({ repository, analysis, baseline } = {}) 'Independent verification: each domain can produce meaningful local evidence.', 'Persistent expertise: retained domain context should reduce reload churn for recurring work.', 'Collision safety: shared files, APIs, schemas, tests, outputs, and scarce resources have an explicit protocol.', + 'Organizational fit: recommend a flat fleet unless evidence shows recurring cross-domain coordination needs a lead.', ], prohibitions: [ 'Do not copy a generic agent catalog or the COMBATRIG roster.', 'Do not invent path ownership unsupported by repository or owner evidence.', 'Do not optimize for a predetermined number of sessions.', + 'Do not grant proposed coordination roles implementation ownership or owner-approval authority.', + 'Do not route routine specialist communication exclusively through a manager.', 'Do not mark the proposal approved; only the project owner may approve it.', ], }, project: { root: repository.root, name: repository.name, branch: repository.branch, head: repository.head, initialCommit: repository.initialCommit, + workingTreeFingerprint: analysis.repository.workingTreeFingerprint, }, reconnaissance: { inventory: analysis.inventory, @@ -66,10 +70,33 @@ export function createFleetDesignBrief({ repository, analysis, baseline } = {}) specificationSignals.some((signal) => !signal.representedInBaseline) ? 'Which unmatched specification responsibilities require a persistent domain?' : null, missingOwnership.length ? 'Which real or planned paths establish ownership for every responsibility-only domain?' : null, baseline.collisions.length ? 'Are the proposed collision strategies sufficient, or should any boundary be redrawn?' : null, + 'Does recurring cross-domain coordination justify leads, or is the smallest useful organization flat? Cite the observed dependency and collision evidence.', + 'For each justified lead, what integrated outcome and bounded coordination decisions does it own, and which product/owner decisions remain escalated?', + 'How will specialists retain direct peer communication without routing routine collaboration through a lead?', 'Which checks and scarce resources are required before each domain can report verified work?', 'Which domains recur often enough for hot context to outweigh fleet coordination overhead?', ]), }, + organizationPolicy: { + recommendation: 'Prefer a flat organization unless repository evidence demonstrates recurring integration load that one or more coordination roles can own.', + evidenceSources: ['dependency edges', 'collision candidates', 'unassigned components', 'specification signals'], + coordinationRoleRules: [ + 'Each proposed lead must cite evidence and name an integrated outcome, not merely relay messages.', + 'Coordination roles do not receive specialist implementation paths or owner-only approval authority.', + 'State bounded coordination decisions and decisions that still require owner approval.', + 'Direct specialist-to-specialist communication remains allowed; reporting lines do not create message-routing barriers.', + 'New persistent identities still require the separate owner-approved Fleet-evolution path.', + ], + authorityQuestions: [ + 'Which identity remains the single accountable implementation owner for each deliverable?', + 'Who coordinates cross-owner sequencing and integration without taking code ownership?', + 'Who owns session, worktree, gate, schedule, and fleet-health mechanics?', + 'Who proposes or sets project priorities, and under what owner policy?', + 'Which independent review or creative authorities are needed, and what can they accept or reject?', + 'Which decisions remain exclusively with the project owner?', + ], + activation: 'This assessment is advisory and pending. It does not change the active organization, create identities, assign implementation ownership, or start sessions.', + }, requiredOutput: { schema: 'torch.dev/domain-proposal/v1alpha1', reviewStatus: 'pending', @@ -79,9 +106,32 @@ export function createFleetDesignBrief({ repository, analysis, baseline } = {}) 'neighbours', 'required_checks', 'runtime', 'evidence', 'resources', ], + organizationAssessment: { + field: 'organization_assessment', + shapeValues: ['flat', 'coordination_roles'], + fields: [ + 'shape', 'rationale', 'evidence', 'authority_boundaries', + 'proposed_coordination_roles', 'direct_peer_communication', + ], + authorityBoundaryFields: [ + 'implementation_ownership', 'coordination_responsibility', + 'fleet_operations_authority', 'project_priority_authority', + 'independent_review_authority', 'owner_only_decisions', + ], + coordinationRoleFields: [ + 'id', 'title', 'coordinates_domains', 'integrated_outcome', 'decision_scope', + 'owner_escalations', 'evidence', 'owns_implementation_paths', + 'has_owner_approval_authority', + ], + fixedBoundaries: { + owns_implementation_paths: false, + has_owner_approval_authority: false, + direct_peer_communication: 'preserved', + }, + }, topLevelFields: [ 'schema', 'generatedAt', 'repository', 'review', 'specifications', 'domains', - 'collisions', 'checks', 'resources', 'schedules', 'architecture', + 'collisions', 'checks', 'resources', 'schedules', 'architecture', 'organization_assessment', ], acceptance: 'Every proposed responsibility cites inspectable evidence; unresolved ownership remains explicit and blocks installation.', }, diff --git a/src/evolution/hierarchy.mjs b/src/evolution/hierarchy.mjs new file mode 100644 index 0000000..4da4920 --- /dev/null +++ b/src/evolution/hierarchy.mjs @@ -0,0 +1,1093 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import { dirname, join, resolve } from 'node:path'; +import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'; +import { z } from 'zod'; +import { BacklogService } from '../backlog/service.mjs'; +import { TorchError } from '../kernel/errors.mjs'; +import { loadProjectConfig, validateProjectConfig } from '../kernel/config.mjs'; +import { + organizationGraphFromConfig, planManagerCheckInSchedules, validateOrganizationGraph, +} from '../kernel/organization.mjs'; +import { readInstallManifest } from '../kernel/install.mjs'; + +const text = z.string().trim().min(1); +const id = text.regex(/^[a-z0-9][a-z0-9-]*$/); +const evidenceSignal = z.object({ + code: id, + summary: text, + occurrences: z.number().int().min(1), + affected_identities: z.array(id).min(1), + references: z.array(text).min(2), +}).strict(); +const alternative = z.object({ + type: z.enum(['no-change', 'process-change', 'specialist-change', 'promote-existing', 'add-role', 'restructure']), + description: text, + expected_outcome: text, +}).strict(); + +export const hierarchyProposalSchema = z.object({ + schema: z.literal('torch.dev/hierarchy-proposal/v1alpha1'), + title: text, + rationale: text, + observation_window: z.object({ + start_at: z.string().datetime({ offset: true }), + end_at: z.string().datetime({ offset: true }), + days: z.number().int().min(7).max(365), + summary: text, + }).strict(), + signals: z.array(evidenceSignal).min(1), + alternatives: z.array(alternative).min(2), + impact: z.object({ + integrated_outcome: text, + why_existing_roles_are_insufficient: text, + expected_benefit: text, + added_coordination_cost: text, + affected_identities: z.array(id).min(1), + }).strict(), + graph: z.unknown(), + pilot: z.object({ + duration_days: z.number().int().min(1).max(90), + baseline_metrics: z.array(z.object({ name: text, value: text, unit: text }).strict()).min(1), + success_criteria: z.array(text).min(1), + stop_criteria: z.array(text).min(1), + reversal_plan: text, + }).strict(), +}).strict().superRefine((proposal, context) => { + const start = Date.parse(proposal.observation_window.start_at); + const end = Date.parse(proposal.observation_window.end_at); + if (end <= start) { + context.addIssue({ + code: 'custom', path: ['observation_window', 'end_at'], + message: 'observation window must end after it starts', + }); + } + const measuredDays = Math.ceil((end - start) / 86_400_000); + if (Math.abs(measuredDays - proposal.observation_window.days) > 1) { + context.addIssue({ + code: 'custom', path: ['observation_window', 'days'], + message: 'observation window day count must agree with its timestamps', + }); + } + const totalOccurrences = proposal.signals.reduce((sum, signal) => sum + signal.occurrences, 0); + if (proposal.signals.length < 2 && totalOccurrences < 3) { + context.addIssue({ + code: 'custom', path: ['signals'], + message: 'hierarchy proposals require at least two independent signals or three recurring observations', + }); + } + const alternativeTypes = new Set(proposal.alternatives.map((entry) => entry.type)); + for (const required of ['no-change', 'process-change', 'promote-existing']) { + if (!alternativeTypes.has(required)) { + context.addIssue({ + code: 'custom', path: ['alternatives'], + message: `must explicitly consider ${required}`, + }); + } + } +}); + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])); +} + +function digest(value) { + return createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(stable(value))).digest('hex'); +} + +function fileDigest(path) { + return createHash('sha256').update(readFileSync(path)).digest('hex'); +} + +function git(repositoryRoot, args) { + return execFileSync('git', ['-C', repositoryRoot, ...args], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); +} + +function rowToProposal(row) { + if (!row) return null; + return { + id: row.id, + state: row.state, + proposer: row.proposer, + createdAt: row.created_at, + updatedAt: row.updated_at, + baseCommit: row.base_commit, + baseGraphDigest: row.base_graph_digest, + fingerprint: row.fingerprint, + revision: row.proposal_revision ?? 1, + proposal: JSON.parse(row.proposal_json), + decidedBy: row.decided_by ?? null, + decidedAt: row.decided_at ?? null, + decisionReason: row.decision_reason ?? null, + activationCommit: row.activation_commit ?? null, + }; +} + +function sameOwners(left, right) { + const normalize = (owners) => owners.map((owner) => `${owner.surface}\0${owner.identity_id}`).sort(); + return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right)); +} + +function normalizeProposal(input) { + const parsed = hierarchyProposalSchema.safeParse(input); + if (!parsed.success) { + throw new TorchError('Hierarchy proposal is invalid', { + code: 'HIERARCHY_PROPOSAL_INVALID', + details: parsed.error.issues.map((issue) => ({ + path: issue.path.length ? issue.path.join('.') : '', + message: issue.message, + code: issue.code, + })), + }); + } + const proposal = parsed.data; + try { + proposal.graph = validateOrganizationGraph(proposal.graph); + } catch (error) { + throw new TorchError('Hierarchy proposal graph is invalid', { + code: 'HIERARCHY_PROPOSAL_GRAPH_INVALID', details: error.details ?? error.message, + }); + } + return proposal; +} + +function initialize(database) { + database.exec(` + CREATE TABLE IF NOT EXISTS hierarchy_proposals ( + id TEXT PRIMARY KEY, + state TEXT NOT NULL, + proposer TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + base_commit TEXT NOT NULL, + base_graph_digest TEXT NOT NULL, + fingerprint TEXT NOT NULL, + proposal_json TEXT NOT NULL, + proposal_revision INTEGER NOT NULL DEFAULT 1, + decided_by TEXT, + decided_at TEXT, + decision_reason TEXT + ); + CREATE INDEX IF NOT EXISTS hierarchy_proposals_state_created + ON hierarchy_proposals(state, created_at, id); + CREATE INDEX IF NOT EXISTS hierarchy_proposals_fingerprint + ON hierarchy_proposals(fingerprint, state); + `); + const columns = new Set(database.prepare('PRAGMA table_info(hierarchy_proposals)').all().map((column) => column.name)); + if (!columns.has('activation_commit')) database.exec('ALTER TABLE hierarchy_proposals ADD COLUMN activation_commit TEXT'); + if (!columns.has('proposal_revision')) database.exec('ALTER TABLE hierarchy_proposals ADD COLUMN proposal_revision INTEGER NOT NULL DEFAULT 1'); + database.exec(` + CREATE TABLE IF NOT EXISTS hierarchy_proposal_revisions ( + proposal_id TEXT NOT NULL, + revision INTEGER NOT NULL, + updated_at TEXT NOT NULL, + updated_by TEXT NOT NULL, + proposal_json TEXT NOT NULL, + proposal_digest TEXT NOT NULL, + PRIMARY KEY (proposal_id, revision) + ); + CREATE TABLE IF NOT EXISTS hierarchy_pilot_reviews ( + id TEXT PRIMARY KEY, + proposal_id TEXT NOT NULL, + report_digest TEXT NOT NULL, + report_json TEXT NOT NULL, + created_at TEXT NOT NULL, + UNIQUE(proposal_id, report_digest) + ); + `); + for (const row of database.prepare('SELECT * FROM hierarchy_proposals').all()) { + const proposal = JSON.parse(row.proposal_json); + const fingerprint = digest({ baseGraphDigest: row.base_graph_digest, graph: proposal.graph }); + if (row.fingerprint !== fingerprint) { + database.prepare('UPDATE hierarchy_proposals SET fingerprint = ? WHERE id = ?').run(fingerprint, row.id); + } + database.prepare(` + INSERT OR IGNORE INTO hierarchy_proposal_revisions ( + proposal_id, revision, updated_at, updated_by, proposal_json, proposal_digest + ) VALUES (?, ?, ?, ?, ?, ?) + `).run(row.id, row.proposal_revision ?? 1, row.updated_at, row.proposer, + row.proposal_json, digest(proposal)); + } +} + +function writeFilesAtomically(repositoryRoot, files) { + const originals = new Map(); + for (const [relativePath] of files) { + const path = join(repositoryRoot, relativePath); + originals.set(path, existsSync(path) ? readFileSync(path, 'utf8') : null); + } + try { + for (const [relativePath, content] of files) { + const path = join(repositoryRoot, relativePath); + mkdirSync(dirname(path), { recursive: true }); + const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`; + writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + renameSync(temporary, path); + } + } catch (error) { + for (const [path, content] of originals) { + try { + if (content === null) { + if (existsSync(path)) unlinkSync(path); + continue; + } + const temporary = `${path}.rollback-${process.pid}-${randomUUID()}`; + writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + renameSync(temporary, path); + } catch { /* preserve the original write failure */ } + } + throw error; + } + return originals; +} + +export class HierarchyEvolutionService { + constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID } = {}) { + if (!controlPlane) throw new TorchError('Hierarchy evolution requires the TORCH control plane', { code: 'CONTROL_PLANE_REQUIRED' }); + this.repositoryRoot = resolve(repositoryRoot ?? controlPlane.repositoryRoot); + this.controlPlane = controlPlane; + this.database = controlPlane.database; + this.clock = clock; + this.idFactory = idFactory; + initialize(this.database); + } + + get(proposalId) { + const idValue = text.parse(proposalId); + const result = rowToProposal(this.database.prepare('SELECT * FROM hierarchy_proposals WHERE id = ?').get(idValue)); + if (!result) throw new TorchError(`Unknown hierarchy proposal: ${idValue}`, { code: 'HIERARCHY_PROPOSAL_NOT_FOUND' }); + return result; + } + + list({ state } = {}) { + const states = ['proposed', 'pilot-approved', 'deferred', 'rejected', 'piloting', 'adopted', 'reversed']; + if (state && !states.includes(state)) { + throw new TorchError(`Invalid hierarchy proposal state: ${state}`, { code: 'HIERARCHY_PROPOSAL_STATE_INVALID' }); + } + const rows = state + ? this.database.prepare('SELECT * FROM hierarchy_proposals WHERE state = ? ORDER BY created_at, id').all(state) + : this.database.prepare('SELECT * FROM hierarchy_proposals ORDER BY created_at, id').all(); + return rows.map(rowToProposal); + } + + proposalHistory(proposalId) { + const record = this.get(proposalId); + return this.database.prepare(` + SELECT revision, updated_at, updated_by, proposal_json, proposal_digest + FROM hierarchy_proposal_revisions WHERE proposal_id = ? ORDER BY revision + `).all(record.id).map((row) => ({ + revision: row.revision, updatedAt: row.updated_at, updatedBy: row.updated_by, + proposal: JSON.parse(row.proposal_json), digest: row.proposal_digest, + })); + } + + pilotReviews(proposalId) { + const record = this.get(proposalId); + return this.database.prepare(`SELECT report_json FROM hierarchy_pilot_reviews + WHERE proposal_id = ? ORDER BY created_at, id`).all(record.id) + .map((row) => JSON.parse(row.report_json)); + } + + recordPilotReview({ proposalId, reviewer = 'session-manager', review } = {}) { + const actor = this.controlPlane.assertIdentity(reviewer); + if (actor !== 'session-manager') throw new TorchError('Only the Session Manager may submit pilot review evidence.', { + code: 'HIERARCHY_REVIEW_AUTHORITY_REQUIRED', + }); + const record = this.get(proposalId); + if (record.state !== 'piloting') throw new TorchError('Pilot review requires an activated pilot.', { + code: 'HIERARCHY_PILOT_REVIEW_BLOCKED', + }); + const schema = z.object({ + summary: text, + recommendation: z.enum(['adopt', 'adjust', 'reverse', 'inconclusive']), + metrics: z.array(z.object({ + name: text, value: text, unit: text, + classification: z.enum(['measured', 'qualitative', 'unavailable']), + evidence: z.array(text).min(1), + }).strict()), + success: z.array(z.object({ criterion: text, outcome: z.enum(['met', 'not-met', 'unknown']), evidence: z.array(text).min(1) }).strict()), + stop: z.array(z.object({ criterion: text, outcome: z.enum(['met', 'not-met', 'unknown']), evidence: z.array(text).min(1) }).strict()), + }).strict(); + const parsed = schema.safeParse(review); + if (!parsed.success) throw new TorchError('Pilot review evidence is invalid.', { + code: 'HIERARCHY_REVIEW_INVALID', details: parsed.error.issues, + }); + const evidence = parsed.data; + const pilotPath = `.torch/hierarchy-pilots/${record.id}.json`; + const bytes = readFileSync(join(this.repositoryRoot, pilotPath), 'utf8'); + const manifest = readInstallManifest(this.repositoryRoot); + const owned = manifest.created.find((entry) => entry.path === pilotPath); + if (!owned || owned.sha256 !== createHash('sha256').update(bytes).digest('hex')) { + throw new TorchError('Pilot record has changed outside its owned manifest.', { code: 'HIERARCHY_PILOT_RECORD_CHANGED' }); + } + const pilot = JSON.parse(bytes); + const currentGraph = organizationGraphFromConfig(loadProjectConfig(this.repositoryRoot)); + if (digest(currentGraph) !== digest(pilot.graph)) throw new TorchError('Pilot graph is no longer active.', { + code: 'HIERARCHY_PILOT_REVIEW_BLOCKED', + }); + const exactCoverage = (expected, actual, key) => expected.length === actual.length + && new Set(actual.map((item) => item[key])).size === actual.length + && expected.every((name) => actual.some((item) => item[key] === name)); + if (!exactCoverage(record.proposal.pilot.baseline_metrics.map((metric) => metric.name), evidence.metrics, 'name') + || !exactCoverage(record.proposal.pilot.success_criteria, evidence.success, 'criterion') + || !exactCoverage(record.proposal.pilot.stop_criteria, evidence.stop, 'criterion')) { + throw new TorchError('Review must cover each baseline metric and success/stop criterion exactly once.', { + code: 'HIERARCHY_REVIEW_INCOMPLETE', + }); + } + const comparisons = record.proposal.pilot.baseline_metrics.map((baseline) => { + const observed = evidence.metrics.find((metric) => metric.name === baseline.name); + if (observed.unit !== baseline.unit) throw new TorchError('Observed metric units must match the baseline.', { + code: 'HIERARCHY_REVIEW_UNIT_MISMATCH', details: { metric: baseline.name }, + }); + return { name: baseline.name, unit: baseline.unit, baseline: baseline.value, + observed: observed.value, classification: observed.classification, evidence: observed.evidence }; + }); + const reviewedAt = this.clock().toISOString(); + const dueAt = new Date(Date.parse(pilot.activatedAt) + pilot.pilot.duration_days * 86_400_000).toISOString(); + if (Date.parse(reviewedAt) < Date.parse(pilot.activatedAt)) throw new TorchError('Review predates pilot activation.', { + code: 'HIERARCHY_REVIEW_TIME_INVALID', + }); + const windowComplete = Date.parse(reviewedAt) >= Date.parse(dueAt); + if (evidence.recommendation === 'adopt' && (!windowComplete + || comparisons.some((metric) => metric.classification !== 'measured') + || evidence.success.some((entry) => entry.outcome !== 'met') + || evidence.stop.some((entry) => entry.outcome !== 'not-met'))) { + throw new TorchError('Adoption recommendation requires a complete window, measured comparisons and passing criteria.', { + code: 'HIERARCHY_REVIEW_ADOPTION_UNPROVEN', + }); + } + const content = { proposalId: record.id, activationCommit: record.activationCommit, + graphDigest: digest(currentGraph), window: { startedAt: pilot.activatedAt, dueAt, complete: windowComplete }, + reviewer: actor, summary: evidence.summary, recommendation: evidence.recommendation, + comparisons, success: evidence.success, stop: evidence.stop, + evidenceVerification: 'reviewer-reported', ownerDecisionRequired: true, + mutationPerformed: true, organizationChanged: false, runtimeStarted: false }; + const fingerprint = digest(content); + this.database.exec('BEGIN IMMEDIATE'); + try { + const existing = this.database.prepare(`SELECT report_json FROM hierarchy_pilot_reviews + WHERE proposal_id = ? AND report_digest = ?`).get(record.id, fingerprint); + if (existing) { + this.database.exec('COMMIT'); + return { ...JSON.parse(existing.report_json), deduplicated: true }; + } + const report = { id: this.idFactory(), reviewedAt, ...content }; + this.database.prepare(`INSERT INTO hierarchy_pilot_reviews + (id, proposal_id, report_digest, report_json, created_at) VALUES (?, ?, ?, ?, ?)`) + .run(report.id, record.id, fingerprint, JSON.stringify(report), reviewedAt); + this.controlPlane.audit({ actorId: actor, operation: 'hierarchy.review-pilot', + entityType: 'hierarchy-proposal', entityId: record.id, + details: { reviewId: report.id, recommendation: report.recommendation, ownerDecisionRequired: true }, + }); + this.database.exec('COMMIT'); + return { ...report, deduplicated: false }; + } catch (error) { + try { this.database.exec('ROLLBACK'); } catch { /* preserve original error */ } + throw error; + } + } + + assess({ assessor = 'session-manager', at = this.clock() } = {}) { + const actor = this.controlPlane.assertIdentity(assessor); + if (actor !== 'session-manager') { + throw new TorchError('Only the Session Manager may assess hierarchy coordination pressure', { + code: 'HIERARCHY_ASSESSMENT_AUTHORITY_REQUIRED', details: { actor }, + }); + } + const assessedAt = at instanceof Date ? new Date(at) : new Date(at); + if (!Number.isFinite(assessedAt.getTime())) { + throw new TorchError('Hierarchy assessment requires a valid timestamp', { + code: 'HIERARCHY_ASSESSMENT_TIME_INVALID', + }); + } + const config = loadProjectConfig(this.repositoryRoot); + const observationWindowDays = config.organization_assessment?.observation_window_days ?? 30; + const minimumRecurrences = config.organization_assessment?.minimum_recurrences ?? 3; + const windowStart = new Date(assessedAt.getTime() - observationWindowDays * 86_400_000); + const withinWindow = (value) => { + const timestamp = Date.parse(value); + return Number.isFinite(timestamp) && timestamp >= windowStart.getTime() + && timestamp <= assessedAt.getTime(); + }; + const signals = []; + const unique = (values) => [...new Set(values.filter(Boolean))].sort(); + const activeIdentities = new Set(this.controlPlane.listAgents().map((agent) => agent.areaId)); + const addRecurringSignal = (code, summary, events, identities, reference) => { + if (events.length < 2) return; + const references = unique(events.map(reference)); + if (references.length < 2) return; + signals.push({ + code, summary, occurrences: events.length, + affected_identities: unique(identities).filter((identity) => activeIdentities.has(identity)), references, + }); + }; + + const tasks = new BacklogService({ + repositoryRoot: this.repositoryRoot, controlPlane: this.controlPlane, + }).list(); + const recurringTasks = new Map(); + for (const task of tasks) { + const occurredAt = (task.history ?? []).some((event) => withinWindow(event.at)) + || withinWindow(task.createdAt); + const domains = unique(task.affectedDomains ?? []); + if (!occurredAt || domains.length < 2) continue; + const key = domains.join(','); + const entries = recurringTasks.get(key) ?? []; + entries.push(task); + recurringTasks.set(key, entries); + } + for (const [domainSet, entries] of recurringTasks) { + const domainIds = domainSet.split(','); + addRecurringSignal( + 'recurring-multi-domain-work', + `Distinct backlog tasks repeatedly span ${domainIds.join(', ')} during the observation window.`, + entries, ['session-manager', ...domainIds], (task) => `backlog:${task.id}`, + ); + } + + const coordinationRequests = this.database.prepare(` + SELECT m.id, m.sender_id AS sender, m.created_at AS createdAt, + m.task_ref AS task, a.acknowledged_at AS acknowledgedAt + FROM messages m + LEFT JOIN message_acks a ON a.message_id = m.id AND a.area_id = m.recipient_id + WHERE m.project_id = ? AND m.recipient_id = 'session-manager' + AND m.kind = 'coordination-request' + ORDER BY m.created_at, m.id + `).all(this.controlPlane.projectId).filter((event) => withinWindow(event.createdAt)); + addRecurringSignal( + 'recurring-manager-coordination-requests', + `Repeated direct coordination requests reached the Session Manager; ${coordinationRequests.filter((event) => !event.acknowledgedAt).length} remain unacknowledged.`, + coordinationRequests, ['session-manager', ...coordinationRequests.map((event) => event.sender)], + (event) => `message:${event.id}`, + ); + + const approvals = this.database.prepare(` + SELECT id, requester_id AS requester, created_at AS createdAt, + decided_at AS decidedAt, status + FROM approval_requests + WHERE project_id = ? AND approver_id = 'session-manager' + ORDER BY created_at, id + `).all(this.controlPlane.projectId).filter((event) => withinWindow(event.createdAt)); + addRecurringSignal( + 'recurring-manager-approval-waits', + `Named owner-facing approval requests recurred; ${approvals.filter((event) => event.status === 'pending').length} are still pending.`, + approvals, ['session-manager', ...approvals.map((event) => event.requester)], + (event) => `approval:${event.id}`, + ); + + const handoffs = this.database.prepare(` + SELECT id, sender_id AS sender, created_at AS createdAt, path_ref AS path, task_ref AS task + FROM handoffs WHERE project_id = ? AND recipient_id = 'session-manager' + ORDER BY created_at, id + `).all(this.controlPlane.projectId).filter((event) => withinWindow(event.createdAt)); + addRecurringSignal( + 'recurring-manager-handoffs', + 'Handoff requests repeatedly routed to the Session Manager during the observation window.', + handoffs, ['session-manager', ...handoffs.map((event) => event.sender)], + (event) => `handoff:${event.id}`, + ); + + const repeatedOccurrences = signals.reduce((sum, signal) => sum + signal.occurrences, 0); + const qualifies = signals.length >= 2 + || signals.some((signal) => signal.occurrences >= minimumRecurrences); + const graph = organizationGraphFromConfig(config); + return { + schema: 'torch.dev/hierarchy-assessment/v1alpha1', + assessedAt: assessedAt.toISOString(), assessor: actor, + observationWindow: { + startAt: windowStart.toISOString(), endAt: assessedAt.toISOString(), + days: observationWindowDays, + summary: `Read-only scan of durable Fleet events from ${windowStart.toISOString()} through ${assessedAt.toISOString()}.`, + }, + organizationRevision: graph.revision, + signals, + recommendation: qualifies ? { + action: 'consider-coordination-role', + reason: 'Recurring measured coordination signals justify Session Manager review, not automatic role creation.', + requiresManagerJudgment: true, requiresOwnerApproval: true, + nextTool: 'torch_propose_hierarchy_change', + } : { + action: 'retain-current-organization', + reason: 'The configured window contains no repeated structural signal above the conservative threshold.', + requiresManagerJudgment: true, requiresOwnerApproval: true, nextTool: null, + }, + threshold: { independentSignalsRequired: 2, minimumRecurrences, observedRecurrences: repeatedOccurrences }, + sourceCounts: { + backlogTasksInWindow: tasks.filter((task) => withinWindow(task.createdAt) + || (task.history ?? []).some((event) => withinWindow(event.at))).length, + coordinationRequests: coordinationRequests.length, + approvalRequestsToSessionManager: approvals.length, + handoffsToSessionManager: handoffs.length, + }, + mutationPerformed: false, + }; + } + + propose({ proposer, proposal: input } = {}) { + const actor = this.controlPlane.assertIdentity(proposer); + if (actor !== 'session-manager') { + throw new TorchError('Only the owner-facing Session Manager may propose an organization change', { + code: 'HIERARCHY_PROPOSAL_AUTHORITY_REQUIRED', details: { actor }, + }); + } + const proposal = normalizeProposal(input); + if (Date.parse(proposal.observation_window.end_at) > this.clock().getTime()) { + throw new TorchError('Hierarchy proposal evidence window cannot end in the future', { + code: 'HIERARCHY_OBSERVATION_WINDOW_FUTURE', + }); + } + const config = loadProjectConfig(this.repositoryRoot); + const currentGraph = organizationGraphFromConfig(config); + const activeIdentities = new Set(['owner', ...this.controlPlane.listAgents().map((agent) => agent.areaId)]); + const proposedIdentities = new Set(proposal.graph.roles.map((role) => role.identity_id)); + const referencedIdentities = [ + ...proposedIdentities, + ...proposal.signals.flatMap((signal) => signal.affected_identities), + ...proposal.impact.affected_identities, + ]; + const unknownIdentities = [...new Set(referencedIdentities)].filter((identity) => !activeIdentities.has(identity)); + if (unknownIdentities.length) { + throw new TorchError('Hierarchy proposals must reference active Fleet identities; propose new sessions through Fleet evolution first', { + code: 'HIERARCHY_IDENTITY_UNKNOWN', details: { identities: unknownIdentities }, + }); + } + if (proposal.graph.revision !== currentGraph.revision + 1) { + throw new TorchError('Proposed organization graph must increment the active revision exactly once', { + code: 'HIERARCHY_REVISION_CONFLICT', + details: { currentRevision: currentGraph.revision, proposedRevision: proposal.graph.revision }, + }); + } + if (!sameOwners(currentGraph.implementation_owners, proposal.graph.implementation_owners)) { + throw new TorchError('Hierarchy proposals cannot change implementation ownership; propose a separate domain-boundary change', { + code: 'HIERARCHY_IMPLEMENTATION_OWNERSHIP_CHANGED', + }); + } + const baseGraphDigest = digest(currentGraph); + // Group one open review by structural intent. Materially newer evidence + // revises that review; once it is decided, a later observation window can + // create a new proposal for the same graph without being suppressed. + const fingerprint = digest({ baseGraphDigest, graph: proposal.graph }); + const duplicate = this.database.prepare(` + SELECT * FROM hierarchy_proposals WHERE fingerprint = ? AND state = 'proposed' + ORDER BY created_at DESC LIMIT 1 + `).get(fingerprint); + if (duplicate) { + const existingProposal = JSON.parse(duplicate.proposal_json); + const existingDigest = digest(existingProposal); + const proposedDigest = digest(proposal); + if (existingDigest === proposedDigest) return { ...rowToProposal(duplicate), deduplicated: true }; + if (Date.parse(proposal.observation_window.end_at) + <= Date.parse(existingProposal.observation_window.end_at)) { + throw new TorchError('A material update to an open hierarchy proposal requires a later evidence window', { + code: 'HIERARCHY_EVIDENCE_WINDOW_NOT_ADVANCED', + details: { + existingEndAt: existingProposal.observation_window.end_at, + proposedEndAt: proposal.observation_window.end_at, + }, + }); + } + const now = this.clock().toISOString(); + const revision = (duplicate.proposal_revision ?? 1) + 1; + const baseCommit = git(this.repositoryRoot, ['rev-parse', 'HEAD']); + const database = this.database; + database.exec('BEGIN IMMEDIATE'); + try { + const update = database.prepare(` + UPDATE hierarchy_proposals SET proposal_json = ?, proposal_revision = ?, + base_commit = ?, updated_at = ? + WHERE id = ? AND state = 'proposed' AND proposal_revision = ? + `).run(JSON.stringify(proposal), revision, baseCommit, now, duplicate.id, + duplicate.proposal_revision ?? 1); + if (update.changes !== 1) { + throw new TorchError('Hierarchy proposal changed during evidence refresh', { + code: 'HIERARCHY_PROPOSAL_REVISION_CONFLICT', details: { proposalId: duplicate.id }, + }); + } + database.prepare(` + INSERT INTO hierarchy_proposal_revisions ( + proposal_id, revision, updated_at, updated_by, proposal_json, proposal_digest + ) VALUES (?, ?, ?, ?, ?, ?) + `).run(duplicate.id, revision, now, actor, JSON.stringify(proposal), proposedDigest); + this.controlPlane.audit({ + actorId: actor, operation: 'hierarchy.update-evidence', + entityType: 'hierarchy-proposal', entityId: duplicate.id, + details: { + revision, previousDigest: existingDigest, proposalDigest: proposedDigest, + previousWindowEndAt: existingProposal.observation_window.end_at, + observationWindowEndAt: proposal.observation_window.end_at, baseCommit, + }, + }); + database.exec('COMMIT'); + } catch (error) { + try { database.exec('ROLLBACK'); } catch { /* preserve the original failure */ } + throw error; + } + return { + ...rowToProposal(database.prepare('SELECT * FROM hierarchy_proposals WHERE id = ?').get(duplicate.id)), + updatedEvidence: true, + }; + } + + const priorDecision = this.database.prepare(` + SELECT proposal_json FROM hierarchy_proposals + WHERE fingerprint = ? AND state IN ('deferred', 'rejected', 'pilot-approved', 'piloting', 'adopted', 'reversed') + ORDER BY updated_at DESC, created_at DESC LIMIT 1 + `).get(fingerprint); + if (priorDecision) { + const priorProposal = JSON.parse(priorDecision.proposal_json); + if (Date.parse(proposal.observation_window.end_at) + <= Date.parse(priorProposal.observation_window.end_at)) { + throw new TorchError('Reconsidering a decided hierarchy proposal requires a later evidence window', { + code: 'HIERARCHY_EVIDENCE_WINDOW_NOT_ADVANCED', + details: { + previousEndAt: priorProposal.observation_window.end_at, + proposedEndAt: proposal.observation_window.end_at, + }, + }); + } + } + + const now = this.clock().toISOString(); + const record = { + id: this.idFactory(), state: 'proposed', proposer: actor, createdAt: now, updatedAt: now, + baseCommit: git(this.repositoryRoot, ['rev-parse', 'HEAD']), baseGraphDigest, fingerprint, + revision: 1, proposal, decidedBy: null, decidedAt: null, decisionReason: null, + }; + this.database.prepare(` + INSERT INTO hierarchy_proposals ( + id, state, proposer, created_at, updated_at, base_commit, base_graph_digest, + fingerprint, proposal_json, proposal_revision, decided_by, decided_at, decision_reason + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NULL, NULL, NULL) + `).run( + record.id, record.state, record.proposer, record.createdAt, record.updatedAt, + record.baseCommit, record.baseGraphDigest, record.fingerprint, JSON.stringify(record.proposal), + ); + this.database.prepare(` + INSERT INTO hierarchy_proposal_revisions ( + proposal_id, revision, updated_at, updated_by, proposal_json, proposal_digest + ) VALUES (?, 1, ?, ?, ?, ?) + `).run(record.id, record.updatedAt, actor, JSON.stringify(record.proposal), digest(record.proposal)); + this.controlPlane.audit({ + actorId: actor, operation: 'hierarchy.propose', entityType: 'hierarchy-proposal', entityId: record.id, + details: { baseCommit: record.baseCommit, graphRevision: proposal.graph.revision }, + }); + return record; + } + + decide({ proposalId, decision, decidedBy, reason } = {}) { + const record = this.get(proposalId); + if (record.state !== 'proposed') { + throw new TorchError(`Hierarchy proposal ${record.id} is ${record.state}, not proposed`, { + code: 'HIERARCHY_PROPOSAL_STATE_CONFLICT', + }); + } + if (!['approve-pilot', 'defer', 'reject'].includes(decision)) { + throw new TorchError(`Unsupported hierarchy decision: ${decision}`, { code: 'HIERARCHY_DECISION_INVALID' }); + } + const actor = text.parse(decidedBy); + const activeGraph = organizationGraphFromConfig(loadProjectConfig(this.repositoryRoot)); + const ownerIdentity = activeGraph.roles.find((role) => role.kind === 'owner')?.identity_id; + if (actor !== ownerIdentity) { + throw new TorchError('Only the project owner may decide a hierarchy proposal', { + code: 'HIERARCHY_OWNER_AUTHORITY_REQUIRED', details: { actor }, + }); + } + const explanation = decision === 'approve-pilot' ? (reason ? text.parse(reason) : null) : text.parse(reason); + const state = { 'approve-pilot': 'pilot-approved', defer: 'deferred', reject: 'rejected' }[decision]; + const now = this.clock().toISOString(); + this.database.prepare(` + UPDATE hierarchy_proposals SET state = ?, updated_at = ?, decided_by = ?, decided_at = ?, decision_reason = ? + WHERE id = ? AND state = 'proposed' + `).run(state, now, actor, now, explanation, record.id); + this.controlPlane.audit({ + actorId: 'session-manager', operation: `hierarchy.${decision}`, + entityType: 'hierarchy-proposal', entityId: record.id, + details: { decidedBy: actor, reason: explanation, authorityVerifiedAgainst: ownerIdentity }, + }); + return this.get(record.id); + } + + planConclusion({ proposalId, decision, reason } = {}) { + if (!['adopt', 'reverse'].includes(decision)) throw new TorchError('Unsupported pilot conclusion.', { code: 'HIERARCHY_DECISION_INVALID' }); + const explanation = text.parse(reason); + const record = this.get(proposalId); + const config = loadProjectConfig(this.repositoryRoot); + const currentGraph = organizationGraphFromConfig(config); + const currentHead = git(this.repositoryRoot, ['rev-parse', 'HEAD']); + const dirtyEntries = git(this.repositoryRoot, ['status', '--porcelain']).split('\n').filter(Boolean); + const manifest = readInstallManifest(this.repositoryRoot); + const pilotPath = `.torch/hierarchy-pilots/${record.id}.json`; + const blockers = []; + if (record.state !== 'piloting') blockers.push({ type: 'pilot-not-active', state: record.state }); + if (dirtyEntries.length) blockers.push({ type: 'repository-dirty', paths: dirtyEntries }); + let pilot = null; + let pilotDigest = null; + if (existsSync(join(this.repositoryRoot, pilotPath))) { + const bytes = readFileSync(join(this.repositoryRoot, pilotPath), 'utf8'); + pilotDigest = createHash('sha256').update(bytes).digest('hex'); + if (manifest.created.find((entry) => entry.path === pilotPath)?.sha256 !== pilotDigest) { + blockers.push({ type: 'pilot-record-changed' }); + } + pilot = JSON.parse(bytes); + } else blockers.push({ type: 'pilot-record-missing' }); + if (!pilot || digest(currentGraph) !== digest(pilot.graph)) blockers.push({ type: 'pilot-graph-no-longer-active' }); + const latestReview = this.pilotReviews(record.id).at(-1) ?? null; + const now = this.clock().getTime(); + const pilotDueAt = pilot ? Date.parse(pilot.activatedAt) + pilot.pilot.duration_days * 86_400_000 : NaN; + if (decision === 'adopt' && (!latestReview || latestReview.recommendation !== 'adopt' + || latestReview.window.complete !== true || latestReview.graphDigest !== digest(currentGraph) + || !Number.isFinite(pilotDueAt) || now < pilotDueAt + || !Number.isFinite(Date.parse(latestReview.reviewedAt)) || Date.parse(latestReview.reviewedAt) > now)) { + blockers.push({ type: 'adoption-review-required' }); + } + const nextConfig = structuredClone(config); + if (decision === 'reverse' && pilot) { + const priorConfig = JSON.parse(git(this.repositoryRoot, ['show', `${record.baseCommit}:.torch/torch.yaml`])); + const priorGraph = organizationGraphFromConfig(priorConfig); + if (!sameOwners(priorGraph.implementation_owners, currentGraph.implementation_owners)) { + blockers.push({ type: 'implementation-ownership-changed' }); + } + const identities = new Set(['owner', ...this.controlPlane.listAgents().map((agent) => agent.areaId)]); + const missing = priorGraph.roles.filter((role) => !identities.has(role.identity_id)).map((role) => role.identity_id); + if (missing.length) blockers.push({ type: 'prior-identities-missing', identities: missing }); + nextConfig.organization = validateOrganizationGraph({ ...priorGraph, revision: currentGraph.revision + 1 }); + // Retain existing schedules, including pilot additions, for explicit owner review. + // Removing them here could discard an owner-adjusted cadence or running timer contract. + } + const validated = validateProjectConfig(nextConfig); + const activeWork = new BacklogService({ repositoryRoot: this.repositoryRoot, controlPlane: this.controlPlane }) + .list().filter((task) => !['completed', 'cancelled'].includes(task.state)) + .map((task) => ({ id: task.id, owner: task.owner, state: task.state, revision: task.revision })); + const schedulePlan = planManagerCheckInSchedules({ graph: organizationGraphFromConfig(validated), schedules: validated.schedules ?? [] }); + const plan = { action: `${decision}-hierarchy-pilot`, proposalId: record.id, + decision, reason: explanation, currentHead, currentGraphDigest: digest(currentGraph), + pilotDigest, manifestDigest: digest(manifest), state: record.state, + latestReviewId: latestReview?.id ?? null, + evidenceVerification: latestReview?.evidenceVerification ?? 'unavailable', + nextGraph: organizationGraphFromConfig(validated), activeWork, + preservedIdentities: this.controlPlane.listAgents().map((agent) => agent.areaId).sort(), + obsoleteScheduleCandidates: schedulePlan.obsolete.map((schedule) => schedule.id), + schedulesRetained: true, + timerReconciliationRequired: decision === 'reverse' + && (manifest.external ?? []).some((entry) => entry.type === 'systemd-user-unit'), + blockers, canProceed: blockers.length === 0, + mutationPerformed: false, runtimeStarted: false }; + return { ...plan, planHash: digest(plan) }; + } + + concludePilot({ proposalId, decision, reason, decidedBy, approved = false, planHash } = {}) { + this.controlPlane.assertOwnerActor(decidedBy); + if (!approved) throw new TorchError('Pilot conclusion requires explicit owner confirmation.', { code: 'APPROVAL_REQUIRED' }); + const previous = this.get(proposalId); + const receiptPath = `.torch/hierarchy-pilots/${previous.id}.json`; + const receiptBytes = existsSync(join(this.repositoryRoot, receiptPath)) + ? readFileSync(join(this.repositoryRoot, receiptPath), 'utf8') : null; + const receipt = receiptBytes ? JSON.parse(receiptBytes) : null; + if (['adopted', 'reversed'].includes(receipt?.state)) { + const manifest = readInstallManifest(this.repositoryRoot); + if (manifest.created.find((entry) => entry.path === receiptPath)?.sha256 + !== createHash('sha256').update(receiptBytes).digest('hex') + || receipt.conclusion?.planHash !== planHash || receipt.conclusion?.decidedBy !== decidedBy + || receipt.conclusion?.reason !== reason?.trim() + || receipt.state !== ({ adopt: 'adopted', reverse: 'reversed' })[decision]) { + throw new TorchError('Confirmation does not match the recorded conclusion.', { code: 'HIERARCHY_CONCLUSION_PLAN_STALE' }); + } + if (previous.state === receipt.state) return { ...previous, replay: true, mutationPerformed: false, runtimeStarted: false }; + // Recover a completed tracked commit whose local SQLite update was interrupted. + if (previous.state !== 'piloting' + || digest(organizationGraphFromConfig(loadProjectConfig(this.repositoryRoot))) !== digest(receipt.conclusion.resultingGraph) + || git(this.repositoryRoot, ['status', '--porcelain']).trim()) { + throw new TorchError('Committed conclusion needs manual inspection before reconciliation.', { code: 'HIERARCHY_CONCLUSION_BLOCKED' }); + } + const committed = git(this.repositoryRoot, ['show', `HEAD:${receiptPath}`]); + if (digest(JSON.parse(committed)) !== digest(receipt)) throw new TorchError('Conclusion receipt is not committed.', { code: 'HIERARCHY_CONCLUSION_BLOCKED' }); + this.#recordConclusion({ record: previous, state: receipt.state, concludedAt: receipt.conclusion.concludedAt, + decidedBy, decision, reason: receipt.conclusion.reason, reviewId: receipt.conclusion.reviewId, + conclusionCommit: git(this.repositoryRoot, ['log', '-1', '--format=%H', '--', receiptPath]), + graphRevision: receipt.conclusion.resultingGraph.revision, + timerReconciliationRequired: receipt.conclusion.timerReconciliationRequired, + }); + return { ...this.get(previous.id), reconciled: true, mutationPerformed: true, runtimeStarted: false }; + } + const plan = this.planConclusion({ proposalId, decision, reason }); + if (!plan.canProceed) throw new TorchError('Pilot conclusion is blocked.', { code: 'HIERARCHY_CONCLUSION_BLOCKED', details: plan.blockers }); + if (!planHash || planHash !== plan.planHash) throw new TorchError('Review the fresh pilot conclusion plan before confirming.', { + code: 'HIERARCHY_CONCLUSION_PLAN_STALE', + }); + const config = loadProjectConfig(this.repositoryRoot); + if (decision === 'reverse') config.organization = plan.nextGraph; + validateProjectConfig(config); + const record = this.get(proposalId); + const pilotPath = `.torch/hierarchy-pilots/${record.id}.json`; + const pilot = JSON.parse(readFileSync(join(this.repositoryRoot, pilotPath), 'utf8')); + const state = decision === 'adopt' ? 'adopted' : 'reversed'; + const concludedAt = this.clock().toISOString(); + pilot.state = state; + pilot.conclusion = { decidedBy, reason: plan.reason, concludedAt, + reviewId: plan.latestReviewId, previousGraph: pilot.graph, resultingGraph: plan.nextGraph, + planHash: plan.planHash, timerReconciliationRequired: plan.timerReconciliationRequired, + schedulesRetained: true, obsoleteScheduleCandidates: plan.obsoleteScheduleCandidates }; + const manifest = readInstallManifest(this.repositoryRoot); + const files = new Map([[pilotPath, `${JSON.stringify(pilot, null, 2)}\n`]]); + if (decision === 'reverse') files.set('.torch/torch.yaml', `${JSON.stringify(config, null, 2)}\n`); + for (const [path, content] of files) { + const entry = manifest.created.find((item) => item.path === path); + if (!entry) throw new TorchError('Manifest does not own conclusion files.', { code: 'HIERARCHY_INSTALL_MANIFEST_INVALID' }); + entry.sha256 = createHash('sha256').update(content).digest('hex'); + } + files.set('.torch/install-manifest.json', `${JSON.stringify(manifest, null, 2)}\n`); + const originals = writeFilesAtomically(this.repositoryRoot, files); + let conclusionCommit; + const message = `chore(torch): ${decision} hierarchy pilot ${record.id}`; + try { + git(this.repositoryRoot, ['add', ...files.keys()]); + git(this.repositoryRoot, ['commit', '-m', message]); + conclusionCommit = git(this.repositoryRoot, ['rev-parse', 'HEAD']); + } catch (error) { + const head = git(this.repositoryRoot, ['rev-parse', 'HEAD']); + if (head !== plan.currentHead && git(this.repositoryRoot, ['show', '-s', '--format=%s', 'HEAD']) === message) { + conclusionCommit = head; + } else { + try { git(this.repositoryRoot, ['restore', '--staged', '--', ...files.keys()]); } catch { /* best effort */ } + for (const [path, content] of originals) { + const temporary = `${path}.rollback-${process.pid}-${randomUUID()}`; + writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + renameSync(temporary, path); + } + throw new TorchError('Conclusion files restored after commit failure.', { code: 'HIERARCHY_CONCLUSION_COMMIT_FAILED', details: error.message }); + } + } + this.#recordConclusion({ record, state, concludedAt, decidedBy, decision, + reason: plan.reason, reviewId: plan.latestReviewId, conclusionCommit, + graphRevision: plan.nextGraph.revision, timerReconciliationRequired: plan.timerReconciliationRequired }); + return { ...this.get(record.id), conclusionCommit, concludedAt, + obsoleteScheduleCandidates: plan.obsoleteScheduleCandidates, + timerReconciliationRequired: plan.timerReconciliationRequired, + runtimeStarted: false, mutationPerformed: true }; + } + + #recordConclusion({ record, state, concludedAt, decidedBy, decision, reason, reviewId, + conclusionCommit, graphRevision, timerReconciliationRequired }) { + this.database.exec('BEGIN IMMEDIATE'); + try { + const update = this.database.prepare(`UPDATE hierarchy_proposals SET state = ?, updated_at = ?, decision_reason = ? + WHERE id = ? AND state = 'piloting'`).run(state, concludedAt, reason, record.id); + if (update.changes !== 1) throw new TorchError('Pilot state changed before conclusion was recorded.', { + code: 'HIERARCHY_CONCLUSION_BLOCKED', + }); + this.controlPlane.auditOwnerAction({ actorId: decidedBy, operation: 'hierarchy.conclude-pilot', + entityType: 'hierarchy-proposal', entityId: record.id, + details: { decision, reason, reviewId, conclusionCommit, graphRevision, timerReconciliationRequired }, + }); + this.database.exec('COMMIT'); + } catch (error) { + try { this.database.exec('ROLLBACK'); } catch { /* preserve original failure */ } + throw error; + } + } + + planPilot(proposalId) { + const record = this.get(proposalId); + const config = loadProjectConfig(this.repositoryRoot); + const currentGraph = organizationGraphFromConfig(config); + const currentHead = git(this.repositoryRoot, ['rev-parse', 'HEAD']); + const dirtyEntries = git(this.repositoryRoot, ['status', '--porcelain']).split('\n').filter(Boolean); + const currentGraphDigest = digest(currentGraph); + const manifest = readInstallManifest(this.repositoryRoot); + const activeIdentities = new Set(['owner', ...this.controlPlane.listAgents().map((agent) => agent.areaId)]); + const activeTasks = new BacklogService({ + repositoryRoot: this.repositoryRoot, controlPlane: this.controlPlane, + }).list().filter((task) => [ + 'assigned', 'in_progress', 'blocked', 'verification', 'ready_to_integrate', + ].includes(task.state)); + const worktrees = (manifest.external ?? []) + .filter((entry) => entry.type === 'worktree') + .map(({ area, path }) => ({ area, path })); + const installedLaunchers = (manifest.external ?? []).filter((entry) => entry.type === 'systemd-user-unit'); + const blockers = []; + blockers.push(...installedLaunchers + .filter((entry) => !existsSync(entry.path) || fileDigest(entry.path) !== entry.sha256) + .map((entry) => ({ type: 'schedule-launcher-integrity', path: entry.path }))); + if (record.state !== 'pilot-approved') blockers.push({ type: 'owner-pilot-approval-required', state: record.state }); + if (currentHead !== record.baseCommit) blockers.push({ type: 'stale-base-commit', expected: record.baseCommit, actual: currentHead }); + if (currentGraphDigest !== record.baseGraphDigest) blockers.push({ type: 'organization-changed', expected: record.baseGraphDigest, actual: currentGraphDigest }); + if (dirtyEntries.length) blockers.push({ type: 'repository-dirty', entries: dirtyEntries }); + if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,100}$/.test(record.id)) { + blockers.push({ type: 'unsafe-proposal-id', proposalId: record.id }); + } + const pilotRecordPath = `.torch/hierarchy-pilots/${record.id}.json`; + if (existsSync(join(this.repositoryRoot, pilotRecordPath)) + || (manifest.created ?? []).some((entry) => entry.path === pilotRecordPath)) { + blockers.push({ type: 'pilot-record-already-exists', path: pilotRecordPath }); + } + const trackedPaths = git(this.repositoryRoot, ['ls-files', '.torch/torch.yaml', '.torch/install-manifest.json']) + .split('\n').filter(Boolean); + if (!trackedPaths.includes('.torch/torch.yaml') || !trackedPaths.includes('.torch/install-manifest.json')) { + blockers.push({ type: 'configuration-not-committed' }); + } + const missingIdentities = [...new Set(record.proposal.graph.roles.map((role) => role.identity_id))] + .filter((identity) => !activeIdentities.has(identity)); + if (missingIdentities.length) blockers.push({ type: 'inactive-identities', identities: missingIdentities }); + const before = new Map(currentGraph.roles.map((entry) => [entry.id, entry])); + const after = new Map(record.proposal.graph.roles.map((entry) => [entry.id, entry])); + let checkInSchedulePlan; + try { + checkInSchedulePlan = planManagerCheckInSchedules({ + graph: record.proposal.graph, schedules: config.schedules, + }); + } catch (error) { + blockers.push({ type: 'manager-check-in-schedule-plan-invalid', code: error.code, details: error.details ?? error.message }); + checkInSchedulePlan = { additions: [], obsolete: [], schedules: config.schedules }; + } + const nextConfig = { + ...config, organization: record.proposal.graph, schedules: checkInSchedulePlan.schedules, + }; + try { + validateProjectConfig(nextConfig); + } catch (error) { + blockers.push({ type: 'resulting-configuration-invalid', details: error.details ?? error.message }); + } + const configEntry = (manifest.created ?? []).find((entry) => entry.path === '.torch/torch.yaml'); + if (!configEntry) { + blockers.push({ type: 'install-manifest-missing-config-ownership' }); + } else if (fileDigest(join(this.repositoryRoot, '.torch', 'torch.yaml')) !== configEntry.sha256) { + blockers.push({ type: 'install-manifest-config-hash-mismatch' }); + } + return { + action: 'activate-hierarchy-pilot', proposalId: record.id, state: record.state, + baseCommit: record.baseCommit, currentHead, baseGraphDigest: record.baseGraphDigest, + graphDelta: { + addedRoles: [...after.keys()].filter((roleId) => !before.has(roleId)), + removedRoles: [...before.keys()].filter((roleId) => !after.has(roleId)), + changedRoles: [...after.keys()].filter((roleId) => before.has(roleId) + && digest(after.get(roleId)) !== digest(before.get(roleId))), + }, + managerCheckInSchedules: { + additions: checkInSchedulePlan.additions, + obsoleteCandidates: checkInSchedulePlan.obsolete.map((schedule) => schedule.id), + resultingSchedules: checkInSchedulePlan.schedules, + note: 'Activation writes the resulting schedules to canonical config; obsolete definitions remain for owner review.', + }, + timerReconciliation: { + installed: installedLaunchers.length > 0, + requiredAfterActivation: installedLaunchers.length > 0, + commands: installedLaunchers.length + ? ['torch schedules launcher plan-reconcile', 'torch schedules launcher reconcile --yes'] : [], + note: 'Hierarchy activation never mutates user systemd state or starts an AI runtime.', + }, + affectedIdentityIds: record.proposal.impact.affected_identities, + activeBacklog: activeTasks.map((task) => ({ id: task.id, state: task.state, owner: task.owner })), + worktrees, + pilot: record.proposal.pilot, + preservation: ['active backlog assignments', 'implementation ownership', 'existing session identities', 'direct peer communication', 'single authoritative backlog'], + blockers, + canProceed: blockers.length === 0, + mutationPerformed: false, + }; + } + + activatePilot({ proposalId, activatedBy } = {}) { + const record = this.get(proposalId); + if (record.state !== 'pilot-approved') { + throw new TorchError(`Hierarchy proposal ${record.id} is ${record.state}, not pilot-approved`, { + code: 'HIERARCHY_PROPOSAL_STATE_CONFLICT', + }); + } + const actor = text.parse(activatedBy); + if (actor !== record.decidedBy || actor !== 'owner') { + throw new TorchError('Hierarchy pilot activation must be confirmed by the owner who approved it', { + code: 'HIERARCHY_OWNER_AUTHORITY_REQUIRED', details: { decidedBy: record.decidedBy, activatedBy: actor }, + }); + } + const plan = this.planPilot(record.id); + if (!plan.canProceed) throw new TorchError('Hierarchy pilot activation is blocked', { + code: 'HIERARCHY_PILOT_BLOCKED', details: plan.blockers, + }); + const config = loadProjectConfig(this.repositoryRoot); + const schedulePlan = planManagerCheckInSchedules({ graph: record.proposal.graph, schedules: config.schedules }); + const nextConfig = validateProjectConfig({ + ...config, organization: record.proposal.graph, schedules: schedulePlan.schedules, + }); + const manifest = readInstallManifest(this.repositoryRoot); + const activatedAt = this.clock().toISOString(); + const relativeRecordPath = `.torch/hierarchy-pilots/${record.id}.json`; + const pilotRecord = { + schema: 'torch.dev/hierarchy-pilot/v1alpha1', proposalId: record.id, + state: 'piloting', title: record.proposal.title, rationale: record.proposal.rationale, + baseCommit: record.baseCommit, graph: record.proposal.graph, + pilot: record.proposal.pilot, activatedBy: actor, activatedAt, + managerCheckInSchedules: { + additions: schedulePlan.additions, + obsoleteCandidates: schedulePlan.obsolete.map((schedule) => schedule.id), + }, + timerReconciliationRequired: (manifest.external ?? []).some((entry) => entry.type === 'systemd-user-unit'), + }; + const serializedConfig = `${JSON.stringify(nextConfig, null, 2)}\n`; + const serializedPilot = `${JSON.stringify(pilotRecord, null, 2)}\n`; + const configEntry = manifest.created.find((entry) => entry.path === '.torch/torch.yaml'); + if (!configEntry) throw new TorchError('Install manifest does not own the tracked project configuration', { + code: 'HIERARCHY_INSTALL_MANIFEST_INVALID', details: { path: '.torch/torch.yaml' }, + }); + configEntry.sha256 = createHash('sha256').update(serializedConfig).digest('hex'); + manifest.created.push({ + path: relativeRecordPath, + sha256: createHash('sha256').update(serializedPilot).digest('hex'), + }); + const files = new Map([ + ['.torch/torch.yaml', serializedConfig], + [relativeRecordPath, serializedPilot], + ['.torch/install-manifest.json', `${JSON.stringify(manifest, null, 2)}\n`], + ]); + const originals = writeFilesAtomically(this.repositoryRoot, files); + const commitMessage = `chore(torch): activate hierarchy pilot ${record.id}`; + let activationCommit; + try { + git(this.repositoryRoot, ['add', ...files.keys()]); + git(this.repositoryRoot, ['commit', '-m', commitMessage]); + activationCommit = git(this.repositoryRoot, ['rev-parse', 'HEAD']); + } catch (error) { + const currentHead = git(this.repositoryRoot, ['rev-parse', 'HEAD']); + const currentSubject = git(this.repositoryRoot, ['show', '-s', '--format=%s', 'HEAD']); + if (currentHead !== plan.currentHead && currentSubject === commitMessage) { + activationCommit = currentHead; + } else { + try { git(this.repositoryRoot, ['restore', '--staged', '--', ...files.keys()]); } catch { /* best effort */ } + for (const [path, content] of originals) { + if (content === null) { + if (existsSync(path)) unlinkSync(path); + } else { + const temporary = `${path}.rollback-${process.pid}-${randomUUID()}`; + writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + renameSync(temporary, path); + } + } + throw new TorchError('Hierarchy files were restored because the activation commit failed', { + code: 'HIERARCHY_ACTIVATION_COMMIT_FAILED', details: error.stderr?.toString().trim() || error.message, + }); + } + } + const now = this.clock().toISOString(); + this.database.prepare(` + UPDATE hierarchy_proposals SET state = 'piloting', activation_commit = ?, updated_at = ? + WHERE id = ? AND state = 'pilot-approved' + `).run(activationCommit, now, record.id); + this.controlPlane.audit({ + actorId: 'session-manager', operation: 'hierarchy.activate-pilot', + entityType: 'hierarchy-proposal', entityId: record.id, + details: { activatedBy: actor, activationCommit, graphRevision: record.proposal.graph.revision, + managerScheduleAdditions: schedulePlan.additions.map((schedule) => schedule.id), + timerReconciliationRequired: pilotRecord.timerReconciliationRequired }, + }); + return { + ...this.get(record.id), activationCommit, activatedAt, + managerCheckInSchedules: pilotRecord.managerCheckInSchedules, + timerReconciliationRequired: pilotRecord.timerReconciliationRequired, + runtimeStarted: false, mutationPerformed: true, + }; + } +} diff --git a/src/evolution/service.mjs b/src/evolution/service.mjs index 5ed90ed..7db7a8b 100644 --- a/src/evolution/service.mjs +++ b/src/evolution/service.mjs @@ -74,14 +74,15 @@ function pathsOverlap(left, right) { return Boolean(a && b && (a.startsWith(`${b}/`) || b.startsWith(`${a}/`))); } -function normalizeDomain(input) { +function normalizeDomain(input, defaultRuntime) { const id = requiredText(input?.id, 'domain.id'); if (!/^[a-z0-9][a-z0-9-]*$/.test(id) || id === 'session-manager') { throw new TorchError(`Invalid proposed domain ID: ${id}`, { code: 'INVALID_FLEET_CHANGE', details: { field: 'domain.id' }, }); } - const runtime = requiredText(input.runtime ?? 'claude', 'domain.runtime'); + const runtime = requiredText(input.runtime === 'default' || input.runtime == null + ? defaultRuntime : input.runtime, 'domain.runtime'); const model = input.model === undefined || input.model === null ? null : requiredText(input.model, 'domain.model'); const branch = input.branch === undefined || input.branch === null @@ -327,7 +328,8 @@ export class FleetEvolutionService { code: 'FLEET_CHANGE_AUTHORITY_REQUIRED', details: { actor }, }); } - const domain = normalizeDomain(input); + const config = loadProjectConfig(this.repositoryRoot); + const domain = normalizeDomain(input, config.runtimes.default); const agents = this.controlPlane.listAgents(); if (agents.some((agent) => agent.areaId === domain.id)) { throw new TorchError(`Fleet identity already exists: ${domain.id}`, { code: 'FLEET_IDENTITY_EXISTS' }); @@ -343,7 +345,6 @@ export class FleetEvolutionService { code: 'FLEET_OWNERSHIP_COLLISION', details: collisions, }); } - const config = loadProjectConfig(this.repositoryRoot); if (!config.runtimes?.[domain.runtime]) { throw new TorchError(`Runtime is not configured for the Fleet: ${domain.runtime}`, { code: 'FLEET_RUNTIME_NOT_CONFIGURED', details: { runtime: domain.runtime }, @@ -470,7 +471,7 @@ export class FleetEvolutionService { code: 'INVALID_FLEET_CHANGE', details: { field: 'resultDomains' }, }); } - const resultDomains = resultInput.map(normalizeDomain); + const resultDomains = resultInput.map((input) => normalizeDomain(input, config.runtimes.default)); if ((changeType === 'merge-domains' && resultDomains.length !== 1) || (changeType === 'split-domain' && resultDomains.length < 2)) { throw new TorchError(`${changeType} has invalid result-domain cardinality`, { diff --git a/src/forge/service.mjs b/src/forge/service.mjs index 4482f3b..247ef85 100644 --- a/src/forge/service.mjs +++ b/src/forge/service.mjs @@ -80,6 +80,86 @@ export function forgeStatus({ repositoryRoot } = {}) { }; } +export function planForgeSync({ repositoryRoot } = {}) { + const config = loadProjectConfig(repositoryRoot); + const remote = config.forge?.remote ?? null; + const branch = config.project.main_branch; + const localCommit = git(repositoryRoot, ['rev-parse', `refs/heads/${branch}`]).stdout; + const remotes = git(repositoryRoot, ['remote']).stdout.split('\n').filter(Boolean); + const blockers = []; + if (config.forge?.provider === 'none' || !remote) { + blockers.push({ code: 'FORGE_NOT_ATTACHED' }); + } + if (config.repository.canonical.type !== 'remote' + || config.repository.canonical.remote !== remote) { + blockers.push({ code: 'FORGE_CANONICAL_CHANGED', canonical: config.repository.canonical }); + } + if (remote && !remotes.includes(remote)) blockers.push({ code: 'FORGE_REMOTE_MISSING', remote }); + const branchCheck = git(repositoryRoot, ['check-ref-format', '--branch', branch]); + if (branchCheck.status !== 0) blockers.push({ code: 'FORGE_TARGET_BRANCH_INVALID', branch }); + const observed = remote && remotes.includes(remote) + ? remoteHead(repositoryRoot, remote, branch) + : { available: false, commit: null, error: 'configured remote is unavailable' }; + if (!observed.available) blockers.push({ code: 'FORGE_UNAVAILABLE', remote, error: observed.error }); + + let disposition = 'blocked'; + if (blockers.length === 0) { + if (observed.commit === localCommit) disposition = 'already-synchronized'; + else if (!observed.commit) disposition = 'publish-new-branch'; + else if (git(repositoryRoot, ['cat-file', '-e', `${observed.commit}^{commit}`], { timeout: 5_000 }).status !== 0) { + blockers.push({ code: 'FORGE_SYNC_FETCH_REQUIRED', remoteCommit: observed.commit, + message: `Fetch ${remote}/${branch} before syncing so TORCH can verify ancestry.` }); + } else if (git(repositoryRoot, [ + 'merge-base', '--is-ancestor', localCommit, observed.commit, + ], { timeout: 5_000 }).status === 0) { + blockers.push({ code: 'FORGE_REMOTE_AHEAD', localCommit, remoteCommit: observed.commit }); + } else if (git(repositoryRoot, [ + 'merge-base', '--is-ancestor', observed.commit, localCommit, + ], { timeout: 5_000 }).status === 0) disposition = 'fast-forward-remote'; + else blockers.push({ code: 'FORGE_REMOTE_DIVERGED', localCommit, remoteCommit: observed.commit }); + } + return { + action: 'sync-forge-canonical-branch', remote, branch, localCommit, + remoteCommit: observed.commit, disposition, requiresConfirmation: disposition !== 'already-synchronized', + blockers, canProceed: blockers.length === 0, mutationPerformed: false, + }; +} + +export function syncForgeRemote({ repositoryRoot, controlPlane, actorId = 'owner' } = {}) { + if (!controlPlane) throw new TorchError('Forge synchronization requires the project control plane for owner audit', { + code: 'FORGE_AUDIT_REQUIRED', + }); + controlPlane.assertOwnerActor(actorId); + const plan = planForgeSync({ repositoryRoot }); + if (!plan.canProceed) throw new TorchError('Forge synchronization is blocked', { + code: 'FORGE_SYNC_BLOCKED', details: plan.blockers, + }); + if (plan.disposition === 'already-synchronized') { + return { ...plan, synchronized: true, publishedCommit: null, mutationPerformed: false }; + } + const result = git(repositoryRoot, [ + 'push', '--porcelain', '--', plan.remote, `${plan.localCommit}:refs/heads/${plan.branch}`, + ], { timeout: 60_000 }); + if (result.status !== 0) throw new TorchError('Forge rejected canonical synchronization; no force push was attempted', { + code: 'FORGE_SYNC_PUSH_REJECTED', details: { remote: plan.remote, branch: plan.branch, stderr: result.stderr }, + }); + const observed = remoteHead(repositoryRoot, plan.remote, plan.branch); + const synchronized = observed.available && observed.commit === plan.localCommit; + controlPlane.auditOwnerAction({ + actorId, operation: 'forge.sync', entityType: 'forge-publication', entityId: plan.localCommit, + details: { + remote: plan.remote, branch: plan.branch, previousRemoteCommit: plan.remoteCommit, + publishedCommit: plan.localCommit, observedRemoteCommit: observed.commit, + synchronized, + }, + }); + return { + ...plan, synchronized, publishedCommit: plan.localCommit, + observedRemoteCommit: observed.commit, mutationPerformed: true, + output: result.stdout, pendingSynchronization: !synchronized, + }; +} + export function planForgeAttach({ repositoryRoot, remote, provider = 'generic-git' } = {}) { const selectedRemote = text(remote, 'remote'); const selectedProvider = text(provider, 'provider'); diff --git a/src/importers/combatrig.mjs b/src/importers/combatrig.mjs index 9b3823d..ae5bde6 100644 --- a/src/importers/combatrig.mjs +++ b/src/importers/combatrig.mjs @@ -3,6 +3,7 @@ import { execFileSync } from 'node:child_process'; import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { dirname, join, relative } from 'node:path'; import { TorchError } from '../kernel/errors.mjs'; +import { workingTreeFingerprint } from '../kernel/git.mjs'; function json(path, code) { try { return JSON.parse(readFileSync(path, 'utf8')); } catch (error) { @@ -37,20 +38,71 @@ function listedWorktrees(root) { return records; } -function backlogInventory(root) { +function backlogInventory(root, areaMap = new Map()) { const directory = join(root, 'docs', 'agents', 'backlog'); - if (!existsSync(directory)) return { total: 0, byStatus: {}, invalid: [], examples: [] }; + if (!existsSync(directory)) return { + total: 0, byStatus: {}, byArea: {}, invalid: [], examples: [], + migration: { + mode: 'assessment-only', unmappedAreas: [], sessionManagerAreaItems: 0, + assignedItems: 0, freeformAssigneeLabels: 0, legacyDoneItems: 0, + observedAtMissing: 0, observedAtInvalid: 0, canImportAutomatically: false, + }, + }; const byStatus = {}; + const byArea = {}; + const unmappedAreas = {}; const invalid = []; const examples = []; + let sessionManagerAreaItems = 0; + let assignedItems = 0; + let freeformAssigneeLabels = 0; + let legacyDoneItems = 0; + let observedAtMissing = 0; + let observedAtInvalid = 0; for (const name of readdirSync(directory).filter((entry) => entry.endsWith('.json')).sort()) { try { const item = json(join(directory, name), 'COMBATRIG_BACKLOG_INVALID'); - byStatus[item.status ?? 'unknown'] = (byStatus[item.status ?? 'unknown'] ?? 0) + 1; + const status = item.status ?? 'unknown'; + const area = typeof item.area === 'string' && item.area.trim() ? item.area.trim() : '(missing)'; + byStatus[status] = (byStatus[status] ?? 0) + 1; + byArea[area] = (byArea[area] ?? 0) + 1; + if (!areaMap.has(area)) unmappedAreas[area] = (unmappedAreas[area] ?? 0) + 1; + if (areaMap.get(area) === 'session-manager') sessionManagerAreaItems += 1; + if (status === 'assigned') { + assignedItems += 1; + if (typeof item.assignedTo === 'string' && item.assignedTo.trim()) freeformAssigneeLabels += 1; + } + if (status === 'done') legacyDoneItems += 1; + if (item.observedAt === undefined || item.observedAt === null || item.observedAt === '') { + observedAtMissing += 1; + } else if (typeof item.observedAt !== 'string' || !/^[0-9a-f]{7,64}$/i.test(item.observedAt)) { + observedAtInvalid += 1; + } if (examples.length < 8) examples.push({ id: item.id ?? name.slice(0, -5), area: item.area ?? null, status: item.status ?? null }); } catch { invalid.push(name); } } - return { total: Object.values(byStatus).reduce((sum, count) => sum + count, 0), byStatus, invalid, examples }; + return { + total: Object.values(byStatus).reduce((sum, count) => sum + count, 0), byStatus, byArea, invalid, examples, + migration: { + mode: 'assessment-only', + statusCorrespondence: { + open: { target: 'ready' }, + assigned: { target: 'assigned', ownerResolution: 'review legacy area and free-form assignedTo; no identity is inferred' }, + blocked: { target: 'blocked', requires: 'owner-reviewed blocked reason' }, + done: { target: 'completed', requires: ['commit', 'evidence', 'landed integration request'] }, + }, + unmappedAreas: Object.entries(unmappedAreas).sort(([left], [right]) => left.localeCompare(right)) + .map(([area, count]) => ({ area, count })), + sessionManagerAreaItems, + assignedItems, + freeformAssigneeLabels, + legacyDoneItems, + observedAtMissing, + observedAtInvalid, + canImportAutomatically: false, + note: 'This report does not create Torch backlog tasks, transfer ownership, or mark legacy work verified.', + }, + }; } function packageChecks(root) { @@ -158,7 +210,10 @@ export function analyzeCombatrigFleet({ repository } = {}) { return { schema: 'torch.dev/domain-proposal/v1alpha1', generatedAt: new Date().toISOString(), - repository: { root, initialCommit: repository.initialCommit, head: repository.head }, + repository: { + root, initialCommit: repository.initialCommit, head: repository.head, + workingTreeFingerprint: workingTreeFingerprint(root), + }, review: { status: 'pending', reviewedAt: null, reviewedBy: null, notes: ['Imported from COMBATRIG portable fleet. Resolve every blocking migration gap before approval.'], @@ -183,7 +238,7 @@ export function analyzeCombatrigFleet({ repository } = {}) { compatibility: { source: 'combatrig-portable-fleet', sourceRoster: 'docs/agents/roster.json', areas: { total: converted.length, domains: domains.length, managerLegacyId: manager?.legacy_id ?? null }, - backlog: backlogInventory(root), + backlog: backlogInventory(root, idMap), checkCatalog: checks.catalog, schedules, release: { provider: 'custom-command', authority: 'owner', command: 'tools/dev/release.sh' }, diff --git a/src/integration/service.mjs b/src/integration/service.mjs index 53ca4e6..c996634 100644 --- a/src/integration/service.mjs +++ b/src/integration/service.mjs @@ -3,6 +3,7 @@ import { spawnSync } from 'node:child_process'; import { TorchError } from '../kernel/errors.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; import { loadFleetDefinition } from '../kernel/worktrees.mjs'; +import { BacklogService } from '../backlog/service.mjs'; function git(root, args, { allowFailure = false } = {}) { const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); @@ -118,22 +119,30 @@ export class IntegrationService { list({ state } = {}) { const rows = state ? this.controlPlane.database.prepare(` - SELECT * FROM integration_requests WHERE state = ? ORDER BY created_at, id + SELECT * FROM integration_requests WHERE state = ? ORDER BY rowid `).all(state) - : this.controlPlane.database.prepare('SELECT * FROM integration_requests ORDER BY created_at, id').all(); + : this.controlPlane.database.prepare('SELECT * FROM integration_requests ORDER BY rowid').all(); return rows.map(integrationRow); } evaluate(requestId) { const request = this.get(requestId); if (request.state === 'landed' || request.state === 'superseded') return request; - if (request.state === 'landing' - && git(this.repositoryRoot, ['rev-parse', request.targetBranch]).stdout === request.sourceCommit) { + const targetCommit = git(this.repositoryRoot, ['rev-parse', request.targetBranch]).stdout; + if (request.authorizedBy + && (this.policy.landing_authority ?? []).includes(request.authorizedBy) + && git(this.repositoryRoot, ['merge-base', '--is-ancestor', request.sourceCommit, targetCommit], { allowFailure: true }).status === 0 + && this.checkService.exactPasses({ commit: request.sourceCommit, requiredChecks: request.requiredChecks })) { const landedAt = this.clock().toISOString(); this.controlPlane.database.prepare(` UPDATE integration_requests SET state = 'landed', reason = NULL, landed_at = ?, updated_at = ? WHERE id = ? `).run(landedAt, landedAt, request.id); + this.controlPlane.audit({ + actorId: request.authorizedBy, operation: 'integration.reconcile-landed', + entityType: 'integration-request', entityId: request.id, + details: { commit: request.sourceCommit, target: request.targetBranch }, + }); return this.get(request.id); } let state = 'ready'; @@ -147,7 +156,16 @@ export class IntegrationService { 'merge-base', '--is-ancestor', request.baseTargetCommit, request.sourceCommit, ], { allowFailure: true }).status !== 0) { state = 'needs_convergence'; reason = 'source-does-not-contain-request-target'; - } else if (!this.checkService.exactPasses({ + } else { + const targetCommit = git(this.repositoryRoot, ['rev-parse', request.targetBranch]).stdout; + if (targetCommit !== request.baseTargetCommit + && git(this.repositoryRoot, [ + 'merge-base', '--is-ancestor', targetCommit, request.sourceCommit, + ], { allowFailure: true }).status !== 0) { + state = 'needs_convergence'; reason = 'target-advanced-since-request'; + } + } + if (state === 'ready' && !this.checkService.exactPasses({ commit: request.sourceCommit, requiredChecks: request.requiredChecks, })) { state = 'testing'; reason = 'required-exact-sha-checks-missing'; @@ -182,6 +200,15 @@ export class IntegrationService { return this.get(request.id); } + firstReadyRequest(actorId) { + for (const queued of this.list()) { + if (queued.authorizedBy !== actorId) continue; + const current = this.evaluate(queued.id); + if (current.state === 'ready') return current; + } + return null; + } + planLanding({ requestId, actorId } = {}) { const actor = this.controlPlane.assertIdentity(actorId); const request = this.evaluate(requestId); @@ -189,8 +216,17 @@ export class IntegrationService { if (!(this.policy.landing_authority ?? []).includes(actor)) blockers.push({ code: 'LANDING_AUTHORITY_REQUIRED' }); if (request.authorizedBy !== actor) blockers.push({ code: 'REQUEST_NOT_AUTHORIZED_BY_ACTOR' }); if (request.state !== 'ready') blockers.push({ code: 'INTEGRATION_NOT_READY', state: request.state, reason: request.reason }); + if (request.state === 'ready') { + const firstReady = this.firstReadyRequest(actor); + if (firstReady && firstReady.id !== request.id) { + blockers.push({ code: 'INTEGRATION_QUEUE_ORDER_REQUIRED', nextRequestId: firstReady.id }); + } + } const currentTarget = git(this.repositoryRoot, ['rev-parse', request.targetBranch]).stdout; - if (this.policy.require_current_main !== false && currentTarget !== request.baseTargetCommit) { + if (this.policy.require_current_main !== false && currentTarget !== request.baseTargetCommit + && git(this.repositoryRoot, [ + 'merge-base', '--is-ancestor', currentTarget, request.sourceCommit, + ], { allowFailure: true }).status !== 0) { blockers.push({ code: 'TARGET_ADVANCED', expected: request.baseTargetCommit, actual: currentTarget }); } const currentBranch = git(this.repositoryRoot, ['branch', '--show-current']).stdout; @@ -207,38 +243,132 @@ export class IntegrationService { } land({ requestId, actorId } = {}) { - const plan = this.planLanding({ requestId, actorId }); - if (!plan.canProceed) { - throw new TorchError('Integration landing refused by main-protection policy', { - code: 'INTEGRATION_LANDING_BLOCKED', details: plan.blockers, - }); - } - const landingAt = this.clock().toISOString(); - this.controlPlane.database.prepare(` - UPDATE integration_requests SET state = 'landing', reason = NULL, updated_at = ? WHERE id = ? - `).run(landingAt, plan.request.id); - this.controlPlane.audit({ - actorId, operation: 'integration.landing-start', entityType: 'integration-request', entityId: plan.request.id, - details: { commit: plan.request.sourceCommit, target: plan.request.targetBranch }, - }); + const database = this.controlPlane.database; try { - git(this.repositoryRoot, ['merge', '--ff-only', plan.request.sourceCommit]); + database.exec('BEGIN IMMEDIATE'); } catch (error) { - const failedAt = this.clock().toISOString(); - this.controlPlane.database.prepare(` - UPDATE integration_requests SET state = 'blocked', reason = ?, updated_at = ? WHERE id = ? - `).run('git-fast-forward-failed', failedAt, plan.request.id); + if (/busy|locked/i.test(error.message ?? '')) { + throw new TorchError('Another integration or project-state writer holds the landing queue. Retry after it finishes.', { + code: 'INTEGRATION_LANDING_BUSY', details: { requestId }, + }); + } throw error; } - const landedAt = this.clock().toISOString(); - this.controlPlane.database.prepare(` - UPDATE integration_requests - SET state = 'landed', reason = NULL, landed_at = ?, updated_at = ? WHERE id = ? - `).run(landedAt, landedAt, plan.request.id); - this.controlPlane.audit({ - actorId, operation: 'integration.land', entityType: 'integration-request', entityId: plan.request.id, - details: { commit: plan.request.sourceCommit, target: plan.request.targetBranch }, - }); - return this.get(plan.request.id); + let transactionFinished = false; + try { + // This write transaction is the per-project landing mutex. A second + // Session Manager process cannot pass the target/ref checks until this + // one has either landed or failed. + const plan = this.planLanding({ requestId, actorId }); + if (!plan.canProceed) { + throw new TorchError('Integration landing refused by main-protection policy', { + code: 'INTEGRATION_LANDING_BLOCKED', details: plan.blockers, + }); + } + const landingAt = this.clock().toISOString(); + database.prepare(` + UPDATE integration_requests SET state = 'landing', reason = NULL, updated_at = ? WHERE id = ? + `).run(landingAt, plan.request.id); + this.controlPlane.audit({ + actorId, operation: 'integration.landing-start', entityType: 'integration-request', entityId: plan.request.id, + details: { commit: plan.request.sourceCommit, target: plan.request.targetBranch }, + }); + try { + git(this.repositoryRoot, ['merge', '--ff-only', plan.request.sourceCommit]); + } catch (error) { + const failedAt = this.clock().toISOString(); + database.prepare(` + UPDATE integration_requests SET state = 'blocked', reason = ?, updated_at = ? WHERE id = ? + `).run('git-fast-forward-failed', failedAt, plan.request.id); + this.controlPlane.audit({ + actorId, operation: 'integration.landing-failed', + entityType: 'integration-request', entityId: plan.request.id, + details: { commit: plan.request.sourceCommit, target: plan.request.targetBranch }, + }); + database.exec('COMMIT'); + transactionFinished = true; + throw error; + } + const landedAt = this.clock().toISOString(); + database.prepare(` + UPDATE integration_requests + SET state = 'landed', reason = NULL, landed_at = ?, updated_at = ? WHERE id = ? + `).run(landedAt, landedAt, plan.request.id); + this.controlPlane.audit({ + actorId, operation: 'integration.land', entityType: 'integration-request', entityId: plan.request.id, + details: { commit: plan.request.sourceCommit, target: plan.request.targetBranch }, + }); + database.exec('COMMIT'); + transactionFinished = true; + const landed = this.get(plan.request.id); + // Closure is a post-landing reconciliation. Never report a successful + // Git landing as failed just because its task bookkeeping needs review. + try { + const { config } = loadFleetDefinition(this.repositoryRoot); + if (config.backlog?.activity?.auto_close_after_landing === true) { + const backlog = new BacklogService({ repositoryRoot: this.repositoryRoot, + controlPlane: this.controlPlane, checkService: this.checkService, + integrationLookup: (id) => this.get(id), clock: this.clock }); + landed.backlogClosure = backlog.reconcileLanded({ integrationRequest: landed.id, actorId, automatic: true }); + } + } catch (error) { + landed.backlogClosure = { disposition: 'needs-review', reason: error.code ?? error.message }; + try { + this.controlPlane.audit({ actorId, operation: 'backlog.landed-closure-needs-review', + entityType: 'integration-request', entityId: landed.id, details: landed.backlogClosure }); + } catch (auditError) { landed.backlogClosure.auditError = auditError.code ?? auditError.message; } + } + return landed; + } catch (error) { + if (!transactionFinished) database.exec('ROLLBACK'); + throw error; + } + } + + drain({ actorId, limit = 50 } = {}) { + const actor = this.controlPlane.assertIdentity(actorId); + if (!(this.policy.landing_authority ?? []).includes(actor)) { + throw new TorchError(`${actor} lacks landing authority`, { + code: 'INTEGRATION_AUTHORITY_REQUIRED', details: { allowed: this.policy.landing_authority ?? [] }, + }); + } + if (!Number.isInteger(limit) || limit < 1 || limit > 500) { + throw new TorchError('Integration queue drain limit must be between 1 and 500', { + code: 'INTEGRATION_DRAIN_LIMIT_INVALID', details: { limit }, + }); + } + const candidates = this.list().filter((request) => request.authorizedBy === actor + && ['ready', 'testing'].includes(request.state)).slice(0, limit); + const results = []; + for (const candidate of candidates) { + const current = this.evaluate(candidate.id); + if (current.state !== 'ready') { + results.push({ requestId: candidate.id, state: current.state, reason: current.reason, landed: false }); + continue; + } + try { + const landed = this.land({ requestId: candidate.id, actorId: actor }); + results.push({ requestId: candidate.id, state: landed.state, landed: landed.state === 'landed', + ...(landed.backlogClosure ? { backlogClosure: landed.backlogClosure } : {}) }); + } catch (error) { + if (error.code === 'INTEGRATION_LANDING_BUSY') { + results.push({ requestId: candidate.id, state: 'queued', reason: 'another-lander-active', landed: false, retryable: true }); + break; + } + if (error.code === 'INTEGRATION_LANDING_BLOCKED') { + const refreshed = this.evaluate(candidate.id); + results.push({ requestId: candidate.id, state: refreshed.state, reason: refreshed.reason, + blockers: error.details, landed: false }); + continue; + } + throw error; + } + } + return { + actorId: actor, processed: results.length, + landed: results.filter((entry) => entry.landed).length, + deferred: results.filter((entry) => !entry.landed).length, + results, mutationPerformed: results.some((entry) => entry.landed), + }; } } diff --git a/src/kernel/analyze.mjs b/src/kernel/analyze.mjs index 36d65b9..1951d9b 100644 --- a/src/kernel/analyze.mjs +++ b/src/kernel/analyze.mjs @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from 'node:fs'; import { extname } from 'node:path'; -import { trackedFiles } from './git.mjs'; +import { workingTreeFiles, workingTreeFingerprint } from './git.mjs'; import { buildArchitectureGraph } from './domains.mjs'; const LANGUAGE_BY_EXTENSION = new Map([ @@ -20,7 +20,8 @@ const MANIFESTS = [ ]; export function analyzeRepository(repository, { specifications = [] } = {}) { - const files = trackedFiles(repository.root); + const workingTree = workingTreeFiles(repository.root); + const files = workingTree.files; const languageCounts = new Map(); const topLevel = new Set(); @@ -70,9 +71,12 @@ export function analyzeRepository(repository, { specifications = [] } = {}) { branch: repository.branch, head: repository.head, hasRemote: repository.remotes.length > 0, + workingTreeFingerprint: workingTreeFingerprint(repository.root, files), }, inventory: { - trackedFileCount: files.length, + trackedFileCount: workingTree.trackedFileCount, + untrackedFileCount: workingTree.untrackedFileCount, + analyzedFileCount: files.length, languages, manifests, topLevelDirectories: [...topLevel].sort(), diff --git a/src/kernel/config.mjs b/src/kernel/config.mjs index 8fb2369..236a1a4 100644 --- a/src/kernel/config.mjs +++ b/src/kernel/config.mjs @@ -2,12 +2,14 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { z } from 'zod'; import { TorchError } from './errors.mjs'; +import { organizationGraphFromConfig, organizationGraphSchema } from './organization.mjs'; export const PROJECT_CONFIG_SCHEMA = 'torch.dev/v1alpha1'; const text = z.string().trim().min(1); const id = text.regex(/^[a-z0-9][a-z0-9-]*$/); const textList = z.array(text); +const launchPolicy = z.record(z.string().regex(/^[a-z][A-Za-z0-9]*$/), text).optional(); const domain = z.object({ id, @@ -22,6 +24,8 @@ const domain = z.object({ resources: textList, runtime: text, model: text.nullable().optional(), + reasoning: text.nullable().optional(), + launchPolicy, branch: text.nullable().optional(), worktree_name: text.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/).nullable().optional(), }).strict(); @@ -33,6 +37,21 @@ const check = z.object({ args: textList.optional(), source: z.record(z.string(), z.unknown()).optional(), resources: textList.optional(), + snapshot: z.object({ + paths: textList.min(1), + source_commit_file: text, + }).strict().optional(), + conditions: z.object({ + command: text, + args: textList.optional(), + timeout_seconds: z.number().int().positive().max(300).optional(), + required: z.array(z.object({ + id, + equals: z.union([z.string(), z.boolean(), z.number().finite(), z.null()]), + }).strict()).min(1).refine((requirements) => + new Set(requirements.map((requirement) => requirement.id)).size === requirements.length, + 'Condition IDs must be unique'), + }).strict().optional(), }).strict(); const resource = z.object({ @@ -53,8 +72,28 @@ const schedule = z.object({ z.object({ type: z.literal('interval'), seconds: z.number().int().positive() }).strict(), z.object({ type: z.literal('cron'), expression: text }).strict(), ]), - behavior: z.enum(['read-only', 'mutating']), - action: z.object({ type: z.literal('command'), command: text, args: textList }).strict(), + behavior: z.enum(['read-only', 'coordination', 'mutating']), + action: z.discriminatedUnion('type', [ + z.object({ type: z.literal('command'), command: text, args: textList }).strict(), + z.object({ + type: z.literal('manager-check-in'), manager_id: id, + stale_work: z.object({ + stale_days: z.number().int().min(1).max(365).optional(), + max_commits: z.number().int().min(1).max(10_000).optional(), + max_items: z.number().int().min(1).max(100).optional(), + }).strict().optional(), + wake: z.object({ + enabled: z.boolean(), + budget_mode: z.enum(['invocation-count', 'usd-hard-cap']).optional(), + max_usd_per_invocation: z.number().finite().positive().optional(), + }).strict().optional(), + }).strict(), + z.object({ + type: z.literal('integration-drain'), landing_authority_id: id, + limit: z.number().int().min(1).max(500).optional(), + }).strict(), + z.object({ type: z.literal('owner-digest') }).strict(), + ]), required_authority: textList.min(1), retry: z.object({ max_attempts: z.number().int().min(1).max(10) }).strict().optional(), failure_recipient: text.optional(), @@ -89,8 +128,19 @@ export const projectConfigSchema = z.object({ message: 'runtimes.default must name a configured runtime', }), checks: z.array(check), + backlog: z.object({ + self_claim: z.object({ enabled: z.boolean(), areas: textList }).strict().optional(), + activity: z.object({ + stale_days: z.number().int().min(1).max(365).optional(), + max_commits: z.number().int().min(1).max(10_000).optional(), + auto_close_after_landing: z.boolean().optional(), + }).strict().optional(), + }).strict().optional(), resources: z.array(resource), schedules: z.array(schedule), + runtime_wake_budget: z.object({ + max_invocations_per_day: z.number().int().min(1).max(24), + }).strict().optional(), integration: z.object({ provider: text, target: text, @@ -99,6 +149,7 @@ export const projectConfigSchema = z.object({ landing_authority: textList.min(1), }).strict(), delivery: z.object({ + retry: z.object({ max_attempts: z.number().int().min(1).max(5) }).strict().optional(), authority: z.object({ implemented: textList.min(1), verified: textList.min(1), @@ -113,14 +164,26 @@ export const projectConfigSchema = z.object({ deployment: z.object({ provider: text }).passthrough(), }).strict(), }).strict(), + owner_digest: z.object({ enabled: z.boolean(), window_hours: z.number().int().min(1).max(168).optional(), + max_items: z.number().int().min(1).max(100).optional() }).strict().optional(), session_manager: z.object({ id: z.literal('session-manager'), runtime: text, - start_last: z.literal(true), + model: text.nullable().optional(), + reasoning: text.nullable().optional(), + launchPolicy, + // Older installations carried this launch-order hint. Current lifecycle + // ordering is derived from the organization graph; retain only for reads. + start_last: z.literal(true).optional(), branch: text.nullable().optional(), worktree_name: text.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/).nullable().optional(), }).strict(), domains: z.array(domain).min(1), + organization: organizationGraphSchema.optional(), + organization_assessment: z.object({ + observation_window_days: z.number().int().min(7).max(365), + minimum_recurrences: z.number().int().min(3).max(10), + }).strict().optional(), retired_domains: z.array(z.object({ id, title: text, @@ -130,6 +193,124 @@ export const projectConfigSchema = z.object({ }).strict()).optional(), }).strict().superRefine((config, context) => { const runtimeNames = new Set(Object.keys(config.runtimes).filter((name) => name !== 'default')); + const scheduleIds = new Set(); + const identityIds = new Set(['session-manager', ...config.domains.map((entry) => entry.id)]); + for (const [index, area] of (config.backlog?.self_claim?.areas ?? []).entries()) { + if (area === 'session-manager' || !identityIds.has(area)) context.addIssue({ + code: 'custom', path: ['backlog', 'self_claim', 'areas', index], + message: 'self-claim allowlist must name a defined specialist identity', + }); + } + for (const [index, entry] of config.schedules.entries()) { + if (scheduleIds.has(entry.id)) { + context.addIssue({ code: 'custom', path: ['schedules', index, 'id'], message: `duplicate schedule id: ${entry.id}` }); + } + scheduleIds.add(entry.id); + if (entry.action.type === 'manager-check-in') { + if (entry.behavior !== 'coordination' || entry.lifetime !== 'system' + || !entry.required_authority.includes('owner')) { + context.addIssue({ + code: 'custom', path: ['schedules', index], + message: 'manager check-in must be an owner-authorized system coordination schedule', + }); + } + if (entry.trigger.type === 'interval' && entry.trigger.seconds < 60) { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'trigger', 'seconds'], + message: 'manager check-in intervals must be at least 60 seconds', + }); + } + if (!identityIds.has(entry.action.manager_id)) { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'action', 'manager_id'], + message: `manager check-in references unknown identity: ${entry.action.manager_id}`, + }); + } + const wake = entry.action.wake; + const mode = wake?.budget_mode ?? (wake?.max_usd_per_invocation !== undefined ? 'usd-hard-cap' : null); + if (wake?.enabled === true && mode === null) { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'action', 'wake', 'budget_mode'], + message: 'enabled manager runtime wakes require an explicit invocation-count policy or a positive per-invocation USD ceiling', + }); + } + if (mode === 'usd-hard-cap' && !(Number.isFinite(wake.max_usd_per_invocation) + && wake.max_usd_per_invocation > 0)) { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'action', 'wake', 'max_usd_per_invocation'], + message: 'enabled manager runtime wakes require a positive per-invocation USD ceiling', + }); + } + if (mode === 'invocation-count' && wake?.max_usd_per_invocation !== undefined) { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'action', 'wake', 'max_usd_per_invocation'], + message: 'invocation-count mode cannot promise a USD ceiling; select usd-hard-cap instead', + }); + } + } else if (entry.action.type === 'integration-drain') { + if (entry.behavior !== 'mutating' || entry.lifetime !== 'system' + || !['interval', 'cron'].includes(entry.trigger.type) + || !entry.required_authority.includes('owner')) { + context.addIssue({ + code: 'custom', path: ['schedules', index], + message: 'integration drain must be an owner-authorized periodic system mutation schedule', + }); + } + if (!identityIds.has(entry.action.landing_authority_id)) { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'action', 'landing_authority_id'], + message: `integration drain references unknown identity: ${entry.action.landing_authority_id}`, + }); + } + if (!config.integration.landing_authority.includes(entry.action.landing_authority_id)) { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'action', 'landing_authority_id'], + message: `integration drain identity lacks configured landing authority: ${entry.action.landing_authority_id}`, + }); + } + if (entry.trigger.type === 'interval' && entry.trigger.seconds < 60) { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'trigger', 'seconds'], + message: 'integration-drain intervals must be at least 60 seconds', + }); + } + } else if (entry.action.type === 'owner-digest') { + if (entry.behavior !== 'coordination' || entry.lifetime !== 'system' + || !entry.required_authority.includes('owner') || (entry.retry?.max_attempts ?? 1) !== 1) { + context.addIssue({ code: 'custom', path: ['schedules', index], + message: 'owner digest must be an owner-authorized system coordination schedule without automatic retry' }); + } + } else if (entry.behavior === 'coordination') { + context.addIssue({ + code: 'custom', path: ['schedules', index, 'behavior'], + message: 'coordination behavior is reserved for the typed manager-check-in action', + }); + } + } + const managerRoleIds = new Set(config.organization + ? config.organization.roles.flatMap((role) => role.reports_to) + : []); + const scheduledManagers = new Set(config.schedules + .filter((entry) => entry.action.type === 'manager-check-in') + .map((entry) => entry.action.manager_id)); + for (const managerIdentity of new Set(organizationGraphFromConfig(config).roles + .filter((role) => managerRoleIds.has(role.id) && role.kind !== 'owner') + .map((role) => role.identity_id))) { + if (!scheduledManagers.has(managerIdentity)) { + context.addIssue({ + code: 'custom', path: ['schedules'], + message: `manager identity ${managerIdentity} has direct reports but no manager-check-in schedule`, + }); + } + } + const wakeEnabled = config.schedules.some((entry) => + entry.action.type === 'manager-check-in' && entry.action.wake?.enabled === true); + if (wakeEnabled && !config.runtime_wake_budget) { + context.addIssue({ + code: 'custom', path: ['runtime_wake_budget'], + message: 'enabled manager runtime wakes require a project-wide daily invocation budget', + }); + } if (!runtimeNames.has(config.runtimes.default)) { context.addIssue({ code: 'custom', path: ['runtimes', 'default'], message: 'default runtime is not configured' }); } @@ -138,6 +319,17 @@ export const projectConfigSchema = z.object({ context.addIssue({ code: 'custom', path: ['domains', index, 'runtime'], message: 'domain runtime is not configured' }); } } + for (const [runtime, profile] of Object.entries(config.runtimes)) { + if (runtime === 'default' || !profile || typeof profile !== 'object' || Array.isArray(profile)) continue; + if (profile.launchPolicy !== undefined) { + const result = launchPolicy.safeParse(profile.launchPolicy); + if (!result.success) { + for (const issue of result.error.issues) context.addIssue({ + ...issue, path: ['runtimes', runtime, 'launchPolicy', ...issue.path], + }); + } + } + } }); function formatIssues(issues) { diff --git a/src/kernel/doctor.mjs b/src/kernel/doctor.mjs index fcc4dc9..7a8552c 100644 --- a/src/kernel/doctor.mjs +++ b/src/kernel/doctor.mjs @@ -7,15 +7,19 @@ import { inspectManagedWorktree } from './worktree-state.mjs'; import { validateProjectConfig } from './config.mjs'; import { forgeStatus } from '../forge/service.mjs'; import { classifyRecoverability } from '../canonical/local.mjs'; -import { createClaudeAdapter } from '../adapters/claude.mjs'; -import { createCodexAdapter } from '../adapters/codex.mjs'; +import { createRuntimeAdapterRegistry } from '../adapters/registry.mjs'; +import { profileCapabilityIssues, resolveLaunchPolicy } from '../adapters/runtime.mjs'; import { assessBacklogHealth, backlogObservedCommitDistance } from '../backlog/service.mjs'; +import { organizationGraphFromConfig } from './organization.mjs'; function parseJsonYaml(path) { return JSON.parse(readFileSync(path, 'utf8')); } -export function diagnoseProject({ repository, env = process.env, now = () => new Date() }) { +export function diagnoseProject({ + repository, env = process.env, now = () => new Date(), + runtimeRegistry = createRuntimeAdapterRegistry({ env }), +}) { const trackedRoot = join(repository.root, '.torch'); const configPath = join(trackedRoot, 'torch.yaml'); const manifestPath = join(trackedRoot, 'install-manifest.json'); @@ -39,6 +43,26 @@ export function diagnoseProject({ repository, env = process.env, now = () => new } if (manifest) { + if (config) { + try { + const graph = organizationGraphFromConfig(config); + const activeIdentities = new Set([ + 'owner', 'session-manager', ...(config.domains ?? []).map((domain) => domain.id), + ]); + const missing = [...new Set(graph.roles + .map((role) => role.identity_id) + .filter((identity) => !activeIdentities.has(identity)))]; + if (missing.length) findings.push({ + severity: 'error', code: 'ORGANIZATION_IDENTITY_MISSING', identities: missing, + recommendation: 'Review the approved organization graph against the active Fleet roster; role references do not create identities.', + }); + } catch (error) { + findings.push({ + severity: 'error', code: 'ORGANIZATION_GRAPH_INVALID', + message: error.message, details: error.details, + }); + } + } for (const record of manifest.created ?? []) { const path = join(repository.root, record.path); if (!existsSync(path)) findings.push({ severity: 'error', code: 'OWNED_FILE_MISSING', path: record.path }); @@ -220,13 +244,24 @@ export function diagnoseProject({ repository, env = process.env, now = () => new findings.push({ severity: 'error', code: 'RECOVERABILITY_INVALID', message: error.message }); } if (config?.runtimes) { - const adapters = new Map([ - ['claude', createClaudeAdapter({ env })], - ['codex', createCodexAdapter({ env })], - ]); + const adapters = runtimeRegistry; + for (const pluginError of adapters.pluginErrors ?? []) findings.push({ + severity: 'warning', code: pluginError.code, runtime: pluginError.name, + message: pluginError.message, details: pluginError.details, + recommendation: 'Review the user-local runtime plugin trust record and the exact adapter module before retrying.', + }); for (const runtime of Object.keys(config.runtimes).filter((name) => name !== 'default')) { const adapter = adapters.get(runtime); if (!adapter) { + const trustedPlugin = adapters.trustedPlugins?.find((plugin) => plugin.name === runtime); + if (trustedPlugin?.status === 'trusted') { + findings.push({ + severity: 'info', code: 'RUNTIME_ADAPTER_TRUSTED_NOT_LOADED', runtime, + modulePath: trustedPlugin.modulePath, sha256: trustedPlugin.sha256, + recommendation: 'The user-local entrypoint hash is trusted. Doctor did not execute it; validate the adapter only in an explicit runtime operation.', + }); + continue; + } findings.push({ severity: 'warning', code: 'RUNTIME_ADAPTER_UNKNOWN', runtime, recommendation: 'Install or configure a TORCH runtime adapter before starting this identity.', @@ -242,6 +277,29 @@ export function diagnoseProject({ repository, env = process.env, now = () => new : `Install ${runtime} or change the approved runtime assignment before startup.`, }); } + const assigned = [ + { id: 'session-manager', ...config.session_manager }, + ...config.domains, + ]; + for (const area of assigned) { + const runtime = area.runtime ?? config.runtimes.default; + const adapter = adapters.get(runtime); + if (!adapter) continue; + const profile = { + model: area.model ?? config.runtimes[runtime]?.model ?? adapter.configuration?.model ?? null, + reasoning: area.reasoning ?? config.runtimes[runtime]?.reasoning ?? adapter.configuration?.reasoning ?? null, + launchPolicy: resolveLaunchPolicy({ + runtime, adapterDefaults: adapter.configuration, + runtimeConfig: config.runtimes[runtime], identity: area, + }), + }; + const unsupported = profileCapabilityIssues(adapter, profile); + if (unsupported.length) findings.push({ + severity: 'error', code: 'RUNTIME_PROFILE_UNSUPPORTED', areaId: area.id, + runtime, adapter: adapter.name, profile, capabilities: unsupported, + recommendation: 'Assign a model/reasoning profile supported by this identity runtime adapter.', + }); + } } if (config?.forge?.provider && config.forge.provider !== 'none') { try { diff --git a/src/kernel/domains.mjs b/src/kernel/domains.mjs index 3b9cf21..a9c6fd4 100644 --- a/src/kernel/domains.mjs +++ b/src/kernel/domains.mjs @@ -1,6 +1,8 @@ import { existsSync, readFileSync } from 'node:fs'; -import { posix } from 'node:path'; +import { join, posix } from 'node:path'; import { TorchError } from './errors.mjs'; +import { organizationGraphFromConfig, planManagerCheckInSchedules } from './organization.mjs'; +import { workingTreeFingerprint } from './git.mjs'; import { validateSpecificationEvidence } from './specifications.mjs'; const SOURCE_ROOTS = new Set(['src', 'app', 'apps', 'server', 'client', 'lib', 'packages', 'services']); @@ -138,7 +140,7 @@ function domainFromComponent(component) { neighbours: [], required_checks: [], resources: [], - runtime: 'claude', + runtime: 'default', evidence: component.evidence, }; } @@ -163,12 +165,24 @@ function specDomain(signal, specification) { : [`responsibilities described by ${specification.path}:${signal.line}`], not_scope: [], owned_paths: signal.pathReferences, - shared_paths: [], neighbours: [], required_checks: [], resources: [], runtime: 'claude', + shared_paths: [], neighbours: [], required_checks: [], resources: [], runtime: 'default', evidence: [`${specification.path}:${signal.line}`], design_status: signal.pathReferences.length ? 'evidence-backed' : 'needs-owner-path-review', }; } +function hasProjectPathEvidence(signal, repositoryRoot) { + return signal.pathReferences.some((reference) => { + const value = reference.replaceAll('\\', '/').replace(/^\.\//, ''); + if (!value || value.startsWith('/') || value.startsWith('~') || value.startsWith('$') + || /^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return false; + const normalized = posix.normalize(value); + if (normalized === '..' || normalized.startsWith('../')) return false; + const prefix = normalized.replace(/[?*{\x5b].*$/, '').replace(/\/+$/, ''); + return Boolean(prefix && existsSync(join(repositoryRoot, ...prefix.split('/')))); + }); +} + export function proposeDomains({ repository, analysis }) { const graph = analysis.architecture; const sourceComponents = graph.components.filter((component) => !SHARED_PATTERN.test(component.key)); @@ -181,32 +195,22 @@ export function proposeDomains({ repository, analysis }) { specification.domainSignals.map((signal) => ({ signal, specification }))); for (const { signal, specification } of signals) { + const grounded = hasProjectPathEvidence(signal, repository.root); const candidate = domains .map((domain) => ({ domain, score: overlap(`${domain.id} ${domain.title} ${domain.owned_paths.join(' ')}`, `${signal.title} ${signal.pathReferences.join(' ')}`) })) .sort((a, b) => b.score - a.score)[0]; - if (candidate?.score > 0) { + if (candidate?.score > 0 && grounded) { for (const responsibility of signal.responsibilities) { if (!candidate.domain.scope.includes(responsibility)) candidate.domain.scope.push(responsibility); } candidate.domain.evidence.push(`${specification.path}:${signal.line}`); continue; } - if (signal.level <= 3 && (signal.pathReferences.length || specFirst)) { + if (signal.level <= 3 && (grounded || specFirst)) { const proposed = specDomain(signal, specification); if (!domains.some((domain) => domain.id === proposed.id)) domains.push(proposed); } } - if (!domains.length) { - domains.push({ - id: 'core', title: 'Core project', kind: 'development', - scope: ['initial project implementation and architecture'], - not_scope: [], owned_paths: ['**'], shared_paths: [], neighbours: [], required_checks: [], - resources: [], - runtime: 'claude', - evidence: analysis.inventory.manifests, - }); - } - if (analysis.inventory.testFileCount >= 2) { domains.push({ id: 'qa', title: 'Quality and integration', kind: 'cross-cutting', @@ -215,7 +219,7 @@ export function proposeDomains({ repository, analysis }) { owned_paths: ['test/**', 'tests/**', 'e2e/**'], shared_paths: [], neighbours: [], required_checks: [], evidence: graph.verificationSurfaces.slice(0, 12), resources: [], - runtime: 'claude', + runtime: 'default', }); } if (graph.operationalSurfaces.some((path) => /deploy|release/i.test(path))) { @@ -226,11 +230,24 @@ export function proposeDomains({ repository, analysis }) { owned_paths: graph.operationalSurfaces.filter((path) => /deploy|release|workflow/i.test(path)).slice(0, 20), shared_paths: [], neighbours: [], required_checks: [], resources: [], - runtime: 'claude', + runtime: 'default', evidence: graph.operationalSurfaces.slice(0, 12), }); } + // Evidence-backed horizontal specialists count as a real initial organization. + // A catch-all Core owner must not precede and overlap QA/release domains. + if (!domains.length) { + domains.push({ + id: 'core', title: 'Core project', kind: 'development', + scope: ['initial project implementation and architecture'], + not_scope: [], owned_paths: ['**'], shared_paths: [], neighbours: [], required_checks: [], + resources: [], + runtime: 'default', + evidence: analysis.inventory.manifests, + }); + } + const byKey = new Map(graph.components.map((component) => [component.key, component.id])); const collisions = []; const addCollision = (leftId, rightId, score, reason) => { @@ -295,13 +312,21 @@ export function proposeDomains({ repository, analysis }) { domain.not_scope.push(...domains .filter((other) => other.id !== domain.id && other.owned_paths.length) .map((other) => `${other.title} owns ${other.owned_paths.join(', ')}`)); - domain.shared_paths = graph.sharedSurfaces.slice(0, 40); + const componentKeys = graph.components + .filter((component) => domain.owned_paths.some((path) => component.paths.includes(path))) + .map((component) => component.key); + domain.shared_paths = [...new Set(graph.dependencies + .filter((edge) => componentKeys.includes(edge.from) && SHARED_PATTERN.test(edge.to)) + .map((edge) => edge.targetEvidence))].sort(); } return { schema: 'torch.dev/domain-proposal/v1alpha1', generatedAt: new Date().toISOString(), - repository: { root: repository.root, initialCommit: repository.initialCommit, head: repository.head }, + repository: { + root: repository.root, initialCommit: repository.initialCommit, head: repository.head, + workingTreeFingerprint: analysis.repository.workingTreeFingerprint, + }, review: { status: 'pending', reviewedAt: null, reviewedBy: null, notes: [] }, specifications: specifications.map((specification) => ({ path: specification.path, @@ -318,18 +343,50 @@ export function proposeDomains({ repository, analysis }) { resources: analysis.inventory.scarceResources.map((id) => ({ id, capacity: 1, queue: 'fifo', max_hold_seconds: 3600, })), - schedules: [], + schedules: planManagerCheckInSchedules({ + graph: organizationGraphFromConfig({ domains }), + }).schedules, architecture: graph, }; } +// Reject provable claim collisions even for planned files that do not exist yet. +// This intentionally does not pretend to solve arbitrary glob intersection. +export function primaryOwnershipProblems(domains = []) { + const claims = (Array.isArray(domains) ? domains : []).flatMap((domain) => + (Array.isArray(domain?.owned_paths) ? domain.owned_paths : []) + .filter((path) => typeof path === 'string' && path.trim()) + .map((path) => ({ id: domain.id, path: path.replaceAll('\\', '/').replace(/^\.\//, '') }))); + const problems = new Set(); + const contains = (parent, child) => parent === '**' + || (parent.endsWith('/**') && !/[?*[]/.test(parent.slice(0, -3)) + && child.startsWith(`${parent.slice(0, -3)}/`)); + for (let left = 0; left < claims.length; left += 1) { + for (let right = left + 1; right < claims.length; right += 1) { + const a = claims[left]; + const b = claims[right]; + if (a.id !== b.id && (a.path === b.path || contains(a.path, b.path) || contains(b.path, a.path))) { + problems.add(`primary ownership overlaps: ${a.id} (${a.path}) and ${b.id} (${b.path})`); + } + } + } + return [...problems]; +} + export function validateApprovedProposal({ proposal, repository }) { const problems = []; if (proposal?.schema !== 'torch.dev/domain-proposal/v1alpha1') problems.push('unsupported proposal schema'); if (proposal?.review?.status !== 'approved') problems.push('proposal review status is not approved'); if (proposal?.repository?.initialCommit !== repository.initialCommit) problems.push('proposal belongs to a different Git history'); if (proposal?.repository?.head !== repository.head) problems.push('proposal is stale because HEAD changed'); + const currentFingerprint = workingTreeFingerprint(repository.root); + if (!proposal?.repository?.workingTreeFingerprint) { + problems.push('proposal has no repository working-tree fingerprint'); + } else if (proposal.repository.workingTreeFingerprint !== currentFingerprint) { + problems.push('proposal is stale because the repository working tree changed'); + } if (!Array.isArray(proposal?.domains) || proposal.domains.length === 0) problems.push('proposal has no domains'); + problems.push(...primaryOwnershipProblems(proposal?.domains)); problems.push(...validateSpecificationEvidence(proposal?.specifications ?? [], repository.root)); const ids = new Set(); for (const domain of proposal?.domains ?? []) { @@ -342,6 +399,10 @@ export function validateApprovedProposal({ proposal, repository }) { if (domain.runtime !== undefined && (typeof domain.runtime !== 'string' || !domain.runtime.trim())) { problems.push(`domain ${domain.id} has an invalid runtime`); } + if (domain.reasoning !== undefined && domain.reasoning !== null + && (typeof domain.reasoning !== 'string' || !domain.reasoning.trim())) { + problems.push(`domain ${domain.id} has an invalid reasoning profile`); + } if (domain.worktree_name !== undefined && domain.worktree_name !== null && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(domain.worktree_name)) { problems.push(`domain ${domain.id} has an invalid worktree_name`); @@ -367,8 +428,23 @@ export function validateApprovedProposal({ proposal, repository }) { if (!collision.resolution?.strategy) problems.push(`collision ${collision.domains?.join('/')} has no resolution`); } for (const schedule of proposal?.schedules ?? []) { + const managerCheckInAction = schedule.action?.type === 'manager-check-in'; + const integrationDrainAction = schedule.action?.type === 'integration-drain'; + const actionValid = managerCheckInAction + ? schedule.behavior === 'coordination' && schedule.lifetime === 'system' + && typeof schedule.action.manager_id === 'string' + && schedule.required_authority?.includes('owner') + : integrationDrainAction + ? schedule.behavior === 'mutating' && schedule.lifetime === 'system' + && ['interval', 'cron'].includes(schedule.trigger?.type) + && (schedule.trigger?.type !== 'interval' || schedule.trigger.seconds >= 60) + && typeof schedule.action.landing_authority_id === 'string' + && schedule.required_authority?.includes('owner') + : schedule.action?.type === 'command' && typeof schedule.action.command === 'string' + && Array.isArray(schedule.action.args) && schedule.behavior !== 'coordination'; if (!schedule.id || !['system', 'session'].includes(schedule.lifetime) - || !['read-only', 'mutating'].includes(schedule.behavior) + || !['read-only', 'coordination', 'mutating'].includes(schedule.behavior) + || !actionValid || !schedule.owner || !schedule.trigger || !schedule.action || !schedule.required_authority?.length || !schedule.source_of_truth) { problems.push(`schedule ${schedule.id ?? ''} is incomplete`); diff --git a/src/kernel/git.mjs b/src/kernel/git.mjs index 78022ff..8d241d8 100644 --- a/src/kernel/git.mjs +++ b/src/kernel/git.mjs @@ -1,6 +1,7 @@ import { execFileSync } from 'node:child_process'; -import { realpathSync } from 'node:fs'; -import { basename } from 'node:path'; +import { createHash } from 'node:crypto'; +import { lstatSync, readFileSync, realpathSync } from 'node:fs'; +import { basename, join } from 'node:path'; import { TorchError } from './errors.mjs'; function git(cwd, args, { optional = false } = {}) { @@ -55,3 +56,60 @@ export function trackedFiles(root) { const output = git(root, ['ls-files', '-z'], { optional: true }); return output ? output.split('\0').filter(Boolean) : []; } + +const SOURCE_CONTENT = /\.(?:[cm]?[jt]sx?|py|rs|go|java|c|cc|cpp|h|hpp|cs|rb|php|swift|kt)$/i; +const MAX_ANALYZED_SOURCE_BYTES = 256_000; +const TEST_SURFACE = /(^|\/)(test|tests|spec|e2e)(\/|\.|$)/i; +const OPERATIONS_SURFACE = /(^|\/)(\.github|infra|ops|deploy|release|docker|systemd)(\/|\.|$)/i; +const SHARED_SURFACE = /(^|\/)(shared|common|types?|schemas?|contracts?|[^/]*config[^/]*)(\/|\.|$)/i; +const MANIFEST_PATHS = new Set([ + 'package.json', 'pyproject.toml', 'requirements.txt', 'Cargo.toml', 'go.mod', + 'pom.xml', 'build.gradle', 'Makefile', 'CMakeLists.txt', 'Dockerfile', +]); + +export function workingTreeFiles(root) { + const tracked = trackedFiles(root); + const untrackedOutput = git(root, ['ls-files', '--others', '--exclude-standard', '-z'], { optional: true }); + const untracked = untrackedOutput ? untrackedOutput.split('\0').filter(Boolean) : []; + const trackedSet = new Set(tracked); + const files = [...new Set([...tracked, ...untracked])].filter((path) => { + try { return lstatSync(join(root, path)).isFile(); } catch { return false; } + }).sort(); + const visible = new Set(files); + return { + files, + trackedFileCount: tracked.filter((path) => visible.has(path)).length, + untrackedFileCount: untracked.filter((path) => visible.has(path) && !trackedSet.has(path)).length, + }; +} + +export function workingTreeFingerprint(root, files = workingTreeFiles(root).files) { + const hash = createHash('sha256'); + const analysisInputs = files.filter((path) => SOURCE_CONTENT.test(path) + || TEST_SURFACE.test(path) || OPERATIONS_SURFACE.test(path) + || SHARED_SURFACE.test(path) || MANIFEST_PATHS.has(path)); + for (const path of [...analysisInputs].sort()) { + hash.update(path); + hash.update('\0'); + if (!SOURCE_CONTENT.test(path) && basename(path) !== 'package.json') continue; + try { + const absolute = join(root, path); + const info = lstatSync(absolute); + if (!info.isFile()) { + hash.update('not-a-regular-file\0'); + continue; + } + hash.update(String(info.size)); + hash.update('\0'); + if (SOURCE_CONTENT.test(path) && info.size > MAX_ANALYZED_SOURCE_BYTES) { + hash.update('source-content-not-inspected\0'); + } else { + hash.update(readFileSync(absolute)); + hash.update('\0'); + } + } catch { + hash.update('unreadable\0'); + } + } + return hash.digest('hex'); +} diff --git a/src/kernel/install.mjs b/src/kernel/install.mjs index 47a0103..fc9cbf4 100644 --- a/src/kernel/install.mjs +++ b/src/kernel/install.mjs @@ -11,6 +11,7 @@ import { projectStatePath } from './paths.mjs'; import { validateApprovedProposal } from './domains.mjs'; import { inspectManagedWorktree } from './worktree-state.mjs'; import { renderDomainPrompt } from './prompts.mjs'; +import { createRuntimeAdapterRegistry } from '../adapters/registry.mjs'; const TRACKED_DIR = '.torch'; @@ -18,34 +19,34 @@ function jsonYaml(value) { return `${JSON.stringify(value, null, 2)}\n`; } -const RUNTIME_DEFINITIONS = Object.freeze({ - claude: { model: 'opus', background: true }, - codex: { sandbox: 'workspace-write', approval: 'approve-for-me' }, -}); - -function resolveInstallRuntimes(proposal, requested) { +function resolveInstallRuntimes(proposal, requested, registry, requestedDefault) { + const explicitRuntime = (runtime) => runtime && runtime !== 'default' ? runtime : null; + const required = new Set([ + explicitRuntime(proposal.session_manager?.runtime), + ...proposal.domains.map((domain) => explicitRuntime(domain.runtime)), + ].filter(Boolean)); + const defaultRuntime = requestedDefault ?? explicitRuntime(proposal.session_manager?.runtime) + ?? requested?.[0] ?? 'claude'; const selected = requested === undefined - ? Object.keys(RUNTIME_DEFINITIONS) + ? [...new Set([defaultRuntime, ...required])] : [...new Set(requested)]; - if (!selected.length || selected.some((runtime) => typeof runtime !== 'string' || !RUNTIME_DEFINITIONS[runtime])) { + if (!selected.length || !registry.has(defaultRuntime) + || selected.some((runtime) => typeof runtime !== 'string' || !registry.has(runtime))) { throw new TorchError('Install runtimes must be one or more supported adapters', { - code: 'INSTALL_RUNTIME_INVALID', details: { requested: selected, supported: Object.keys(RUNTIME_DEFINITIONS) }, + code: 'INSTALL_RUNTIME_INVALID', details: { requested: selected, defaultRuntime, supported: registry.names() }, }); } - const required = new Set([ - proposal.session_manager?.runtime ?? 'claude', - ...proposal.domains.map((domain) => domain.runtime ?? 'claude'), - ]); - const missing = [...required].filter((runtime) => !selected.includes(runtime)); + const missing = [...new Set([defaultRuntime, ...required])].filter((runtime) => !selected.includes(runtime)); if (missing.length) { throw new TorchError('Selected install runtimes do not cover the approved Fleet', { code: 'INSTALL_RUNTIME_MISSING', details: { selected, required: [...required], missing }, }); } - return selected; + return { runtimes: selected, defaultRuntime }; } -function initialFiles({ repository, projectId, createdAt, proposal, runtimes }) { +function initialFiles({ repository, projectId, createdAt, proposal, runtimes, defaultRuntime, runtimeRegistry }) { + const selectedRuntime = (runtime) => runtime && runtime !== 'default' ? runtime : defaultRuntime; const promptContent = (source, fallback) => source ? readFileSync(safeProjectPath(repository.root, source), 'utf8') : fallback; @@ -70,12 +71,14 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes }) }, synchronization: { strategy: 'dispatcher-managed', auto_merge_worktrees: false }, runtimes: Object.fromEntries([ - ['default', proposal.session_manager?.runtime ?? 'claude'], - ...runtimes.map((runtime) => [runtime, RUNTIME_DEFINITIONS[runtime]]), + ['default', defaultRuntime], + ...runtimes.map((runtime) => [runtime, runtimeRegistry.configuration(runtime)]), ]), checks: proposal.checks ?? [], + backlog: { self_claim: { enabled: false, areas: [] } }, resources: proposal.resources ?? [], schedules: proposal.schedules ?? [], + ...(proposal.runtime_wake_budget ? { runtime_wake_budget: proposal.runtime_wake_budget } : {}), integration: { provider: 'torch', target: repository.branch || 'main', require_current_main: true, required_checks: (proposal.checks ?? []).map((check) => check.id), @@ -97,10 +100,13 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes }) }, }, session_manager: { - id: 'session-manager', runtime: proposal.session_manager?.runtime ?? 'claude', start_last: true, + id: 'session-manager', runtime: selectedRuntime(proposal.session_manager?.runtime), + model: proposal.session_manager?.model ?? null, + reasoning: proposal.session_manager?.reasoning ?? null, branch: proposal.session_manager?.branch ?? null, worktree_name: proposal.session_manager?.worktree_name ?? null, }, + organization_assessment: { observation_window_days: 30, minimum_recurrences: 3 }, domains: proposal.domains.map((domain) => ({ id: domain.id, title: domain.title, @@ -111,8 +117,9 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes }) neighbours: domain.neighbours ?? [], required_checks: domain.required_checks ?? [], resources: domain.resources ?? [], - runtime: domain.runtime ?? 'claude', + runtime: selectedRuntime(domain.runtime), model: domain.model ?? null, + reasoning: domain.reasoning ?? null, branch: domain.branch ?? null, worktree_name: domain.worktree_name ?? null, })), @@ -125,6 +132,8 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes }) not_scope: ['project feature implementation by default'], neighbours: ['all'], runtime: config.session_manager.runtime, + model: config.session_manager.model, + reasoning: config.session_manager.reasoning, branch: config.session_manager.branch, worktree_name: config.session_manager.worktree_name, }, ...config.domains], @@ -132,7 +141,7 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes }) const managerPrompt = `${promptContent( proposal.session_manager?.prompt_source, - '# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n', + '# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n\nAt startup and when recurring cross-domain friction appears, run `torch_assess_hierarchy_needs` (or `torch fleet hierarchy-assess`). Treat its configured-window findings as measured coordination evidence, not as permission to create a role. Compare process changes, specialist ownership, promoting an existing identity, and a coordination role; submit a hierarchy proposal only when the evidence and integrated outcome justify owner review. Never activate a proposal or start a manager without separate owner approval.\n', ).trimEnd()}\n\n## Backlog loop\n\nKeep one durable Fleet backlog. Before assigning work, call \`torch_next_backlog_task\` for the specialist: a \`resume\` result outranks every ready item, a \`ready\` result may be assigned only when the specialist has no active item, and \`idle\` is valid. After completion, repeat the same lookup. Call \`torch_backlog_health\` to surface queue anomalies; never auto-fix or create parallel manager queues.\n\n## Fleet evolution\n\nAt startup, after backlog intake, and when repeated handoffs or cross-domain work appear, call \`torch_assess_fleet_evolution\`. Treat its threshold as a prompt for architectural judgment, not as an automatic decision. When recurring work has no coherent owner, or a durable specialist would materially improve context locality, ownership clarity, or verification, inspect repository evidence and use \`torch_propose_domain\` to submit an evidence-backed Fleet change. When a specialist no longer earns its coordination cost, use \`torch_propose_domain_retirement\`; retirement preserves its branch and refuses active or unrecoverable work. Use \`torch_list_fleet_changes\` to follow change state. Never create, retire, approve, activate, or start a persistent identity yourself; owner approval, CLI activation, and quota-consuming runtime start are separate. Recommend a merge or split for owner review when boundaries should change but do not silently rewrite ownership.\n`; const files = new Map([ ['torch.yaml', jsonYaml(config)], @@ -148,7 +157,8 @@ function initialFiles({ repository, projectId, createdAt, proposal, runtimes }) ['INSTALLATION.md', `# TORCH installation\n\nInstalled ${createdAt}. Run \`torch doctor\` before starting the fleet.\n`], ]); for (const domain of config.domains) { - const generatedPrompt = renderDomainPrompt(domain); + const generatedPrompt = renderDomainPrompt(domain, { projectChecks: config.integration.required_checks + .map((id) => config.checks.find((check) => check.id === id)).filter(Boolean) }); const proposalDomain = proposal.domains.find((candidate) => candidate.id === domain.id); files.set(`prompts/${domain.id}.md`, promptContent(proposalDomain?.prompt_source, generatedPrompt)); } @@ -227,9 +237,11 @@ function removeOwnedDirectories(projectRoot, directories) { } } -export function planInstall({ repository, proposal, env = process.env, runtimes: requestedRuntimes } = {}) { +export function planInstall({ repository, proposal, env = process.env, runtimes: requestedRuntimes, + defaultRuntime: requestedDefault, + runtimeRegistry = createRuntimeAdapterRegistry({ env }) } = {}) { validateApprovedProposal({ proposal, repository }); - const runtimes = resolveInstallRuntimes(proposal, requestedRuntimes); + const { runtimes, defaultRuntime } = resolveInstallRuntimes(proposal, requestedRuntimes, runtimeRegistry, requestedDefault); const trackedRoot = join(repository.root, TRACKED_DIR); const blockers = trackedStateIgnored(repository.root) ? [{ code: 'TRACKED_STATE_IGNORED', path: `${TRACKED_DIR}/torch.yaml` }] : []; @@ -241,6 +253,7 @@ export function planInstall({ repository, proposal, env = process.env, runtimes: wouldCreateTrackedState: !existsSync(trackedRoot), approvedDomainCount: proposal.domains.length, runtimes, + defaultRuntime, blockers, canProceed: blockers.length === 0, mutationPerformed: false, @@ -254,9 +267,11 @@ export function installProject({ projectId = randomUUID(), now = () => new Date(), runtimes: requestedRuntimes, + defaultRuntime: requestedDefault, + runtimeRegistry = createRuntimeAdapterRegistry({ env }), } = {}) { validateApprovedProposal({ proposal, repository }); - const runtimes = resolveInstallRuntimes(proposal, requestedRuntimes); + const { runtimes, defaultRuntime } = resolveInstallRuntimes(proposal, requestedRuntimes, runtimeRegistry, requestedDefault); const trackedRoot = join(repository.root, TRACKED_DIR); const trackedRootExisted = existsSync(trackedRoot); if (trackedRootExisted @@ -284,7 +299,9 @@ export function installProject({ const directoriesBefore = directoryInventory(trackedRoot, repository.root); let manifestWritten = false; try { - for (const [path, content] of initialFiles({ repository, projectId, createdAt, proposal, runtimes })) { + for (const [path, content] of initialFiles({ + repository, projectId, createdAt, proposal, runtimes, defaultRuntime, runtimeRegistry, + })) { const absolute = join(trackedRoot, path); const record = writeNewFile(absolute, content); created.push({ path: relative(repository.root, record.path), sha256: record.sha256 }); diff --git a/src/kernel/organization.mjs b/src/kernel/organization.mjs new file mode 100644 index 0000000..16d27e7 --- /dev/null +++ b/src/kernel/organization.mjs @@ -0,0 +1,242 @@ +import { z } from 'zod'; +import { TorchError } from './errors.mjs'; + +const text = z.string().trim().min(1); +const id = text.regex(/^[a-z0-9][a-z0-9-]*$/); + +const role = z.object({ + id, + title: text, + kind: z.enum([ + 'owner', 'owner-facing', 'program-direction', 'fleet-operations', 'domain-coordination', + 'specialist', 'independent-review', + ]), + identity_id: id, + responsibilities: z.array(text).min(1), + authority: z.array(z.enum([ + 'receive-owner-requests', 'propose-priorities', 'sequence-domains', + 'operate-fleet', 'coordinate-domains', 'review-deliverables', + 'own-implementation', 'approve-organization', 'approve-release', + ])), + coordinates: z.array(id), + reports_to: z.array(id), + consults_with: z.array(id), +}).strict(); + +const implementationOwner = z.object({ + surface: text, + identity_id: id, +}).strict(); + +export const organizationGraphSchema = z.object({ + schema: z.literal('torch.dev/organization/v1alpha1'), + revision: z.number().int().positive(), + owner_facing_role: id, + roles: z.array(role).min(1), + implementation_owners: z.array(implementationOwner), +}).strict().superRefine((graph, context) => { + const rolesById = new Map(); + const roleByIdentity = new Map(); + graph.roles.forEach((entry, index) => { + if (rolesById.has(entry.id)) { + context.addIssue({ code: 'custom', path: ['roles', index, 'id'], message: `duplicate role ID: ${entry.id}` }); + } + rolesById.set(entry.id, entry); + if (!roleByIdentity.has(entry.identity_id)) roleByIdentity.set(entry.identity_id, []); + roleByIdentity.get(entry.identity_id).push(entry); + if (entry.kind !== 'owner' && entry.authority.some((capability) => + ['approve-organization', 'approve-release'].includes(capability))) { + context.addIssue({ + code: 'custom', path: ['roles', index, 'authority'], + message: 'owner-reserved approval authority cannot be assigned to a fleet role', + }); + } + if (entry.kind !== 'specialist' && entry.authority.includes('own-implementation')) { + context.addIssue({ + code: 'custom', path: ['roles', index, 'authority'], + message: 'implementation ownership belongs to specialist roles, not coordination roles', + }); + } + }); + + const owners = graph.roles.filter((entry) => entry.kind === 'owner'); + if (owners.length !== 1 || owners[0]?.identity_id !== 'owner') { + context.addIssue({ + code: 'custom', path: ['roles'], + message: 'organization must represent exactly one human owner identity', + }); + } + const ownerFacing = graph.roles.filter((entry) => entry.kind === 'owner-facing'); + if (ownerFacing.length !== 1 || ownerFacing[0]?.id !== graph.owner_facing_role) { + context.addIssue({ + code: 'custom', path: ['owner_facing_role'], + message: 'organization must name its single owner-facing role', + }); + } + + const references = ['coordinates', 'reports_to', 'consults_with']; + graph.roles.forEach((entry, index) => { + for (const field of references) { + entry[field].forEach((target, targetIndex) => { + if (!rolesById.has(target)) { + context.addIssue({ + code: 'custom', path: ['roles', index, field, targetIndex], + message: `unknown role reference: ${target}`, + }); + } + }); + } + }); + + const visiting = new Set(); + const visited = new Set(); + function visit(roleId) { + if (visiting.has(roleId)) return true; + if (visited.has(roleId)) return false; + visiting.add(roleId); + const entry = rolesById.get(roleId); + const cyclic = entry?.reports_to.some((manager) => visit(manager)) ?? false; + visiting.delete(roleId); + visited.add(roleId); + return cyclic; + } + graph.roles.forEach((entry, index) => { + if (visit(entry.id)) { + context.addIssue({ + code: 'custom', path: ['roles', index, 'reports_to'], + message: 'reporting relationships must be acyclic', + }); + } + }); + + const surfaces = new Set(); + graph.implementation_owners.forEach((owner, index) => { + if (surfaces.has(owner.surface)) { + context.addIssue({ + code: 'custom', path: ['implementation_owners', index, 'surface'], + message: `implementation surface has multiple owners: ${owner.surface}`, + }); + } + surfaces.add(owner.surface); + const holders = roleByIdentity.get(owner.identity_id) ?? []; + if (!holders.some((entry) => entry.kind === 'specialist')) { + context.addIssue({ + code: 'custom', path: ['implementation_owners', index, 'identity_id'], + message: 'implementation owner must have a specialist role in the organization', + }); + } + }); +}); + +export function validateOrganizationGraph(value) { + const result = organizationGraphSchema.safeParse(value); + if (!result.success) { + throw new TorchError('TORCH organization graph is invalid', { + code: 'ORGANIZATION_GRAPH_INVALID', + details: result.error.issues.map((issue) => ({ + path: issue.path.length ? issue.path.join('.') : '', + message: issue.message, + code: issue.code, + })), + }); + } + return value; +} + +export function organizationGraphFromConfig(config) { + if (config.organization) return validateOrganizationGraph(config.organization); + const domains = config.domains ?? []; + const domainIds = new Set(domains.map((domain) => domain.id)); + const specialistRoles = domains.map((domain) => ({ + id: domain.id, + title: domain.title, + kind: 'specialist', + identity_id: domain.id, + responsibilities: [...(domain.scope ?? [])], + authority: ['own-implementation'], + coordinates: [], + reports_to: ['session-manager'], + consults_with: (domain.neighbours ?? []).filter((neighbour) => domainIds.has(neighbour)), + })); + const graph = { + schema: 'torch.dev/organization/v1alpha1', + revision: 1, + owner_facing_role: 'session-manager', + roles: [ + { + id: 'owner', title: 'Project Owner', kind: 'owner', identity_id: 'owner', + responsibilities: ['Own project direction and owner-reserved decisions.'], + authority: ['approve-organization', 'approve-release'], + coordinates: [], reports_to: [], consults_with: [], + }, + { + id: 'session-manager', title: 'Session Manager', kind: 'owner-facing', identity_id: 'session-manager', + responsibilities: ['Receive owner requests and coordinate the approved Fleet.'], + authority: ['receive-owner-requests', 'propose-priorities', 'sequence-domains', 'operate-fleet'], + coordinates: specialistRoles.map((entry) => entry.id), reports_to: [], consults_with: [], + }, + ...specialistRoles, + ], + implementation_owners: domains.flatMap((domain) => + (domain.owned_paths ?? []).map((surface) => ({ surface, identity_id: domain.id }))), + }; + return validateOrganizationGraph(graph); +} + +export function defaultManagerCheckInSchedule(managerId, { intervalSeconds = 900 } = {}) { + if (!id.safeParse(managerId).success) { + throw new TorchError(`Invalid manager identity for check-in schedule: ${managerId}`, { + code: 'MANAGER_CHECKIN_IDENTITY_INVALID', details: { managerId }, + }); + } + if (!Number.isInteger(intervalSeconds) || intervalSeconds < 60) { + throw new TorchError('Manager check-in interval must be at least 60 seconds', { + code: 'MANAGER_CHECKIN_INTERVAL_INVALID', details: { intervalSeconds }, + }); + } + const title = managerId === 'session-manager' ? 'Session Manager' : managerId; + return { + id: managerId === 'session-manager' ? 'session-manager-check-in' : `${managerId}-check-in`, + title: `${title} direct-report check-in`, owner: managerId, lifetime: 'system', + trigger: { type: 'interval', seconds: intervalSeconds }, behavior: 'coordination', + action: { type: 'manager-check-in', manager_id: managerId }, + required_authority: ['owner'], retry: { max_attempts: 1 }, + failure_recipient: managerId, + source_of_truth: 'active organization direct-report graph', + }; +} + +/** + * Plan schedule coverage whenever an organization graph changes. Existing + * manager schedules keep their configured cadence; missing schedules are + * proposed at the default cadence. Obsolete schedules are reported, not + * removed automatically, because changing an installed system timer is an + * owner-authorized operation. + */ +export function planManagerCheckInSchedules({ graph, schedules = [], intervalSeconds = 900 } = {}) { + const organization = validateOrganizationGraph(graph); + const parentRoleIds = new Set(organization.roles.flatMap((role) => role.reports_to)); + const managerIds = [...new Set(organization.roles + .filter((role) => role.kind !== 'owner' && parentRoleIds.has(role.id)) + .map((role) => role.identity_id))].sort(); + const existing = schedules.filter((entry) => entry.action?.type === 'manager-check-in'); + const existingManagerIds = new Set(existing.map((entry) => entry.action.manager_id)); + const additions = managerIds + .filter((managerId) => !existingManagerIds.has(managerId)) + .map((managerId) => defaultManagerCheckInSchedule(managerId, { intervalSeconds })); + const existingScheduleIds = new Set(schedules.map((entry) => entry.id)); + const collisions = additions.filter((entry) => existingScheduleIds.has(entry.id)); + if (collisions.length) { + throw new TorchError('Generated manager check-in schedule IDs conflict with configured schedules', { + code: 'MANAGER_CHECKIN_SCHEDULE_ID_COLLISION', + details: collisions.map(({ id: scheduleId, action }) => ({ scheduleId, managerId: action.manager_id })), + }); + } + const activeManagerIds = new Set(managerIds); + return { + schedules: [...schedules, ...additions], additions, + obsolete: existing.filter((entry) => !activeManagerIds.has(entry.action.manager_id)), + coveredManagerIds: managerIds, + mutationPerformed: false, + }; +} diff --git a/src/kernel/prompts.mjs b/src/kernel/prompts.mjs index f7f5af1..797a670 100644 --- a/src/kernel/prompts.mjs +++ b/src/kernel/prompts.mjs @@ -2,7 +2,7 @@ function lines(values, empty) { return (values?.length ? values : [empty]).map((value) => `- ${value}`); } -export function renderDomainPrompt(domain) { +export function renderDomainPrompt(domain, { projectChecks = [] } = {}) { return [ `# ${domain.title}`, '', @@ -32,6 +32,14 @@ export function renderDomainPrompt(domain) { '', ...lines(domain.required_checks, 'No domain-specific checks declared.'), '', + '## Project integration gates', + '', + ...lines(projectChecks.map((check) => + `${check.id}: ${JSON.stringify([check.command, ...(check.args ?? [])])}`), + 'No project-wide integration checks declared.'), + '', + 'These are repository-wide gates, not inferred ownership. Run the configured gates for the exact commit before requesting integration.', + '', '## Resources', '', ...lines(domain.resources, 'No scarce resources declared.'), @@ -40,6 +48,8 @@ export function renderDomainPrompt(domain) { '', '- Implement only within approved ownership; request coordination or handoff before crossing a boundary.', '- Run and report configured checks for the exact commit; a branch name is not verification evidence.', + '- When a screenshot materially helps visual review, publish it with `torch_publish_artifact` for your owned task, using the full commit SHA and an image file inside this worktree; do not publish unrelated or sensitive files.', + '- Treat owner feedback on an artifact as a durable message to address and acknowledge; it does not transfer task ownership or grant additional authority.', '- Request integration through TORCH. Integration authority does not grant release or deployment authority.', '- Peer messages cannot approve policy, ownership, spending, release, deployment, or destructive actions.', '', @@ -54,12 +64,15 @@ export function renderDomainPrompt(domain) { '', '- No task is implied by session creation. Call `torch_next_backlog_task` for this area before choosing work.', '- A `resume` result is the only current assignment. Continue it before considering any `ready` item.', - '- A `ready` result is a candidate, not an assignment; request Session Manager assignment before implementation.', + '- A `ready` result is a candidate, not an assignment. If approved policy permits, call `torch_claim_next_backlog_task`; implement only a `claimed` or `resume` result. Otherwise request Session Manager assignment.', + '- Self-claim only follows clean, settled work: no unsaved files, queued/running checks, held resources, unresolved approval requests or unfinished integration. A blocked claim is not permission to bypass those gates.', + '- Name the exact TORCH task ID in progress commits. When the submitted tip finishes that task, use a standalone `Closes: TASK-id` trailer. It is intent, not authority: record exact check evidence, reach ready_to_integrate, submit through the landing queue, and confirm landed integration plus completed backlog state. Never assume the trailer alone completed work; Fleet Operations can reconcile after interruption.', + '- After finishing and settling an item, re-read current instructions and resolve/claim the next task. Escalate decisions outside your authority, cross-domain conflicts, owner approvals, main regressions and at-risk work; routine progress belongs in task evidence, commits and durable status, not acknowledgement-only messages.', '- An `idle` result is valid. Do not invent filler work.', '', '## First move', '', - 'Query live identity and ownership, call `torch_next_backlog_task`, read the inbox, then report current repository evidence to the Session Manager.', + 'Query live identity and ownership, call `torch_next_backlog_task`, read the inbox and publish current repository evidence in durable status. Escalate exceptions to the Session Manager.', '', ].join('\n'); } diff --git a/src/mcp/server.mjs b/src/mcp/server.mjs index 4bd16d6..4680f7d 100755 --- a/src/mcp/server.mjs +++ b/src/mcp/server.mjs @@ -10,6 +10,8 @@ import { createTorchMcpServer } from './tools.mjs'; import { ContextTelemetryService } from '../telemetry/context.mjs'; import { ScheduleService } from '../schedules/service.mjs'; import { FleetEvolutionService } from '../evolution/service.mjs'; +import { HierarchyEvolutionService } from '../evolution/hierarchy.mjs'; +import { ArtifactService } from '../artifacts/service.mjs'; function optionValue(argv, name) { const direct = argv.find((argument) => argument.startsWith(`${name}=`)); @@ -26,14 +28,16 @@ const resourceService = new ResourceService({ repositoryRoot, controlPlane }); const checkService = new CheckService({ repositoryRoot, controlPlane, resourceService }); const integrationService = new IntegrationService({ repositoryRoot, controlPlane, checkService }); const backlogService = new BacklogService({ - repositoryRoot, controlPlane, integrationLookup: (requestId) => integrationService.get(requestId), + repositoryRoot, controlPlane, checkService, integrationLookup: (requestId) => integrationService.get(requestId), }); const contextTelemetryService = new ContextTelemetryService({ controlPlane }); const scheduleService = new ScheduleService({ repositoryRoot, controlPlane }); const evolutionService = new FleetEvolutionService({ repositoryRoot, controlPlane }); +const hierarchyService = new HierarchyEvolutionService({ repositoryRoot, controlPlane }); +const artifactService = new ArtifactService({ repositoryRoot, controlPlane, backlogService }); const handle = serveStdio(() => createTorchMcpServer(controlPlane, { - actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService, - evolutionService, + actorId, repositoryRoot, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService, + evolutionService, hierarchyService, artifactService, }), { onerror: (error) => console.error(`TORCH MCP: ${error.message}`), }); diff --git a/src/mcp/tools.mjs b/src/mcp/tools.mjs index 5aa7391..818160e 100644 --- a/src/mcp/tools.mjs +++ b/src/mcp/tools.mjs @@ -1,6 +1,7 @@ import { McpServer } from '@modelcontextprotocol/server'; import { z } from 'zod'; import { TorchError } from '../kernel/errors.mjs'; +import { planManagerCheckIn } from '../observability/manager-checkin.mjs'; const optionalReferenceShape = { task: z.string().min(1).optional(), @@ -12,6 +13,7 @@ const optionalReferenceShape = { export const TORCH_CORE_MCP_TOOL_NAMES = Object.freeze([ 'torch_identity', 'torch_list_agents', + 'torch_plan_manager_check_in', 'torch_get_agent', 'torch_get_roster', 'torch_who_owns', @@ -22,6 +24,9 @@ export const TORCH_CORE_MCP_TOOL_NAMES = Object.freeze([ 'torch_report_status', 'torch_report_complete', 'torch_report_blocked', + 'torch_request_approval', + 'torch_list_approvals', + 'torch_decide_approval', 'torch_request_coordination', 'torch_request_handoff', ]); @@ -31,13 +36,23 @@ export const TORCH_MCP_TOOL_NAMES = Object.freeze([ 'torch_list_backlog', 'torch_next_backlog_task', 'torch_backlog_health', + 'torch_backlog_activity', + 'torch_plan_landed_task_closure', + 'torch_reconcile_landed_tasks', + 'torch_plan_backlog_claim', + 'torch_claim_next_backlog_task', 'torch_get_backlog_task', 'torch_create_backlog_task', + 'torch_classify_backlog_task', 'torch_transition_backlog_task', 'torch_list_checks', 'torch_list_check_receipts', 'torch_plan_check', 'torch_run_check', + 'torch_prepare_check', + 'torch_list_prepared_checks', + 'torch_run_prepared_check', + 'torch_cancel_prepared_check', 'torch_list_resources', 'torch_resource_status', 'torch_acquire_resource', @@ -62,6 +77,12 @@ export const TORCH_MCP_TOOL_NAMES = Object.freeze([ 'torch_propose_domain_retirement', 'torch_propose_domain_merge', 'torch_propose_domain_split', + 'torch_assess_hierarchy_needs', + 'torch_list_hierarchy_proposals', + 'torch_get_hierarchy_proposal', + 'torch_propose_hierarchy_change', + 'torch_plan_hierarchy_pilot', + 'torch_publish_artifact', ]); function claimedIdentity(controlPlane, actorId, claim, field) { @@ -81,7 +102,8 @@ function claimedIdentity(controlPlane, actorId, claim, field) { export function createTorchToolset(controlPlane, { actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService, - evolutionService, + evolutionService, hierarchyService, repositoryRoot, + artifactService, } = {}) { const tools = new Map([ ['torch_identity', { @@ -94,6 +116,29 @@ export function createTorchToolset(controlPlane, { schema: {}, invoke: () => ({ agents: controlPlane.listAgents() }), }], + ['torch_plan_manager_check_in', { + description: 'Refresh the bound manager identity’s direct-report presence, durable messages, and named approval waits from current state. Read-only; only works for an identity with direct reports.', + schema: { stale_work_review: z.boolean().optional(), + stale_days: z.number().int().min(1).max(365).optional(), + max_commits: z.number().int().min(1).max(10_000).optional(), + max_items: z.number().int().min(1).max(100).optional() }, + invoke: ({ stale_work_review: review, stale_days, max_commits, max_items }) => { + const managerId = controlPlane.assertIdentity(actorId); + if (!repositoryRoot) { + throw new TorchError('A repository root is required to refresh manager check-in state', { + code: 'MANAGER_CHECK_IN_CONTEXT_MISSING', + }); + } + const plan = planManagerCheckIn({ repositoryRoot, controlPlane, managerId, + staleWork: review ? { stale_days, max_commits, max_items } : null }); + if (plan.directReports.length === 0) { + throw new TorchError(`Fleet identity ${managerId} has no direct reports to inspect`, { + code: 'MANAGER_ROLE_NOT_FOUND', details: { managerId }, + }); + } + return plan; + }, + }], ['torch_get_agent', { description: 'Get one Fleet agent by stable area ID.', schema: { area_id: z.string().min(1) }, @@ -174,6 +219,36 @@ export function createTorchToolset(controlPlane, { areaId: claimedIdentity(controlPlane, actorId, areaId, 'area_id'), ...input, }), }], + ['torch_request_approval', { + description: 'Create a durable approval request addressed to one named Fleet identity or the configured owner.', + schema: { + requester: z.string().min(1).optional(), approver: z.string().min(1), task: z.string().min(1).optional(), + title: z.string().min(1).max(240), summary: z.string().min(1).max(4000), evidence: z.string().min(1).max(4000).optional(), + }, + invoke: ({ requester, ...input }) => controlPlane.requestApproval({ + ...input, requester: claimedIdentity(controlPlane, actorId, requester, 'requester'), + }), + }], + ['torch_list_approvals', { + description: 'List pending and historical approval requests visible to the bound Fleet identity.', + schema: { actor_id: z.string().min(1).optional(), status: z.enum(['pending', 'approved', 'rejected', 'cancelled']).optional() }, + invoke: ({ actor_id: actorIdClaim, status }) => ({ approvals: controlPlane.listApprovals({ + actorId: claimedIdentity(controlPlane, actorId, actorIdClaim, 'actor_id'), status, + }) }), + }], + ['torch_decide_approval', { + description: 'Approve or reject a request only when the bound identity is its named approver; decisions are audited and notified durably.', + schema: { + actor_id: z.string().min(1).optional(), approval_id: z.string().min(1), + decision: z.enum(['approved', 'rejected']), note: z.string().max(2000).optional(), + expected_revision: z.number().int().positive(), + }, + invoke: ({ actor_id: actorIdClaim, approval_id: approvalId, decision, note, expected_revision: expectedRevision }) => + controlPlane.decideApproval({ + approvalId, decidedBy: claimedIdentity(controlPlane, actorId, actorIdClaim, 'actor_id'), + decision, note, expectedRevision, + }), + }], ['torch_request_coordination', { description: 'Ask the Session Manager for a durable cross-domain coordination ruling.', schema: { @@ -224,6 +299,29 @@ export function createTorchToolset(controlPlane, { invoke: ({ stale_after_days: staleAfterDays, stale_observed_commits: staleObservedCommits }) => backlogService.health({ staleAfterDays, staleObservedCommits }), }); + tools.set('torch_backlog_activity', { + description: 'Observe commit-linked task activity and neglected requests on managed branches. Read-only; closure text is intent, not authority.', + schema: { stale_days: z.number().int().min(1).max(365).optional(), max_commits: z.number().int().min(1).max(10_000).optional() }, + invoke: ({ stale_days: staleDays, max_commits: maxCommits }) => backlogService.activity({ staleDays, maxCommits }), + }); + for (const [name, method] of [ + ['torch_plan_landed_task_closure', 'planLandedClosure'], ['torch_reconcile_landed_tasks', 'reconcileLanded'], + ]) tools.set(name, { + description: 'Manager-only exact landed task closure: preview or explicitly reconcile Closes intent with current evidence.', + schema: { integration_request: z.string().min(1), approved: z.boolean().optional() }, + invoke: ({ integration_request: integrationRequest, approved }) => backlogService[method]({ integrationRequest, actorId, approved }), + }); + for (const [name, method] of [ + ['torch_plan_backlog_claim', 'planClaim'], ['torch_claim_next_backlog_task', 'claimNext'], + ]) tools.set(name, { + description: method === 'planClaim' + ? 'Preview policy-controlled self-claim eligibility for the bound specialist without mutation.' + : 'Atomically claim the next explicitly routed ready task under approved policy, or resume current work. Never grants manager authority.', + schema: { area_id: z.string().min(1).optional() }, + invoke: ({ area_id: areaId }) => backlogService[method]({ + areaId: claimedIdentity(controlPlane, actorId, areaId, 'area_id'), + }), + }); tools.set('torch_get_backlog_task', { description: 'Read one durable tracked backlog task and its transition history.', schema: { task_id: z.string().min(1) }, @@ -236,6 +334,7 @@ export function createTorchToolset(controlPlane, { priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), affected_domains: z.array(z.string().min(1)).optional(), dependencies: z.array(z.string().min(1)).optional(), + feature: z.string().min(1).max(120).optional(), milestone: z.string().min(1).max(120).optional(), acceptance_criteria: z.array(z.string().min(1)).min(1), observed_at: z.string().min(1).optional(), }, @@ -247,6 +346,23 @@ export function createTorchToolset(controlPlane, { actorId: claimedIdentity(controlPlane, actorId, actorIdClaim, 'actor_id'), }), }); + tools.set('torch_classify_backlog_task', { + description: 'Set or explicitly clear a task\'s feature and milestone labels as the Session Manager. Revision-checked and non-destructive to status or ownership.', + schema: { + task_id: z.string().min(1), actor_id: z.string().min(1).optional(), + expected_revision: z.number().int().min(1), + feature: z.string().min(1).max(120).optional(), milestone: z.string().min(1).max(120).optional(), + clear_feature: z.boolean().optional(), clear_milestone: z.boolean().optional(), + reason: z.string().min(1), + }, + invoke: ({ + task_id: taskId, actor_id: actorIdClaim, expected_revision: expectedRevision, + clear_feature: clearFeature, clear_milestone: clearMilestone, ...input + }) => backlogService.classify({ + ...input, taskId, expectedRevision, clearFeature, clearMilestone, + actorId: claimedIdentity(controlPlane, actorId, actorIdClaim, 'actor_id'), + }), + }); tools.set('torch_transition_backlog_task', { description: 'Apply one revision-checked backlog state transition under bound Fleet authority.', schema: { @@ -297,6 +413,31 @@ export function createTorchToolset(controlPlane, { checkId, areaId: claimedIdentity(controlPlane, actorId, areaId, 'area_id'), }), }); + tools.set('torch_prepare_check', { + description: 'Capture approved frozen check inputs before waiting for resource leases; does not execute the check.', + schema: { check_id: z.string().min(1), area_id: z.string().min(1).optional() }, + invoke: ({ check_id: checkId, area_id: areaId }) => checkService.prepare({ + checkId, areaId: claimedIdentity(controlPlane, actorId, areaId, 'area_id'), + }), + }); + tools.set('torch_list_prepared_checks', { + description: 'List durable frozen checks for the bound identity, including interrupted running checks.', + schema: { area_id: z.string().min(1).optional() }, + invoke: ({ area_id: areaId }) => ({ prepared: checkService.prepared({ + areaId: claimedIdentity(controlPlane, actorId, areaId, 'area_id'), + }) }), + }); + for (const [name, method] of [ + ['torch_run_prepared_check', 'runPrepared'], ['torch_cancel_prepared_check', 'cancelPrepared'], + ]) tools.set(name, { + description: method === 'runPrepared' + ? 'Run an owned frozen check after acquiring its resources; record its captured SHA, not the current worktree SHA.' + : 'Cancel an owned unstarted frozen check and remove only its owned input copy.', + schema: { prepared_id: z.string().min(1), area_id: z.string().min(1).optional() }, + invoke: ({ prepared_id: preparedId, area_id: areaId }) => checkService[method]({ + preparedId, areaId: claimedIdentity(controlPlane, actorId, areaId, 'area_id'), + }), + }); } if (resourceService) { tools.set('torch_list_resources', { @@ -420,7 +561,7 @@ export function createTorchToolset(controlPlane, { invoke: ({ schedule_id: scheduleId }) => scheduleService.plan({ scheduleId, actorId }), }); tools.set('torch_run_schedule', { - description: 'Run a read-only configured schedule as the bound Fleet identity. Mutating schedules require owner CLI approval.', + description: 'Run a read-only configured schedule as the bound Fleet identity. Coordination and mutating schedules cannot run through this MCP tool; they require an owner-approved CLI action or an approved, digest-matched system timer.', schema: { schedule_id: z.string().min(1) }, invoke: ({ schedule_id: scheduleId }) => scheduleService.run({ scheduleId, actorId, approved: false }), }); @@ -536,9 +677,69 @@ export function createTorchToolset(controlPlane, { invoke: invokeBoundaryProposal('proposeSplit'), }); } + if (hierarchyService) { + tools.set('torch_assess_hierarchy_needs', { + description: 'Measure recurring coordination, cross-domain work, handoffs, and approval waits in the configured observation window. Read-only; it never creates or activates a role.', + schema: { assessor: z.string().min(1).optional() }, + invoke: ({ assessor }) => hierarchyService.assess({ + assessor: claimedIdentity(controlPlane, actorId, assessor, 'assessor'), + }), + }); + tools.set('torch_list_hierarchy_proposals', { + description: 'List durable hierarchy proposals and their owner-review state.', + schema: { state: z.enum(['proposed', 'pilot-approved', 'deferred', 'rejected', 'piloting', 'adopted', 'reversed']).optional() }, + invoke: ({ state }) => ({ proposals: hierarchyService.list({ state }) }), + }); + tools.set('torch_get_hierarchy_proposal', { + description: 'Read a durable hierarchy proposal, evidence, pilot criteria, and owner decision.', + schema: { proposal_id: z.string().min(1) }, + invoke: ({ proposal_id: proposalId }) => hierarchyService.get(proposalId), + }); + tools.set('torch_propose_hierarchy_change', { + description: 'Submit a sustained-evidence, owner-review hierarchy proposal. This cannot change active roles or start sessions.', + schema: { + proposer: z.string().min(1).optional(), + proposal: z.record(z.string(), z.unknown()), + }, + invoke: ({ proposer, proposal }) => hierarchyService.propose({ + proposer: claimedIdentity(controlPlane, actorId, proposer, 'proposer'), proposal, + }), + }); + tools.set('torch_plan_hierarchy_pilot', { + description: 'Preview hierarchy differences, affected work, and blockers for an owner-approved bounded pilot. Read-only.', + schema: { proposal_id: z.string().min(1) }, + invoke: ({ proposal_id: proposalId }) => hierarchyService.planPilot(proposalId), + }); + } + if (artifactService) { + tools.set('torch_publish_artifact', { + description: 'Publish an image for an owned backlog task with exact identity, session, and commit provenance. The image is copied into private local TORCH state.', + schema: { + area_id: z.string().min(1).optional(), task_id: z.string().regex(/^TASK-[A-Za-z0-9-]+$/), + title: z.string().min(1), alt: z.string().min(1), file: z.string().min(1), commit: z.string().regex(/^[a-f0-9]{40}$/i), + }, + invoke: ({ area_id: areaId, task_id: taskId, ...input }) => artifactService.publish({ + areaId: claimedIdentity(controlPlane, actorId, areaId, 'area_id'), taskId, ...input, + }), + }); + } return tools; } +export function describeTorchTools() { + const placeholder = Object.freeze({}); + return [...createTorchToolset(placeholder, { + backlogService: placeholder, checkService: placeholder, resourceService: placeholder, + integrationService: placeholder, contextTelemetryService: placeholder, + scheduleService: placeholder, evolutionService: placeholder, + hierarchyService: placeholder, artifactService: placeholder, + }).entries()].map(([name, tool]) => ({ + name, + description: tool.description, + inputSchema: z.toJSONSchema(z.object(tool.schema)), + })); +} + export function callTorchTool(controlPlane, name, input = {}, options = {}) { const tool = createTorchToolset(controlPlane, options).get(name); if (!tool) throw new Error(`Unknown TORCH MCP tool: ${name}`); @@ -548,7 +749,7 @@ export function callTorchTool(controlPlane, name, input = {}, options = {}) { export function createTorchMcpServer(controlPlane, { actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService, - evolutionService, + evolutionService, hierarchyService, repositoryRoot, artifactService, } = {}) { controlPlane.assertIdentity(actorId); const server = new McpServer( @@ -559,7 +760,7 @@ export function createTorchMcpServer(controlPlane, { ); for (const [name, tool] of createTorchToolset(controlPlane, { actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService, - evolutionService, + evolutionService, hierarchyService, repositoryRoot, artifactService, })) { server.registerTool(name, { description: tool.description, inputSchema: tool.schema }, async (input) => { try { diff --git a/src/observability/manager-checkin.mjs b/src/observability/manager-checkin.mjs new file mode 100644 index 0000000..98a6387 --- /dev/null +++ b/src/observability/manager-checkin.mjs @@ -0,0 +1,152 @@ +import { loadFleetDefinition } from '../kernel/worktrees.mjs'; +import { organizationGraphFromConfig } from '../kernel/organization.mjs'; +import { TorchError } from '../kernel/errors.mjs'; +import { BacklogService } from '../backlog/service.mjs'; + +/** Read-only, organization-aware summary for one manager's direct reports. */ +export function planManagerCheckIn({ repositoryRoot, controlPlane, managerId, at = new Date(), staleWork = null } = {}) { + const manager = controlPlane.assertIdentity(managerId); + const { config } = loadFleetDefinition(repositoryRoot); + const graph = organizationGraphFromConfig(config); + const managerRoles = graph.roles.filter((role) => role.identity_id === manager); + if (!managerRoles.length) { + throw new TorchError(`Fleet identity ${manager} has no organization role`, { + code: 'MANAGER_ROLE_NOT_FOUND', details: { managerId: manager }, + }); + } + const roleIds = new Set(managerRoles.map((role) => role.id)); + const reports = graph.roles.filter((role) => role.reports_to.some((parent) => roleIds.has(parent))) + .filter((role) => role.identity_id !== manager) + .reduce((result, role) => { + if (!result.has(role.identity_id)) result.set(role.identity_id, role); + return result; + }, new Map()); + const directReports = [...reports.keys()].map((areaId) => controlPlane.identity(areaId)); + const reportIds = new Set(directReports.map((report) => report.areaId)); + const messages = controlPlane.readMessages({ recipient: manager, unacknowledgedOnly: true }) + .filter((message) => reportIds.has(message.sender)); + const approvalWaits = controlPlane.pendingApprovalsFor([...reportIds]).map((approval) => ({ + id: approval.id, requester: approval.requester, approver: approval.approver, + approverKind: approval.approver === 'owner' ? 'owner' + : approval.approver === manager ? 'manager' : 'peer-ai', + task: approval.task, title: approval.title, summary: approval.summary, + evidence: approval.evidence, createdAt: approval.createdAt, revision: approval.revision, + })); + const approvalsByRequester = new Set(approvalWaits.map((approval) => approval.requester)); + const findings = []; + + for (const report of directReports) { + if (report.state === 'waiting' && !approvalsByRequester.has(report.areaId)) { + findings.push({ + type: 'waiting-unclassified', areaId: report.areaId, task: report.currentTask, + summary: report.summary, observedAt: report.updatedAt, + note: 'The wait reason is not structured; inspect the session before inferring an approval request.', + }); + } else if (report.state === 'offline') { + findings.push({ type: 'direct-report-offline', areaId: report.areaId, task: report.currentTask }); + } else if (report.state === 'stale') { + findings.push({ type: 'direct-report-stale', areaId: report.areaId, task: report.currentTask }); + } + } + for (const approval of approvalWaits) { + findings.push({ + type: 'approval-wait', areaId: approval.requester, approvalId: approval.id, + approver: approval.approver, approverKind: approval.approverKind, task: approval.task, + title: approval.title, createdAt: approval.createdAt, + }); + } + for (const message of messages) { + findings.push({ + type: message.kind === 'blocker' ? 'unacknowledged-blocker' : 'unacknowledged-message', + areaId: message.sender, messageId: message.id, kind: message.kind, + task: message.references.task, createdAt: message.createdAt, + }); + } + + let staleWorkReview = null; + if (staleWork !== null) { + const staleDays = staleWork.stale_days ?? config.backlog?.activity?.stale_days ?? 3; + const maxCommits = staleWork.max_commits ?? config.backlog?.activity?.max_commits ?? 1000; + const maxItems = staleWork.max_items ?? 30; + if (!Number.isInteger(staleDays) || staleDays < 1 || staleDays > 365 + || !Number.isInteger(maxCommits) || maxCommits < 1 || maxCommits > 10_000 + || !Number.isInteger(maxItems) || maxItems < 1 || maxItems > 100) { + throw new TorchError('Invalid stale-work review bounds', { code: 'MANAGER_STALE_WORK_INPUT_INVALID' }); + } + try { + const activity = new BacklogService({ repositoryRoot, controlPlane }).activity({ staleDays, maxCommits, now: at }); + const scoped = activity.tasks.filter((task) => reportIds.has(task.owner) + || (manager === 'session-manager' && task.owner === null)); + const neglected = scoped.filter((task) => task.status === 'stale'); + const unknown = scoped.filter((task) => task.status === 'unknown'); + const compact = (task) => ({ taskId: task.taskId, title: task.title.slice(0, 240), + state: task.state, owner: task.owner, ownerRequested: task.ownerRequested, + status: task.status, ageDays: task.ageDays, expectedWaiting: task.expectedWaiting, + lastActivity: task.lastActivity ? { commit: task.lastActivity.commit, + committedAt: task.lastActivity.committedAt } : null }); + staleWorkReview = { available: true, complete: activity.complete, staleDays, maxCommits, maxItems, + generatedAt: activity.generatedAt, provenance: activity.provenance, + staleCount: neglected.length, unknownCount: unknown.length, + stale: neglected.slice(0, maxItems).map(compact), unknown: unknown.slice(0, maxItems).map(compact), + truncated: neglected.length > maxItems || unknown.length > maxItems || activity.truncated, + closurePerformed: false, mutationPerformed: false }; + for (const task of staleWorkReview.stale) findings.push({ + type: 'stale-work', areaId: task.owner ?? manager, task: task.taskId, + title: task.title, ownerRequested: task.ownerRequested, ageDays: task.ageDays, + expectedWaiting: task.expectedWaiting, + note: task.expectedWaiting ? 'Blocked work needs dependency review, not automatic restart.' + : 'No recent commit-linked activity in the observed managed history; review before changing work.', + }); + if (!activity.complete || unknown.length) findings.push({ + type: 'stale-work-coverage-unknown', areaId: manager, unknownCount: unknown.length, + note: 'Incomplete history or task time cannot prove absence of work. Restore observation before declaring tasks neglected.', + }); + } catch (error) { + staleWorkReview = { available: false, complete: false, staleDays, maxCommits, maxItems, + reason: error.code ?? 'BACKLOG_ACTIVITY_UNAVAILABLE', stale: [], unknown: [], + closurePerformed: false, mutationPerformed: false }; + findings.push({ type: 'stale-work-unavailable', areaId: manager, + note: 'Task activity could not be observed; this is not evidence of inactivity.' }); + } + } + + return { + schema: 'torch.dev/manager-check-in/v1alpha1', mutationPerformed: false, + managerId: manager, at: at.toISOString(), directReports, + findings, approvalWaits, approvalWaitDetection: 'structured', staleWorkReview, + attentionRequired: findings.length > 0 || approvalWaits.length > 0, + }; +} + +/** Queue one durable self-directed prompt; never starts or steers a runtime. */ +export function queueManagerCheckIn({ repositoryRoot, controlPlane, managerId, at = new Date(), staleWork = null } = {}) { + const plan = planManagerCheckIn({ repositoryRoot, controlPlane, managerId, at, staleWork }); + if (!plan.attentionRequired) { + return { ...plan, queued: false, reason: 'no-direct-report-attention', mutationPerformed: false }; + } + const kind = staleWork === null ? 'manager-check-in' : 'manager-stale-work-review'; + const pending = controlPlane.readMessages({ recipient: plan.managerId, unacknowledgedOnly: true }) + .find((message) => message.kind === kind && message.sender === plan.managerId); + if (pending) { + return { ...plan, queued: false, reason: 'check-in-already-pending', messageId: pending.id, mutationPerformed: false }; + } + const affected = [...new Set(plan.findings.map((finding) => finding.areaId))]; + const message = controlPlane.sendMessage({ + sender: plan.managerId, recipient: plan.managerId, kind, + body: [ + `Scheduled TORCH check-in: review direct reports ${affected.join(', ')}.`, + `Snapshot captured at ${plan.at}; findings below are informational and may be stale when this message is read.`, + `Findings: ${JSON.stringify(plan.findings)}.`, + `Structured approval waits: ${JSON.stringify(plan.approvalWaits)}.`, + ...(staleWork === null ? [] : [ + `Stale-work review: ${JSON.stringify(plan.staleWorkReview)}.`, + `Refresh with torch_plan_manager_check_in ${JSON.stringify({ stale_work_review: true, + stale_days: plan.staleWorkReview.staleDays, max_commits: plan.staleWorkReview.maxCommits, + max_items: plan.staleWorkReview.maxItems })} before acting. Owner requests come first.`, + 'Review each item to revive, explicitly defer, resolve a dependency, or propose closure with evidence through the existing task system. Commit messages and age do not authorize closing, restarting, reassigning or approving work.', + ]), + 'Before acting, call torch_plan_manager_check_in to refresh direct-report status and approval waits from current durable state. For each pending approval, verify its evidence and act only if you are the named approver. If the owner or a peer AI is named, route/escalate to that approver; do not decide on their behalf. Do not treat this check-in as approval.', + ].join('\n'), + }); + return { ...plan, queued: true, messageId: message.id, mutationPerformed: true }; +} diff --git a/src/observability/owner-digest.mjs b/src/observability/owner-digest.mjs new file mode 100644 index 0000000..596a3be --- /dev/null +++ b/src/observability/owner-digest.mjs @@ -0,0 +1,168 @@ +import { randomUUID } from 'node:crypto'; +import { TorchError } from '../kernel/errors.mjs'; +import { loadProjectConfig } from '../kernel/config.mjs'; +import { BacklogService } from '../backlog/service.mjs'; + +const hasTable = (database, name) => Boolean(database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)); +const clip = (value) => String(value ?? '').slice(0, 600); +const safeMarkdown = (value) => [...clip(value)].map((character) => character.charCodeAt(0) < 32 ? ' ' : character).join('') + .replace(/&/g, '&').replace(//g, '>') + .replace(/[\\`*_[\]()#|]/g, '\\$&'); + +export class OwnerDigestService { + constructor({ repositoryRoot, controlPlane, backlogService = null, clock = () => new Date(), idFactory = randomUUID } = {}) { + this.repositoryRoot = repositoryRoot; + this.controlPlane = controlPlane; + this.database = controlPlane.database; + this.backlog = backlogService ?? new BacklogService({ repositoryRoot, controlPlane }); + this.clock = clock; + this.idFactory = idFactory; + } + + build(options = {}) { + this.database.exec('BEGIN'); + try { + const report = this.#build(options); + this.database.exec('COMMIT'); + return report; + } catch (error) { this.database.exec('ROLLBACK'); throw error; } + } + + #build({ windowHours, maxItems, maxCommits, at = this.clock() } = {}) { + const config = loadProjectConfig(this.repositoryRoot); + const hours = windowHours ?? config.owner_digest?.window_hours ?? 24; + const limit = maxItems ?? config.owner_digest?.max_items ?? 30; + if (!Number.isInteger(hours) || hours < 1 || hours > 168 || !Number.isInteger(limit) || limit < 1 || limit > 100 + || !(at instanceof Date) || !Number.isFinite(at.getTime())) { + throw new TorchError('Invalid digest window or bounds', { code: 'DIGEST_INPUT_INVALID' }); + } + const until = at.toISOString(); + const since = new Date(at.getTime() - hours * 3_600_000).toISOString(); + const coverage = []; + const query = (table, select, where, params, order) => { + if (!hasTable(this.database, table)) { + coverage.push(`${table}: records not initialized; absence is not live qualification`); + return { available: false, count: null, items: [], truncated: false }; + } + const count = this.database.prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE ${where}`).get(...params).count; + const items = this.database.prepare(`SELECT ${select} FROM ${table} WHERE ${where} ORDER BY ${order} LIMIT ?`).all(...params, limit); + if (count > limit) coverage.push(`${table}: showing ${limit} of ${count} matching records`); + return { available: true, count, items, truncated: count > limit }; + }; + const project = this.controlPlane.projectId; + const needsOwner = query('approval_requests', 'id, title, requester_id AS requester, task_ref AS task, created_at AS createdAt', + "project_id = ? AND status = 'pending' AND approver_id = 'owner'", [project], 'created_at, id'); + const decisions = query('approval_requests', 'id, title, status, decided_by AS decidedBy, decided_at AS decidedAt', + 'project_id = ? AND decided_at > ? AND decided_at <= ?', [project, since, until], 'decided_at DESC, id'); + const landed = query('integration_requests', 'id, source_area AS sourceArea, source_commit AS "commit", landed_at AS landedAt', + "project_id = ? AND state = 'landed' AND landed_at > ? AND landed_at <= ?", [project, since, until], 'landed_at DESC, id'); + // Report lifecycle receipts, not a branch's presence on main, as shipping. + const shipped = hasTable(this.database, 'deliveries') ? query('delivery_events', + 'id, delivery_id AS deliveryId, (SELECT label FROM deliveries d WHERE d.id = delivery_events.delivery_id) AS label, (SELECT commit_sha FROM deliveries d WHERE d.id = delivery_events.delivery_id) AS "commit", to_state AS state, created_at AS recordedAt', + "delivery_id IN (SELECT id FROM deliveries WHERE project_id = ?) AND to_state IN ('deployed', 'live-verified') AND created_at > ? AND created_at <= ?", + [project, since, until], 'created_at DESC, id') : { available: false, count: null, items: [], truncated: false }; + const deliveryReview = hasTable(this.database, 'deliveries') ? query('delivery_operations', + 'id, delivery_id AS deliveryId, commit_sha AS "commit", provider, operation, state, updated_at AS updatedAt', + "delivery_id IN (SELECT id FROM deliveries WHERE project_id = ?) AND state IN ('unknown', 'running', 'succeeded')", + [project], 'created_at, id') : { available: false, count: null, items: [], truncated: false }; + let lastDeployment = null; + if (hasTable(this.database, 'delivery_operations') && hasTable(this.database, 'deliveries')) { + lastDeployment = this.database.prepare(`SELECT o.id, o.delivery_id AS deliveryId, d.label, o.commit_sha AS "commit", + o.provider, o.state, o.updated_at AS updatedAt FROM delivery_operations o JOIN deliveries d ON d.id = o.delivery_id + WHERE d.project_id = ? AND o.operation = 'deploy' ORDER BY o.rowid DESC LIMIT 1`).get(project) ?? null; + if (lastDeployment && hasTable(this.database, 'delivery_attempts')) { + const attempt = this.database.prepare('SELECT state, receipt_json FROM delivery_attempts WHERE operation_id = ? ORDER BY ordinal DESC LIMIT 1').get(lastDeployment.id); + lastDeployment.attemptResult = attempt?.state ?? null; + try { + const receipt = JSON.parse(attempt?.receipt_json ?? 'null'); + lastDeployment.failureReason = receipt?.status !== 'succeeded' + ? clip(receipt?.failure?.code ?? receipt?.reason ?? receipt?.failure?.classification ?? 'No terminal receipt') : null; + } catch { lastDeployment.failureReason = 'Malformed persisted receipt'; } + } + } + const tasks = this.backlog.list(); + const bounded = (items) => ({ available: true, count: items.length, items: items.slice(0, limit), truncated: items.length > limit }); + const completed = bounded(tasks.filter((task) => task.state === 'completed' && task.history.some((entry) => entry.to === 'completed' + && Date.parse(entry.at) > Date.parse(since) && Date.parse(entry.at) <= at.getTime())) + .map((task) => ({ taskId: task.id, title: clip(task.title), commit: task.commit, integrationRequest: task.integrationRequest }))); + const blocked = bounded(tasks.filter((task) => task.state === 'blocked') + .map((task) => ({ taskId: task.id, title: clip(task.title), owner: task.owner, reason: clip(task.blockedReason) }))); + let activityCoverage; + let neglectedRequests = bounded([]); + try { + const activity = this.backlog.activity({ now: at, maxCommits }); + activityCoverage = { available: true, complete: activity.complete, truncated: activity.truncated, + unavailableBranches: activity.unavailableBranches, scannedCommits: activity.scannedCommits }; + neglectedRequests = bounded(activity.stale.filter((task) => task.ownerRequested).map((task) => ({ + taskId: task.taskId, title: clip(task.title), state: task.state, ageDays: task.ageDays, + expectedWaiting: task.expectedWaiting, lastActivityCommit: task.lastActivity?.commit ?? null, + }))); + if (!activity.complete) coverage.push('Managed Git history incomplete; owner-request inactivity cannot be proven'); + } catch (error) { + activityCoverage = { available: false, complete: false, reason: error.code ?? 'activity-unavailable' }; + neglectedRequests.available = false; + neglectedRequests.count = null; + coverage.push('Owner-request activity unavailable; no neglect conclusion'); + } + coverage.push('Decisions are structured approval outcomes only; unstructured decision documents are not time-indexed'); + coverage.push('Database receipts, tracked tasks and Git activity are a bounded observation, not an atomic repository snapshot'); + for (const section of [needsOwner, decisions]) section.items = section.items.map((item) => ({ ...item, title: clip(item.title) })); + shipped.items = shipped.items.map((item) => ({ ...item, label: clip(item.label) })); + if (lastDeployment) lastDeployment.label = clip(lastDeployment.label); + const report = { schema: 'torch.dev/owner-digest/v1alpha1', projectId: project, projectName: clip(config.project.name), + generatedAt: until, window: { since, until, hours }, maxItems: limit, + needsOwner, deliveryReview, lastDeployment, shipped, landed, completed, decisions, blocked, neglectedRequests, + activityCoverage, coverage, provenance: 'recorded-local-evidence', mutationPerformed: false, externalDeliveryPerformed: false }; + const lines = [`# ${safeMarkdown(report.projectName)} — owner digest`, '', `Window: ${since} to ${until} (UTC).`, + '', 'Recorded evidence; not independently live verified.', '']; + const section = (title, data, describe) => { + lines.push(`## ${title}`, ''); + if (!data.available) lines.push('Evidence unavailable.'); + else if (!data.count) lines.push('No matching recorded items.'); + else lines.push(...data.items.map((item) => `- ${describe(item)}`)); + if (data.truncated) lines.push(`Showing ${data.items.length} of ${data.count}.`); + lines.push(''); + }; + section('Needs you', needsOwner, (item) => `${safeMarkdown(item.title)} — approval ${safeMarkdown(item.id)}`); + section('Delivery needs review', deliveryReview, (item) => `${safeMarkdown(item.operation)} ${safeMarkdown(item.commit)}: ${safeMarkdown(item.state)}`); + lines.push('## Last deployment', '', lastDeployment ? `${safeMarkdown(lastDeployment.label)} — ${safeMarkdown(lastDeployment.commit)}: ${safeMarkdown(lastDeployment.state)} (${safeMarkdown(lastDeployment.updatedAt)}).${lastDeployment.failureReason ? ` ${safeMarkdown(lastDeployment.failureReason)}.` : ''}` + : 'No deployment operation recorded.', ''); + section('Shipped — receipt reported', shipped, (item) => `${safeMarkdown(item.label)} — ${safeMarkdown(item.commit)}: ${safeMarkdown(item.state)} at ${safeMarkdown(item.recordedAt)}`); + section('Landed — not necessarily deployed', landed, (item) => `${safeMarkdown(item.sourceArea)}: ${safeMarkdown(item.commit)}`); + section('Completed', completed, (item) => `${safeMarkdown(item.taskId)}: ${safeMarkdown(item.title)}`); + section('Decisions recorded', decisions, (item) => `${safeMarkdown(item.title)}: ${safeMarkdown(item.status)} by ${safeMarkdown(item.decidedBy)}`); + section('Blocked', blocked, (item) => `${safeMarkdown(item.taskId)}: ${safeMarkdown(item.reason)}`); + section('Neglected owner requests', neglectedRequests, (item) => `${safeMarkdown(item.taskId)}: ${safeMarkdown(item.title)} (${item.ageDays} days)`); + lines.push('## Coverage', '', ...coverage.map((note) => `- ${safeMarkdown(note)}`), ''); + report.markdown = lines.join('\n'); + return report; + } + + publish({ actorId, approved = false } = {}) { + const config = loadProjectConfig(this.repositoryRoot); + if (!['owner', 'session-manager'].includes(actorId)) throw new TorchError('Only the owner or Fleet Operations may publish the local digest', { code: 'DIGEST_AUTHORITY_REQUIRED' }); + if (actorId === 'owner' && !approved) throw new TorchError('Local digest publication requires approval', { code: 'APPROVAL_REQUIRED' }); + if (actorId === 'session-manager' && config.owner_digest?.enabled !== true) throw new TorchError('Automatic local digest publication is disabled', { code: 'DIGEST_POLICY_DISABLED' }); + const report = this.build(); + const id = this.idFactory(); + this.database.exec('BEGIN IMMEDIATE'); + try { + if (actorId === 'session-manager' && loadProjectConfig(this.repositoryRoot).owner_digest?.enabled !== true) { + throw new TorchError('Digest policy was revoked before publication', { code: 'DIGEST_POLICY_DISABLED' }); + } + this.database.exec('CREATE TABLE IF NOT EXISTS owner_digests (id TEXT PRIMARY KEY, project_id TEXT NOT NULL, generated_at TEXT NOT NULL, report_json TEXT NOT NULL)'); + this.database.prepare('INSERT INTO owner_digests (id, project_id, generated_at, report_json) VALUES (?, ?, ?, ?)') + .run(id, this.controlPlane.projectId, report.generatedAt, JSON.stringify(report)); + this.controlPlane.audit({ actorId: 'session-manager', operation: 'owner-digest.publish', entityType: 'owner-digest', entityId: id, + details: { publishedBy: actorId, generatedAt: report.generatedAt, externalDeliveryPerformed: false } }); + this.database.exec('COMMIT'); + } catch (error) { this.database.exec('ROLLBACK'); throw error; } + return { id, report, mutationPerformed: true, externalDeliveryPerformed: false }; + } + + latest() { + if (!hasTable(this.database, 'owner_digests')) return null; + const row = this.database.prepare('SELECT id, report_json FROM owner_digests WHERE project_id = ? ORDER BY generated_at DESC, rowid DESC LIMIT 1').get(this.controlPlane.projectId); + return row ? { id: row.id, report: JSON.parse(row.report_json) } : null; + } +} diff --git a/src/observability/snapshot.mjs b/src/observability/snapshot.mjs index d7162e3..b8ff51a 100644 --- a/src/observability/snapshot.mjs +++ b/src/observability/snapshot.mjs @@ -7,8 +7,15 @@ 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 { organizationGraphFromConfig } from '../kernel/organization.mjs'; import { inspectManagedWorktree } from '../kernel/worktree-state.mjs'; +import { createRuntimeAdapterRegistry } from '../adapters/registry.mjs'; import { classifyRecoverability } from '../canonical/local.mjs'; +import { observeArtifacts } from '../artifacts/service.mjs'; +import { assessBacklogHealth, backlogObservedCommitDistance } from '../backlog/service.mjs'; +import { observeTaskActivity } from '../backlog/activity.mjs'; +import { ScheduleLauncherService } from '../schedules/launcher.mjs'; +import { hierarchyOrder } from '../runtime/hierarchy-order.mjs'; function json(path) { return JSON.parse(readFileSync(path, 'utf8')); @@ -18,10 +25,109 @@ function hasTable(database, name) { return Boolean(database.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(name)); } +function runtimeAdapterChoices(config, env) { + const registry = createRuntimeAdapterRegistry({ env }); + const builtins = registry.names().map((name) => { + const adapterDefaults = registry.configuration(name) ?? {}; + const settings = config.runtimes?.[name] ?? {}; + const labels = { claude: 'Claude Code', codex: 'Codex', pi: 'Pi' }; + return { + name, title: labels[name] ?? name, + kind: 'built-in', available: true, + model: settings.model ?? adapterDefaults.model ?? null, + reasoning: settings.reasoning ?? adapterDefaults.reasoning ?? null, + }; + }); + const plugins = registry.trustedPlugins.map((plugin) => ({ + name: plugin.name, title: plugin.name, kind: 'local adapter', + available: plugin.status === 'trusted', status: plugin.status, + reason: plugin.status === 'trusted' ? null : `Local adapter is ${plugin.status}. Review it under Runtime settings before selecting it.`, + model: config.runtimes?.[plugin.name]?.model ?? null, + reasoning: config.runtimes?.[plugin.name]?.reasoning ?? null, + })); + return [...builtins, ...plugins]; +} + function rows(database, table, statement) { return hasTable(database, table) ? database.prepare(statement).all() : []; } +function storedJson(value) { + if (!value) return null; + try { return JSON.parse(value); } catch { return { unavailable: true, reason: 'Stored evidence is unreadable' }; } +} + +function capturedInput(value) { + const snapshot = storedJson(value); + if (!snapshot || snapshot.unavailable) return snapshot; + return { commit: snapshot.commit, digest: snapshot.digest, copyStrategy: snapshot.copyStrategy, + provenance: snapshot.provenance, fileCount: Array.isArray(snapshot.files) ? snapshot.files.length : null }; +} + +export function observeDeliveryEvidence(database, projectId) { + if (!hasTable(database, 'delivery_operations') || !hasTable(database, 'deliveries')) { + return { deliveryOperations: [], deliveryAttempts: [], deliveryEvidenceCoverage: 'Delivery operations not initialized; absence is not deployment verification' }; + } + const deliveryOperations = database.prepare(`SELECT o.id, o.delivery_id AS deliveryId, + o.commit_sha AS "commit", o.actor, o.provider, o.operation, o.state, + o.created_at AS createdAt, o.updated_at AS updatedAt + FROM delivery_operations o JOIN deliveries d ON d.id = o.delivery_id + WHERE d.project_id = ? ORDER BY o.rowid DESC LIMIT 40`).all(projectId); + const deliveryAttempts = hasTable(database, 'delivery_attempts') ? database.prepare(`SELECT a.id, + a.operation_id AS operationId, a.ordinal, a.state, a.started_at AS startedAt, + a.finished_at AS finishedAt, a.receipt_json + FROM delivery_attempts a JOIN delivery_operations o ON o.id = a.operation_id + JOIN deliveries d ON d.id = o.delivery_id WHERE d.project_id = ? ORDER BY a.rowid DESC LIMIT 80`) + .all(projectId).map(({ receipt_json, ...attempt }) => { + const receipt = receipt_json?.length <= 65_536 ? storedJson(receipt_json) : null; + return { ...attempt, receiptSummary: { + status: ['succeeded', 'failed', 'unknown'].includes(receipt?.status) ? receipt.status : 'unavailable', + classification: ['transient', 'permanent'].includes(receipt?.failure?.classification) ? receipt.failure.classification : null, + effects: ['not-applied', 'unknown'].includes(receipt?.failure?.effects) ? receipt.failure.effects : null, + reason: ['invalid-adapter-receipt', 'adapter-threw-or-invalid-receipt'].includes(receipt?.reason) ? receipt.reason : null, + hasReference: typeof receipt?.reference === 'string' && Boolean(receipt.reference.trim()), + } }; + }) : []; + return { deliveryOperations, deliveryAttempts, + deliveryEvidenceCoverage: 'Latest 40 operations and 80 attempts; adapter-reported receipts, not independent live verification. Raw references and error text omitted.' }; +} + +// Read-only projection: never follow artifact paths or infer that a recorded +// running check still has a live executor. Older databases need no migration. +export function observeCheckEvidence(database, projectId) { + const preparedColumns = hasTable(database, 'prepared_checks') + ? database.prepare('PRAGMA table_info(prepared_checks)').all().map((column) => column.name) : []; + const prepared = hasTable(database, 'prepared_checks') + ? database.prepare(`SELECT id, check_id AS checkId, area_id AS areaId, state, + created_at AS createdAt, receipt_id AS receiptId, snapshot_json, + ${preparedColumns.includes('recovery_json') ? 'recovery_json' : 'NULL AS recovery_json'} + FROM prepared_checks WHERE project_id = ? ORDER BY created_at DESC, id LIMIT 40`) + .all(projectId).map(({ snapshot_json, recovery_json, ...record }) => { + const recovery = recovery_json?.length <= 65_536 ? storedJson(recovery_json) : null; + const reasons = ['CHECK_SNAPSHOT_INVALID', 'CHECK_SNAPSHOT_CLEANUP_FAILED']; + return { ...record, snapshot: capturedInput(snapshot_json), + recovery: record.state === 'abandoned' ? { + recordedAt: typeof recovery?.at === 'string' && Number.isFinite(Date.parse(recovery.at)) ? recovery.at : null, + cleanupStatus: recovery?.cleanupPending === true ? 'pending' + : recovery?.cleanupPending === false ? 'completed' : 'unconfirmed', + cleanupReason: reasons.includes(recovery?.cleanupReason) ? recovery.cleanupReason : null, + stoppedExecutors: recovery?.executorsStopped === true ? 'owner-attested' : 'unconfirmed', + } : null }; + }) : []; + if (!hasTable(database, 'check_receipts')) return { receipts: [], prepared }; + const columns = database.prepare('PRAGMA table_info(check_receipts)').all().map((column) => column.name); + const receipts = database.prepare(`SELECT id, check_id AS checkId, area_id AS areaId, + commit_sha AS "commit", result, finished_at AS finishedAt, invalid_reason AS invalidReason, + ${columns.includes('conditions_json') ? 'conditions_json' : 'NULL AS conditions_json'} + FROM check_receipts WHERE project_id = ? ORDER BY finished_at DESC, id LIMIT 40`) + .all(projectId).map(({ conditions_json, ...record }) => ({ + ...record, conditions: storedJson(conditions_json), + snapshot: hasTable(database, 'prepared_checks') ? capturedInput(database.prepare(`SELECT snapshot_json + FROM prepared_checks WHERE project_id = ? AND receipt_id = ? LIMIT 1`).get(projectId, record.id)?.snapshot_json) : null, + })); + return { receipts, prepared, limit: 40, coverage: 'Latest 40 receipts and preparations; running records are not process-liveness evidence' }; +} + 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'); @@ -31,8 +137,14 @@ function backlog(repositoryRoot, manifest) { 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, + id: task.id, title: task.title, description: task.description ?? '', + state: task.state, owner: task.owner ?? null, priority: task.priority ?? null, + affectedDomains: task.affectedDomains ?? [], dependencies: task.dependencies ?? [], + feature: task.feature ?? null, milestone: task.milestone ?? null, + acceptanceCriteria: task.acceptanceCriteria ?? [], evidence: task.evidence ?? [], + commit: task.commit ?? null, observedAt: task.observedAt ?? null, + updatedAt: task.updatedAt ?? null, revision: task.revision ?? null, + createdAt: task.createdAt ?? null, history: task.history?.length ? [{ actorId: task.history[0].actorId }] : [], }); } catch { tasks.push({ id: name.slice(0, -5), title: 'Unreadable task', state: 'invalid', owner: null }); @@ -41,6 +153,114 @@ function backlog(repositoryRoot, manifest) { return tasks; } +function backlogHealth(repositoryRoot, tasks, agents, now) { + return assessBacklogHealth({ + tasks, agents, now: now(), staleObservedCommits: 50, + commitDistance: (observedAt) => backlogObservedCommitDistance(repositoryRoot, observedAt), + }); +} + +export function observeBacklogActivity({ repositoryRoot, tasks, config, manifest, now }) { + try { + return { available: true, ...observeTaskActivity({ repositoryRoot, tasks, now, + staleDays: config.backlog?.activity?.stale_days ?? 3, + maxCommits: config.backlog?.activity?.max_commits ?? 1000, + branches: [config.project.main_branch, ...(manifest.external ?? []) + .filter((entry) => entry.type === 'worktree').map((entry) => entry.branch)], + }) }; + } catch (error) { + return { available: false, complete: false, reason: error.code ?? 'BACKLOG_ACTIVITY_UNAVAILABLE', + tasks: [], stale: [], mutationPerformed: false, closurePerformed: false }; + } +} + +function approvalRecord(row) { + return { + id: row.id, requester: row.requester_id, approver: row.approver_id, + task: row.task_ref ?? null, title: row.title, summary: row.summary, + evidence: row.evidence ?? null, status: row.status, revision: row.revision, + createdAt: row.created_at, decidedAt: row.decided_at ?? null, + decidedBy: row.decided_by ?? null, decisionNote: row.decision_note ?? null, + }; +} + +function managerCheckInProjection({ graph, agents, schedules, approvals, database, generatedAt }) { + const rolesById = new Map(graph.roles.map((role) => [role.id, role])); + const agentById = new Map(agents.map((agent) => [agent.areaId, agent])); + const reportsByManager = new Map(); + for (const reportRole of graph.roles) { + for (const parentRoleId of reportRole.reports_to ?? []) { + const managerRole = rolesById.get(parentRoleId); + if (!managerRole || managerRole.kind === 'owner' || managerRole.identity_id === reportRole.identity_id) continue; + const managerId = managerRole.identity_id; + if (!reportsByManager.has(managerId)) reportsByManager.set(managerId, new Map()); + reportsByManager.get(managerId).set(reportRole.identity_id, reportRole); + } + } + + const pending = approvals.filter((approval) => approval.status === 'pending'); + const managers = [...reportsByManager.entries()].map(([managerId, reportRoles]) => { + const reportIds = new Set(reportRoles.keys()); + const approvalWaits = pending.filter((approval) => reportIds.has(approval.requester)).map((approval) => { + const requesterRoles = graph.roles.filter((role) => role.identity_id === approval.requester); + const directManagerIds = new Set(requesterRoles.flatMap((role) => (role.reports_to ?? []) + .map((roleId) => rolesById.get(roleId)?.identity_id).filter(Boolean))); + return { + ...approval, + approverKind: approval.approver === 'owner' ? 'owner' + : directManagerIds.has(approval.approver) ? 'manager' : 'peer-ai', + }; + }); + const approvalsByRequester = new Set(approvalWaits.map((approval) => approval.requester)); + const directReports = [...reportRoles.keys()].map((areaId) => { + const identity = agentById.get(areaId); + return { + areaId, title: identity?.title ?? reportRoles.get(areaId)?.title ?? areaId, + state: identity?.state ?? 'missing', task: identity?.task ?? null, + summary: identity?.summary ?? null, heartbeatAt: identity?.heartbeatAt ?? null, + }; + }); + const findings = []; + for (const report of directReports) { + if (report.state === 'waiting' && !approvalsByRequester.has(report.areaId)) { + findings.push({ type: 'waiting-unclassified', areaId: report.areaId, task: report.task }); + } else if (['offline', 'missing', 'stale'].includes(report.state)) { + findings.push({ type: `direct-report-${report.state}`, areaId: report.areaId, task: report.task }); + } + } + for (const approval of approvalWaits) { + findings.push({ + type: 'approval-wait', areaId: approval.requester, approvalId: approval.id, + approver: approval.approver, approverKind: approval.approverKind, task: approval.task, + }); + } + if (hasTable(database, 'messages')) { + for (const reportId of reportIds) { + const row = hasTable(database, 'message_acks') + ? database.prepare(` + SELECT COUNT(*) AS count FROM messages m + LEFT JOIN message_acks a ON a.message_id = m.id AND a.area_id = ? + WHERE m.recipient_id = ? AND m.sender_id = ? AND a.message_id IS NULL + `).get(managerId, managerId, reportId) + : { count: 0 }; + if (row.count > 0) findings.push({ type: 'unacknowledged-message', areaId: reportId, count: row.count }); + } + } + const schedule = schedules.find((entry) => entry.action?.type === 'manager-check-in' + && entry.action.manager_id === managerId) ?? null; + return { + managerId, title: agentById.get(managerId)?.title + ?? graph.roles.find((role) => role.identity_id === managerId)?.title ?? managerId, + state: agentById.get(managerId)?.state ?? 'missing', generatedAt, + cadence: schedule?.trigger ?? null, scheduleId: schedule?.id ?? null, + timerStatus: schedule ? 'installation-not-verified' : 'not-configured', + directReports, findings, approvalWaits, + attentionRequired: findings.length > 0 || approvalWaits.length > 0, + }; + }); + return managers.sort((a, b) => a.managerId.localeCompare(b.managerId)); +} + export function observeProject({ repositoryRoot, env = process.env, now = () => new Date() } = {}) { const repository = inspectRepository(repositoryRoot); const installed = existsSync(join(repository.root, '.torch', 'torch.yaml')); @@ -51,6 +271,7 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => mode: 'not-installed', mutationPerformed: false, repository: analysis.repository, inventory: analysis.inventory, + organization: { status: 'not-installed', graph: null, domains: [], edges: [], lifecycle: null }, contextLocality: { measured: false, status: 'unavailable', reason: 'Install a Fleet and connect runtime usage reporting before comparing context locality.', @@ -60,6 +281,7 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => const config = loadProjectConfig(repository.root); const roster = json(join(repository.root, '.torch', 'roster.yaml')); + const hierarchy = hierarchyOrder(roster, config); const manifest = readInstallManifest(repository.root); const doctor = diagnoseProject({ repository, env }); const stateRoot = projectStatePath(manifest.projectId, env); @@ -90,22 +312,65 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => }, doctor, organization: { + status: 'active', + graph: organizationGraphFromConfig(config), sessionManager: areas.find((area) => area.id === 'session-manager') ?? null, domains: areas.filter((area) => area.id !== 'session-manager'), edges, + lifecycle: { + startupOrder: hierarchy.startupOrder, + shutdownOrder: hierarchy.shutdownOrder, + startupWaves: hierarchy.startupWaves, + managerIdsByIdentity: Object.fromEntries([...hierarchy.managersByIdentity] + .map(([identityId, managerIds]) => [identityId, [...managerIds]])), + blockers: hierarchy.blockers, + mutationPerformed: false, + }, }, - agents: 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: [] }, + agents: areas.map((area) => { + const configured = area.id === 'session-manager' + ? config.session_manager : config.domains?.find((domain) => domain.id === area.id); + const runtime = configured?.runtime ?? area.runtime + ?? config.runtimes?.default; + const runtimeConfig = config.runtimes?.[runtime] ?? {}; + return { + areaId: area.id, title: area.title, state: 'offline', runtime: runtime ?? null, + model: configured?.model ?? area.model + ?? runtimeConfig.model ?? null, + reasoning: configured?.reasoning ?? area.reasoning + ?? runtimeConfig.reasoning ?? null, + runtimeSessionId: null, summary: null, task: null, heartbeatAt: null, + }; + }), + messages: { total: 0, unacknowledged: 0, recent: [] }, + approvalRequests: { + available: false, pendingCount: 0, items: [], + reason: 'Structured approval state is not available in the local project database yet.', + }, + managerCheckIns: { + available: false, managers: [], + reason: 'Manager check-in state requires an initialized local project database.', + }, + managerWakes: { + available: false, blockingCount: 0, reservations: [], + reason: 'Manager launch records are not initialized in this project database.', + }, backlog: backlog(repository.root, manifest), checks: { definitions: config.checks ?? [], receipts: [] }, resources: { definitions: config.resources ?? [], holders: [], waiters: [] }, integration: [], deliveries: [], + deliveryOperations: [], + deliveryAttempts: [], + ownerDigest: null, fleetChanges: [], + hierarchyProposals: [], + artifacts: { + available: false, items: [], + reason: 'No project-local artifact catalog with task, session, and commit provenance is connected yet.', + }, schedules: config.schedules ?? [], + scheduleLauncher: new ScheduleLauncherService({ repositoryRoot: repository.root, env }).status(), worktrees: (manifest.external ?? []) .filter((entry) => entry.type === 'worktree') .map((entry) => inspectManagedWorktree(repository.root, entry, config.project.main_branch)), @@ -129,6 +394,7 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => }, delivery: config.delivery.adapters, }, + runtimeAdapters: runtimeAdapterChoices(config, env), audit: [], contextLocality: { measured: false, status: 'unavailable', @@ -136,6 +402,9 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => metrics: ['cachedInput', 'uncachedInput', 'cacheCreation', 'cacheRead', 'compactions', 'resumedPromptBytes', 'costPerVerifiedItem'], }, }; + result.backlogHealth = backlogHealth(repository.root, result.backlog, result.agents, now); + result.backlogActivity = observeBacklogActivity({ repositoryRoot: repository.root, + tasks: result.backlog, config, manifest, now: now() }); if (!existsSync(databasePath)) return result; const database = new DatabaseSync(databasePath, { readOnly: true }); @@ -154,6 +423,27 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => } : agent; }); } + if (hasTable(database, 'schedule_wake_reservations')) { + const rows = database.prepare(` + SELECT * FROM schedule_wake_reservations WHERE project_id = ? + ORDER BY (outcome = 'reserved') DESC, reserved_at DESC, id DESC LIMIT 100 + `).all(config.project.id); + result.managerWakes = { + available: true, + blockingCount: database.prepare(`SELECT COUNT(*) AS count FROM schedule_wake_reservations + WHERE project_id = ? AND outcome = 'reserved'`).get(config.project.id).count, + reservations: rows.map((row) => ({ + id: row.id, managerId: row.manager_id, scheduleId: row.schedule_id, + messageId: row.message_id, budgetDay: row.budget_day, + reservedAt: row.reserved_at, outcome: row.outcome, + blocksManagerWake: row.outcome === 'reserved', + managerState: result.agents.find((agent) => agent.areaId === row.manager_id)?.state ?? null, + runtimeStoppedVerified: false, + })), + limit: 100, + reason: null, + }; + } if (hasTable(database, 'messages')) { result.messages.total = database.prepare('SELECT COUNT(*) AS count FROM messages').get().count; result.messages.recent = database.prepare(` @@ -175,10 +465,25 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => `).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 - `); + if (hasTable(database, 'approval_requests')) { + result.approvalRequests = { + available: true, + items: database.prepare(` + SELECT * FROM approval_requests WHERE project_id = ? + ORDER BY CASE status WHEN 'pending' THEN 0 ELSE 1 END, created_at DESC, id DESC LIMIT 200 + `).all(manifest.projectId).map(approvalRecord), + pendingCount: database.prepare(` + SELECT COUNT(*) AS count FROM approval_requests WHERE project_id = ? AND status = 'pending' + `).get(manifest.projectId).count, + reason: null, + }; + } + Object.assign(result.checks, observeCheckEvidence(database, manifest.projectId)); + result.canonicalFetches = (hasTable(database, 'canonical_fetches') ? database.prepare(`SELECT id, remote, branch, + commit_sha AS "commit", state, updated_at AS updatedAt, attempt_limit AS attemptLimit + FROM canonical_fetches WHERE project_id = ? ORDER BY rowid DESC LIMIT 20`).all(manifest.projectId) : []) + .map((record) => ({ ...record, attempts: database.prepare(`SELECT ordinal, state, classification, + finished_at AS finishedAt FROM canonical_fetch_attempts WHERE fetch_id = ? ORDER BY ordinal`).all(record.id) })); 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 @@ -188,21 +493,50 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => 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 + SELECT id, source_area AS sourceArea, source_commit AS sourceCommit, target_branch AS targetBranch, + authorized_by AS authorizedBy, created_at AS createdAt, state, reason, updated_at AS updatedAt, + row_number() OVER (ORDER BY rowid) AS queueOrder + FROM integration_requests WHERE state NOT IN ('landed', 'superseded') ORDER BY rowid LIMIT 40 `); result.deliveries = rows(database, 'deliveries', ` SELECT id, label, source_area AS sourceArea, commit_sha AS "commit", state, updated_at AS updatedAt FROM deliveries ORDER BY updated_at DESC, id LIMIT 40 `); + Object.assign(result, observeDeliveryEvidence(database, manifest.projectId)); + if (hasTable(database, 'owner_digests')) { + const digest = database.prepare('SELECT id, report_json FROM owner_digests WHERE project_id = ? ORDER BY generated_at DESC, rowid DESC LIMIT 1') + .get(manifest.projectId); + result.ownerDigest = digest ? { id: digest.id, report: JSON.parse(digest.report_json) } : null; + } result.fleetChanges = rows(database, 'fleet_changes', ` SELECT id, change_type AS changeType, state, proposer, COALESCE(json_extract(domain_json, '$.id'), json_extract(proposal_json, '$.resultDomains[0].id')) AS domainId, COALESCE(json_extract(domain_json, '$.title'), json_extract(proposal_json, '$.resultDomains[0].title')) AS title, json_extract(expected_benefit_json, '$.summary') AS expectedBenefit, - approved_by AS approvedBy, updated_at AS updatedAt + approved_by AS approvedBy, updated_at AS updatedAt, base_commit AS baseCommit, + rationale, evidence_json AS evidenceJson, domain_json AS domainJson FROM fleet_changes ORDER BY updated_at DESC, id LIMIT 40 - `); + `).map((change) => ({ + ...change, + evidence: JSON.parse(change.evidenceJson ?? '[]'), + domain: JSON.parse(change.domainJson ?? 'null'), + evidenceJson: undefined, + domainJson: undefined, + })); + if (hasTable(database, 'hierarchy_proposals')) { + result.hierarchyProposals = database.prepare(` + SELECT id, state, proposer, created_at AS createdAt, updated_at AS updatedAt, + decided_by AS decidedBy, decided_at AS decidedAt, decision_reason AS decisionReason, proposal_json AS proposalJson + FROM hierarchy_proposals ORDER BY updated_at DESC, id LIMIT 40 + `).all().map((proposal) => ({ + ...proposal, proposal: JSON.parse(proposal.proposalJson), proposalJson: undefined, + pilotReviews: hasTable(database, 'hierarchy_pilot_reviews') + ? database.prepare(`SELECT report_json FROM hierarchy_pilot_reviews WHERE proposal_id = ? + ORDER BY created_at DESC, id DESC LIMIT 5`).all(proposal.id) + .map((review) => JSON.parse(review.report_json)) : [], + })); + } + result.artifacts = observeArtifacts({ database, stateRoot }); 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 @@ -222,8 +556,21 @@ export function observeProject({ repositoryRoot, env = process.env, now = () => ? 'Usage without verified outcomes cannot establish efficiency.' : null, }; } + if (hasTable(database, 'identities')) { + const approvals = result.approvalRequests.items; + result.managerCheckIns = { + available: true, + managers: managerCheckInProjection({ + graph: result.organization.graph, agents: result.agents, schedules: result.schedules, + approvals, database, generatedAt: result.generatedAt, + }), + reason: result.approvalRequests.available ? null + : 'Presence and message state are available; structured approval records are not initialized.', + }; + } } finally { database.close(); } + result.backlogHealth = backlogHealth(repository.root, result.backlog, result.agents, now); return result; } diff --git a/src/runtime/hierarchy-order.mjs b/src/runtime/hierarchy-order.mjs new file mode 100644 index 0000000..92076ce --- /dev/null +++ b/src/runtime/hierarchy-order.mjs @@ -0,0 +1,64 @@ +import { organizationGraphFromConfig } from '../kernel/organization.mjs'; + +export function hierarchyOrder(roster, config) { + const rosterIds = new Set(roster.areas.map((area) => area.id)); + const graph = organizationGraphFromConfig(config); + const rolesById = new Map(graph.roles.map((role) => [role.id, role])); + const rolesByIdentity = new Map(); + for (const role of graph.roles) { + if (!rolesByIdentity.has(role.identity_id)) rolesByIdentity.set(role.identity_id, []); + rolesByIdentity.get(role.identity_id).push(role); + } + const managersByIdentity = new Map(roster.areas.map((area) => [area.id, new Set()])); + const dependencies = new Map(roster.areas.map((area) => [area.id, new Set()])); + const blockers = []; + + for (const area of roster.areas) { + if (!rolesByIdentity.has(area.id)) blockers.push({ + code: 'FLEET_STARTUP_ROLE_MISSING', areaId: area.id, + }); + } + + for (const role of graph.roles) { + if (!rosterIds.has(role.identity_id)) continue; + for (const parentRoleId of role.reports_to) { + const parent = rolesById.get(parentRoleId); + if (!parent || parent.identity_id === role.identity_id || parent.identity_id === 'owner') continue; + if (!rosterIds.has(parent.identity_id)) { + blockers.push({ + code: 'FLEET_STARTUP_MANAGER_MISSING', areaId: role.identity_id, + managerId: parent.identity_id, + }); + continue; + } + managersByIdentity.get(role.identity_id).add(parent.identity_id); + dependencies.get(role.identity_id).add(parent.identity_id); + } + } + + const remaining = new Set(roster.areas.map((area) => area.id)); + const startupWaves = []; + while (remaining.size) { + const ready = roster.areas.filter((area) => remaining.has(area.id) + && [...dependencies.get(area.id)].every((managerId) => !remaining.has(managerId))); + if (!ready.length) { + blockers.push({ + code: 'FLEET_STARTUP_IDENTITY_CYCLE', + areaIds: roster.areas.filter((area) => remaining.has(area.id)).map((area) => area.id), + }); + startupWaves.push(roster.areas.filter((area) => remaining.has(area.id))); + break; + } + startupWaves.push(ready); + for (const area of ready) remaining.delete(area.id); + } + const areas = startupWaves.flat(); + return { + areas, + startupWaves: startupWaves.map((wave) => wave.map((area) => area.id)), + startupOrder: areas.map((area) => area.id), + shutdownOrder: [...areas].reverse().map((area) => area.id), + managersByIdentity, + blockers, + }; +} diff --git a/src/runtime/lifecycle.mjs b/src/runtime/lifecycle.mjs index 67f2c83..1047820 100644 --- a/src/runtime/lifecycle.mjs +++ b/src/runtime/lifecycle.mjs @@ -5,7 +5,12 @@ import { fileURLToPath } from 'node:url'; import { TorchError } from '../kernel/errors.mjs'; import { readInstallManifest } from '../kernel/install.mjs'; import { loadFleetDefinition } from '../kernel/worktrees.mjs'; -import { validateRuntimeAdapter } from '../adapters/runtime.mjs'; +import { organizationGraphFromConfig } from '../kernel/organization.mjs'; +import { sha256 } from '../kernel/files.mjs'; +import { + costCeilingPlanIssues, profileCapabilityIssues, resolveLaunchPolicy, validateRuntimeAdapter, +} from '../adapters/runtime.mjs'; +import { hierarchyOrder } from './hierarchy-order.mjs'; const MCP_ENTRY = fileURLToPath(new URL('../mcp/server.mjs', import.meta.url)); @@ -21,13 +26,7 @@ function worktreesByArea(manifest) { .map((entry) => [entry.area, entry])); } -function orderedAreas(roster) { - return [...roster.areas.filter((area) => area.id !== 'session-manager'), - ...roster.areas.filter((area) => area.id === 'session-manager')]; -} - -function selectedAreas(roster, only, blockers) { - const ordered = orderedAreas(roster); +function selectedAreas(roster, only, blockers, ordered = roster.areas) { if (only === undefined) return ordered; const selected = Array.isArray(only) ? [...new Set(only.map((id) => typeof id === 'string' ? id.trim() : '').filter(Boolean))] @@ -53,29 +52,118 @@ function adapterMap({ adapter, adapters } = {}) { function configuredRuntime(area, config) { if (area.id === 'session-manager') { - return area.runtime ?? config.session_manager?.runtime ?? config.runtimes?.default; + return config.session_manager?.runtime ?? area.runtime ?? config.runtimes?.default; } - return area.runtime ?? config.domains?.find((domain) => domain.id === area.id)?.runtime + return config.domains?.find((domain) => domain.id === area.id)?.runtime ?? area.runtime ?? config.runtimes?.default; } -function promptPaths(stateRoot, worktree, areaId) { +function configuredProfile(area, config, runtime) { + const runtimeConfig = config.runtimes?.[runtime] ?? {}; + const configured = area.id === 'session-manager' + ? config.session_manager : config.domains?.find((domain) => domain.id === area.id); + const launchPolicy = resolveLaunchPolicy({ + runtime, adapterDefaults: {}, runtimeConfig, identity: configured, + }); return { - common: join(worktree.path, '.torch', 'prompts', 'COMMON.md'), - area: join(worktree.path, '.torch', 'prompts', `${areaId}.md`), - combined: join(stateRoot, 'sessions', 'prompts', `${areaId}.md`), + model: configured?.model ?? area.model + ?? runtimeConfig.model ?? null, + reasoning: configured?.reasoning ?? area.reasoning + ?? runtimeConfig.reasoning ?? null, + launchPolicy, }; } -function startupMessage(areaId, fresh) { +function organizationContext(config, areaId) { + const graph = organizationGraphFromConfig(config); + const rolesById = new Map(graph.roles.map((role) => [role.id, role])); + const ownRoles = graph.roles.filter((role) => role.identity_id === areaId); + const ownRoleIds = new Set(ownRoles.map((role) => role.id)); + const summarizeRole = (role) => ({ + roleId: role.id, title: role.title, kind: role.kind, + responsibilities: role.responsibilities, authority: role.authority, + reportsTo: role.reports_to.map((roleId) => rolesById.get(roleId)?.title ?? roleId), + }); + const directReports = new Map(); + for (const role of graph.roles) { + if (role.identity_id === areaId || !role.reports_to.some((parent) => ownRoleIds.has(parent))) continue; + directReports.set(role.identity_id, summarizeRole(role)); + } + const related = (field) => [...new Map(ownRoles.flatMap((role) => role[field] + .map((roleId) => rolesById.get(roleId)).filter(Boolean) + .filter((role) => role.identity_id !== areaId) + .map((role) => [role.identity_id, { identityId: role.identity_id, title: role.title }]))).values()] + .sort((left, right) => left.identityId.localeCompare(right.identityId)); + return { + schema: graph.schema, revision: graph.revision, identityId: areaId, + roles: ownRoles.map(summarizeRole), + directReports: [...directReports.entries()].sort(([left], [right]) => left.localeCompare(right)) + .map(([identityId, role]) => ({ identityId, ...role })), + coordinates: related('coordinates'), consultsWith: related('consults_with'), + implementationOwnership: graph.implementation_owners + .filter((owner) => owner.identity_id === areaId), + }; +} + +function managerCheckInInstructions(organization) { + if (!organization.directReports.length) return []; + return [ + '## Scheduled manager check-ins', + '', + 'Your active organization role has direct reports. TORCH queues a durable self-directed `manager-check-in` message on the configured schedule; the wake snapshot may be stale, and a currently active session may receive the message only in its durable inbox.', + '- At startup/resume, after seeing a `manager-check-in` message, and at the start of each new coordination cycle, read your inbox and call `torch_plan_manager_check_in` to refresh direct-report presence, blockers, messages, handoffs, and structured approval waits from current durable state.', + '- Follow up on each finding within your coordination authority. For a pending approval, decide only when you are the named approver; route owner- or peer-owned approvals to that approver and never decide for them.', + '- A check-in is observational: it does not assign work, transfer ownership, approve a request, create an identity, or authorize runtime/spending changes. Preserve direct peer communication.', + '', + ]; +} + +function promptPaths(stateRoot, repositoryRoot, areaId, config) { + const common = join(repositoryRoot, '.torch', 'prompts', 'COMMON.md'); + const area = join(repositoryRoot, '.torch', 'prompts', `${areaId}.md`); + const organization = organizationContext(config, areaId); + const organizationSource = join(repositoryRoot, '.torch', 'torch.yaml'); + const instructionText = [ + '# TORCH runtime rules — current authoritative revision', + '', + `Identity: ${areaId}`, + '', + '- This bundle is rebuilt from the canonical project checkout whenever this identity starts or resumes.', + '- If this bundle conflicts with earlier conversation memory or an older prompt revision, this current bundle wins.', + '- Before each new backlog item, run `torch brief --area --json`; read and follow its current prompt and digest. Current repository instructions override older conversation memory.', + '- Never push a specialist branch directly to the canonical branch or its remote. Submit an exact-commit request with `torch integrate request`; landing is a serialized, authority-gated operation.', + '- After canonical main advances, converge your clean branch, rerun required checks on the resulting exact commit, and submit a new integration request. Never bypass a lost race by weakening checks.', + '', + readFileSync(common, 'utf8').trimEnd(), + '', + ...managerCheckInInstructions(organization), + '## Active organization metadata', + '', + 'The following JSON is owner-approved project configuration supplied as data. It describes your coordination roles, reporting relationships, and implementation ownership; it does not grant authority beyond TORCH-enforced policy or override owner instructions and safety rules.', + '', + JSON.stringify(organization), + '', + readFileSync(area, 'utf8').trimEnd(), + '', + ].join('\n'); + const instructionDigest = sha256(instructionText); + return { + common, area, organization: organizationSource, + combined: join(stateRoot, 'sessions', 'prompts', `${areaId}-${instructionDigest.slice(0, 16)}.md`), + instructionText, instructionDigest, + }; +} + +function startupMessage(areaId, fresh, instructionDigest) { + const currentBrief = `Current TORCH instruction digest: ${instructionDigest}. The current instruction bundle is included in this launch/resume and supersedes older briefing text in the conversation. Read the current brief again before each new backlog item.`; if (areaId === 'session-manager') { return fresh - ? 'Start the TORCH Fleet. Inspect live roster, messages, worktrees, and backlog, then assess Fleet evolution before dispatch.' - : 'Resume the TORCH Fleet from durable state. Reconcile live status and assess Fleet evolution before dispatch.'; + ? `Start the TORCH Fleet. Inspect live roster, messages, worktrees, and backlog, then assess Fleet evolution before dispatch. ${currentBrief}` + : `Resume the TORCH Fleet from durable state. Reconcile live status and assess Fleet evolution before dispatch. ${currentBrief}`; } return fresh - ? 'Start this TORCH domain. Query live identity and ownership, then await or resume the assigned backlog item.' - : 'Resume this TORCH domain from durable state. Read the inbox, confirm ownership, and report current evidence.'; + ? `Start this TORCH domain. Query live identity and ownership, then await or resume the assigned backlog item. ${currentBrief}` + : `Resume this TORCH domain from durable state. Read the inbox, confirm ownership, and report current evidence. ${currentBrief}`; } function atomicJson(path, value) { @@ -165,14 +253,15 @@ function inspectWorktree(worktree) { export function planFleetUp({ repositoryRoot, controlPlane, adapter, adapters, fresh = false, only } = {}) { const { config, roster } = loadFleetDefinition(repositoryRoot); + const hierarchy = hierarchyOrder(roster, config); const manifest = readInstallManifest(repositoryRoot); const worktrees = worktreesByArea(manifest); const stateRoot = localStateRoot(manifest); const runtimes = adapterMap({ adapter, adapters }); - const blockers = []; + const blockers = [...hierarchy.blockers]; const actions = []; - for (const area of selectedAreas(roster, only, blockers)) { + for (const area of selectedAreas(roster, only, blockers, hierarchy.areas)) { const worktree = worktrees.get(area.id); if (!worktree) { blockers.push({ areaId: area.id, code: 'WORKTREE_MISSING' }); @@ -194,51 +283,75 @@ export function planFleetUp({ repositoryRoot, controlPlane, adapter, adapters, f const identity = controlPlane.identity(area.id); const runtimeIntegration = runtimeAdapter.configure({ repositoryRoot, areaId: area.id, mcpEntry: MCP_ENTRY, + projectId: manifest.projectId, stateRoot, }); - if (runtimeIntegration?.mutationPerformed !== false || !runtimeIntegration?.mcp) { + if (runtimeIntegration?.mutationPerformed !== false + || !(runtimeIntegration?.mcp || runtimeIntegration?.integration)) { blockers.push({ areaId: area.id, code: 'RUNTIME_CONFIGURATION_INVALID', runtime, }); continue; } - const prompts = promptPaths(stateRoot, worktree, area.id); + const prompts = promptPaths(stateRoot, repositoryRoot, area.id, config); + const profile = configuredProfile(area, config, runtime); + const unsupported = profileCapabilityIssues(runtimeAdapter, profile); + if (unsupported.length) { + blockers.push({ + areaId: area.id, code: 'RUNTIME_PROFILE_UNSUPPORTED', runtime, + adapter: runtimeAdapter.name, profile, capabilities: unsupported, + }); + continue; + } const runtimeConfig = config.runtimes?.[runtime] ?? {}; - const model = area.model ?? runtimeConfig.model; + const { model, reasoning, launchPolicy } = profile; const background = runtimeConfig.background ?? true; const shouldResume = !fresh && identity.runtime === runtime && Boolean(identity.runtimeSessionId); const runtimePlan = shouldResume ? runtimeAdapter.resumeSession({ areaId: area.id, runtimeSessionId: identity.runtimeSessionId, worktree: worktree.path, - model, background, message: startupMessage(area.id, false), mcp: runtimeIntegration.mcp, + model, reasoning, launchPolicy, background, + message: startupMessage(area.id, false, prompts.instructionDigest), + promptFile: prompts.combined, instructionText: prompts.instructionText, + instructionDigest: prompts.instructionDigest, mcp: runtimeIntegration.mcp, + integration: runtimeIntegration.integration, }) : runtimeAdapter.createSession({ areaId: area.id, title: area.title, worktree: worktree.path, promptFile: prompts.combined, - firstMessage: startupMessage(area.id, true), model, background, mcp: runtimeIntegration.mcp, + firstMessage: startupMessage(area.id, true, prompts.instructionDigest), + model, reasoning, launchPolicy, background, mcp: runtimeIntegration.mcp, + integration: runtimeIntegration.integration, }); actions.push({ areaId: area.id, title: area.title, mode: shouldResume ? 'resume' : 'create', + managerIds: [...(hierarchy.managersByIdentity.get(area.id) ?? [])], runtime: runtimeAdapter.name, runtimeSessionId: runtimePlan.runtimeSessionId, + profile, requiresRuntimeIdCapture: runtimePlan.requiresRuntimeIdCapture ?? false, completionState: runtimePlan.completionState ?? 'starting', worktree: worktree.path, branch: worktree.branch, promptFile: prompts.combined, - promptSources: [prompts.common, prompts.area], mcp: runtimeIntegration.mcp, + promptSources: [prompts.common, prompts.area, prompts.organization], instructionText: prompts.instructionText, + instructionDigest: prompts.instructionDigest, mcp: runtimeIntegration.mcp ?? null, + integration: runtimeIntegration.integration ?? null, launch: runtimePlan.launch, }); } return { action: 'fleet-up', projectId: manifest.projectId, stateRoot, fresh, - only: only ?? null, actions, blockers, + only: only ?? null, startupOrder: actions.map((action) => action.areaId), actions, blockers, canProceed: blockers.length === 0, mutationPerformed: false, }; } -export function planAreaUp({ repositoryRoot, controlPlane, areaId, adapter, adapters, fresh = false } = {}) { +export function planAreaUp({ repositoryRoot, controlPlane, areaId, adapter, adapters, fresh = false, maxCostUsd } = {}) { const { config, roster } = loadFleetDefinition(repositoryRoot); const manifest = readInstallManifest(repositoryRoot); const worktree = worktreesByArea(manifest).get(areaId); const area = roster.areas.find((candidate) => candidate.id === areaId); const blockers = []; + if (maxCostUsd !== undefined && (!Number.isFinite(maxCostUsd) || maxCostUsd <= 0)) { + blockers.push({ areaId, code: 'RUNTIME_COST_CEILING_INVALID', maxUsd: maxCostUsd }); + } if (!area) blockers.push({ areaId, code: 'FLEET_IDENTITY_MISSING' }); if (!worktree) blockers.push({ areaId, code: 'WORKTREE_MISSING' }); if (blockers.length) { @@ -264,35 +377,81 @@ export function planAreaUp({ repositoryRoot, controlPlane, areaId, adapter, adap const identity = controlPlane.identity(area.id); const runtimeIntegration = runtimeAdapter.configure({ repositoryRoot, areaId: area.id, mcpEntry: MCP_ENTRY, + projectId: manifest.projectId, stateRoot: localStateRoot(manifest), }); - if (runtimeIntegration?.mutationPerformed !== false || !runtimeIntegration?.mcp) { + if (runtimeIntegration?.mutationPerformed !== false + || !(runtimeIntegration?.mcp || runtimeIntegration?.integration)) { return { action: 'area-up', projectId: manifest.projectId, areaId, fresh, actions: [], blockers: [{ areaId, code: 'RUNTIME_CONFIGURATION_INVALID', runtime }], canProceed: false, mutationPerformed: false, }; } - const prompts = promptPaths(localStateRoot(manifest), worktree, area.id); + const prompts = promptPaths(localStateRoot(manifest), repositoryRoot, area.id, config); + const profile = configuredProfile(area, config, runtime); + const unsupported = profileCapabilityIssues(runtimeAdapter, profile); + if (unsupported.length) { + return { + action: 'area-up', projectId: manifest.projectId, areaId, fresh, + actions: [], blockers: [{ + areaId: area.id, code: 'RUNTIME_PROFILE_UNSUPPORTED', runtime, + adapter: runtimeAdapter.name, profile, capabilities: unsupported, + }], canProceed: false, mutationPerformed: false, + }; + } const runtimeConfig = config.runtimes?.[runtime] ?? {}; - const model = area.model ?? runtimeConfig.model; + const { model, reasoning, launchPolicy } = profile; const background = runtimeConfig.background ?? true; const shouldResume = !fresh && identity.runtime === runtime && Boolean(identity.runtimeSessionId); + const operation = shouldResume ? 'resumeSession' : 'createSession'; + const declaredCostModes = runtimeAdapter.capabilities.perInvocationCostCeiling[operation]; + if (maxCostUsd !== undefined && declaredCostModes.length === 0) { + return { + action: 'area-up', projectId: manifest.projectId, areaId, fresh, + actions: [], blockers: [{ + areaId, code: 'RUNTIME_COST_CEILING_UNSUPPORTED', runtime: runtimeAdapter.name, + operation, maxUsd: maxCostUsd, background, + }], canProceed: false, mutationPerformed: false, + }; + } const runtimePlan = shouldResume ? runtimeAdapter.resumeSession({ areaId: area.id, runtimeSessionId: identity.runtimeSessionId, worktree: worktree.path, - model, background, message: startupMessage(area.id, false), mcp: runtimeIntegration.mcp, - }) + model, reasoning, launchPolicy, background, + message: startupMessage(area.id, false, prompts.instructionDigest), + promptFile: prompts.combined, instructionText: prompts.instructionText, + instructionDigest: prompts.instructionDigest, mcp: runtimeIntegration.mcp, + integration: runtimeIntegration.integration, + ...(maxCostUsd === undefined ? {} : { maxCostUsd }), + }) : runtimeAdapter.createSession({ areaId: area.id, title: area.title, worktree: worktree.path, promptFile: prompts.combined, - firstMessage: startupMessage(area.id, true), model, background, mcp: runtimeIntegration.mcp, - }); + firstMessage: startupMessage(area.id, true, prompts.instructionDigest), + model, reasoning, launchPolicy, background, mcp: runtimeIntegration.mcp, + integration: runtimeIntegration.integration, + ...(maxCostUsd === undefined ? {} : { maxCostUsd }), + }); + if (maxCostUsd !== undefined) { + const ceilingIssues = costCeilingPlanIssues(runtimeAdapter, operation, maxCostUsd, runtimePlan); + if (ceilingIssues.length) { + return { + action: 'area-up', projectId: manifest.projectId, areaId, fresh, + actions: [], blockers: ceilingIssues.map((issue) => ({ areaId, ...issue, background })), + canProceed: false, mutationPerformed: false, + }; + } + } const action = { areaId: area.id, title: area.title, mode: shouldResume ? 'resume' : 'create', runtime: runtimeAdapter.name, runtimeSessionId: runtimePlan.runtimeSessionId, + profile, + ...(maxCostUsd === undefined ? {} : { costCeiling: runtimePlan.costCeiling }), requiresRuntimeIdCapture: runtimePlan.requiresRuntimeIdCapture ?? false, completionState: runtimePlan.completionState ?? 'starting', worktree: worktree.path, branch: worktree.branch, promptFile: prompts.combined, - promptSources: [prompts.common, prompts.area], mcp: runtimeIntegration.mcp, + promptSources: [prompts.common, prompts.area, prompts.organization], instructionText: prompts.instructionText, + instructionDigest: prompts.instructionDigest, mcp: runtimeIntegration.mcp ?? null, + integration: runtimeIntegration.integration ?? null, launch: runtimePlan.launch, }; return { @@ -302,9 +461,8 @@ export function planAreaUp({ repositoryRoot, controlPlane, areaId, adapter, adap } function writeCombinedPrompt(action) { - const content = action.promptSources.map((path) => readFileSync(path, 'utf8').trimEnd()).join('\n\n'); mkdirSync(dirname(action.promptFile), { recursive: true }); - writeFileSync(action.promptFile, `${content}\n`, { encoding: 'utf8', mode: 0o600 }); + writeFileSync(action.promptFile, action.instructionText, { encoding: 'utf8', mode: 0o600 }); } export function startFleet({ plan, controlPlane, executor, adapters } = {}) { @@ -322,7 +480,7 @@ export function startFleet({ plan, controlPlane, executor, adapters } = {}) { const runtimes = adapterMap({ adapters }); for (const action of plan.actions) { controlPlane.assertIdentity(action.areaId); - if (action.mode === 'create') writeCombinedPrompt(action); + writeCombinedPrompt(action); const result = executor({ ...action.launch, areaId: action.areaId, runtimeSessionId: action.runtimeSessionId }); if (result?.status !== undefined && result.status !== 0) { controlPlane.reportStatus({ @@ -378,7 +536,8 @@ export function startFleet({ plan, controlPlane, executor, adapters } = {}) { } export function planFleetDown({ repositoryRoot, controlPlane, adapters } = {}) { - const { roster } = loadFleetDefinition(repositoryRoot); + const { config, roster } = loadFleetDefinition(repositoryRoot); + const hierarchy = hierarchyOrder(roster, config); const manifest = readInstallManifest(repositoryRoot); const worktrees = worktreesByArea(manifest); const runtimes = adapterMap({ adapters }); @@ -387,11 +546,12 @@ export function planFleetDown({ repositoryRoot, controlPlane, adapters } = {}) { const hasTable = (name) => Boolean(database.prepare( "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", ).get(name)); - const actions = orderedAreas(roster).map((area) => { + const actions = [...hierarchy.areas].reverse().map((area) => { const identity = controlPlane.identity(area.id); const worktree = worktrees.get(area.id); return { areaId: area.id, state: identity.state, runtime: identity.runtime, + managerIds: [...(hierarchy.managersByIdentity.get(area.id) ?? [])], runtimeSessionId: identity.runtimeSessionId, currentTask: identity.currentTask, requiresRuntimeStop: identity.runtimeSessionId ? runtimes.get(identity.runtime)?.capabilities.stopSession !== false @@ -402,9 +562,9 @@ export function planFleetDown({ repositoryRoot, controlPlane, adapters } = {}) { worktree: worktree ? inspectWorktree(worktree) : null, }; }); - const blockers = actions + const blockers = [...hierarchy.blockers, ...actions .filter((action) => ['starting', 'working', 'stopping'].includes(action.state)) - .map((action) => ({ areaId: action.areaId, state: action.state, code: 'ACTIVE_SESSION' })); + .map((action) => ({ areaId: action.areaId, state: action.state, code: 'ACTIVE_SESSION' }))]; blockers.push(...actions .filter((action) => action.runtimeAdapterMissing) .map((action) => ({ areaId: action.areaId, runtime: action.runtime, code: 'RUNTIME_ADAPTER_MISSING' }))); @@ -435,6 +595,7 @@ export function planFleetDown({ repositoryRoot, controlPlane, adapters } = {}) { return { action: 'fleet-down', projectId: manifest.projectId, stateRoot: localStateRoot(manifest), resumeBriefPath: managerWorktree ? join(managerWorktree.path, '.torch', 'RESUME-BRIEF.md') : null, + shutdownOrder: actions.map((action) => action.areaId), actions, blockers, canProceed: blockers.length === 0, mutationPerformed: false, }; } @@ -452,14 +613,16 @@ export function stopFleet({ plan, controlPlane, stopRuntime, now = () => new Dat } const stopped = []; for (const action of plan.actions) { - if (action.areaId !== 'session-manager' && action.state !== 'offline') { - controlPlane.sendMessage({ - sender: action.areaId, recipient: 'session-manager', - body: `Final status: ${action.state}. ${action.worktree - ? `Branch ${action.worktree.branch} at ${action.worktree.commit}; ${action.worktree.dirtyEntries.length} dirty entries.` - : 'No managed worktree.'}`, - references: { commit: action.worktree?.commit }, - }); + if (action.state !== 'offline') { + for (const managerId of action.managerIds ?? []) { + controlPlane.sendMessage({ + sender: action.areaId, recipient: managerId, + body: `Final status: ${action.state}. ${action.worktree + ? `Branch ${action.worktree.branch} at ${action.worktree.commit}; ${action.worktree.dirtyEntries.length} dirty entries.` + : 'No managed worktree.'}`, + references: { commit: action.worktree?.commit }, + }); + } } if (!action.runtimeSessionId || action.state === 'offline' || action.requiresRuntimeStop === false) { if (action.state !== 'offline') { @@ -509,23 +672,23 @@ export function captureFleet({ repositoryRoot, controlPlane, now = () => new Dat } export function createFleetBrief({ repositoryRoot, controlPlane, areaId } = {}) { - const { roster } = loadFleetDefinition(repositoryRoot); + const { config, roster } = loadFleetDefinition(repositoryRoot); + const manifest = readInstallManifest(repositoryRoot); const selected = areaId ? roster.areas.filter((area) => area.id === areaId) - : orderedAreas(roster); + : hierarchyOrder(roster, config).areas; if (!selected.length) { throw new TorchError(`Unknown Fleet identity: ${areaId}`, { code: 'UNKNOWN_FLEET_IDENTITY' }); } - const commonPath = join(repositoryRoot, '.torch', 'prompts', 'COMMON.md'); - const common = readFileSync(commonPath, 'utf8').trimEnd(); return { schema: 'torch.dev/fleet-brief/v1alpha1', mutationPerformed: false, areas: selected.map((area) => { - const promptPath = join(repositoryRoot, '.torch', 'prompts', `${area.id}.md`); + const prompts = promptPaths(localStateRoot(manifest), repositoryRoot, area.id, config); return { areaId: area.id, title: area.title, identity: controlPlane.identity(area.id), - promptSources: [commonPath, promptPath], - prompt: `${common}\n\n${readFileSync(promptPath, 'utf8').trimEnd()}\n`, + promptSources: [prompts.common, prompts.area, prompts.organization], + instructionDigest: prompts.instructionDigest, + prompt: prompts.instructionText, }; }), }; diff --git a/src/schedules/launcher.mjs b/src/schedules/launcher.mjs index 047f3ce..d3a3222 100644 --- a/src/schedules/launcher.mjs +++ b/src/schedules/launcher.mjs @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process'; -import { createHash } from 'node:crypto'; +import { createHash, randomUUID } from 'node:crypto'; import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join, resolve } from 'node:path'; @@ -97,8 +97,13 @@ export class ScheduleLauncherService { }; } - install() { + install({ expectedDigest } = {}) { const plan = this.plan(); + if (expectedDigest && plan.digest !== expectedDigest) { + throw new TorchError('System schedule configuration changed after review', { + code: 'SCHEDULE_LAUNCHER_PLAN_STALE', details: { expected: expectedDigest, actual: plan.digest }, + }); + } if (!plan.canProceed) throw new TorchError('System schedule launcher installation is blocked', { code: 'SCHEDULE_LAUNCHER_BLOCKED', details: plan.blockers, }); @@ -129,6 +134,99 @@ export class ScheduleLauncherService { } } + planReconcile() { + const config = loadProjectConfig(this.repositoryRoot); + const manifest = readInstallManifest(this.repositoryRoot); + const installed = (manifest.external ?? []).filter((entry) => entry.type === 'systemd-user-unit'); + const digest = scheduleConfigDigest(this.repositoryRoot); + const rendered = render({ + repositoryRoot: this.repositoryRoot, projectId: manifest.projectId, + command: this.command, digest, env: this.env, + }); + const blockers = []; + if (!config.schedules.some((schedule) => schedule.lifetime === 'system')) { + blockers.push({ code: 'NO_SYSTEM_SCHEDULES' }); + } + if (!installed.length) blockers.push({ code: 'LAUNCHER_NOT_INSTALLED' }); + for (const file of rendered.files) { + const record = installed.find((entry) => entry.name === file.name && resolve(entry.path) === resolve(file.path)); + if (!record) { + blockers.push({ code: 'LAUNCHER_OWNERSHIP_MISMATCH', path: file.path }); + continue; + } + if (!existsSync(file.path)) { + blockers.push({ code: 'LAUNCHER_FILE_MISSING', path: file.path }); + } else if (sha256(readFileSync(file.path)) !== record.sha256) { + blockers.push({ code: 'LAUNCHER_FILE_MODIFIED', path: file.path }); + } + } + for (const record of installed) { + if (!rendered.files.some((file) => file.name === record.name && resolve(file.path) === resolve(record.path))) { + blockers.push({ code: 'LAUNCHER_OWNERSHIP_MISMATCH', path: record.path }); + } + } + const updates = rendered.files.map((file) => ({ + name: file.name, path: file.path, + changed: !existsSync(file.path) || readFileSync(file.path, 'utf8') !== file.content, + })); + const stale = installed.some((entry) => entry.configDigest !== digest); + return { + action: 'reconcile-system-schedule-launcher', projectId: manifest.projectId, + digest, currentConfigDigest: digest, + installedConfigDigests: [...new Set(installed.map((entry) => entry.configDigest))], + systemSchedules: config.schedules.filter((schedule) => schedule.lifetime === 'system').map((schedule) => schedule.id), + ...rendered, updates, stale, + blockers, canProceed: blockers.length === 0, + mutationPerformed: false, + }; + } + + reconcile({ expectedDigest } = {}) { + const plan = this.planReconcile(); + if (expectedDigest && plan.digest !== expectedDigest) { + throw new TorchError('System schedule configuration changed after review', { + code: 'SCHEDULE_LAUNCHER_PLAN_STALE', details: { expected: expectedDigest, actual: plan.digest }, + }); + } + if (!plan.canProceed) throw new TorchError('System schedule launcher reconciliation is blocked', { + code: 'SCHEDULE_LAUNCHER_RECONCILE_BLOCKED', details: plan.blockers, + }); + if (!plan.stale && !plan.updates.some((entry) => entry.changed)) { + return { ...plan, changed: false, mutationPerformed: false }; + } + const manifest = readInstallManifest(this.repositoryRoot); + const originalManifest = structuredClone(manifest); + const previous = new Map(plan.files.map((file) => [file.path, readFileSync(file.path, 'utf8')])); + try { + for (const file of plan.files) { + const temporary = `${file.path}.tmp-${process.pid}-${randomUUID()}`; + writeFileSync(temporary, file.content, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + renameSync(temporary, file.path); + } + for (const record of manifest.external ?? []) { + if (record.type !== 'systemd-user-unit') continue; + const replacement = plan.files.find((file) => file.name === record.name && resolve(file.path) === resolve(record.path)); + record.sha256 = sha256(replacement.content); + record.configDigest = plan.digest; + } + writeInstallManifest(this.repositoryRoot, manifest); + runSystemctl(this.executor, ['daemon-reload']); + runSystemctl(this.executor, ['enable', '--now', plan.timerName]); + return { ...plan, changed: true, mutationPerformed: true, requiresCommit: ['.torch/install-manifest.json'] }; + } catch (error) { + for (const [path, content] of previous) { + try { + const temporary = `${path}.rollback-${process.pid}-${randomUUID()}`; + writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); + renameSync(temporary, path); + } catch { /* preserve the original failure; status will expose drift */ } + } + writeInstallManifest(this.repositoryRoot, originalManifest); + try { runSystemctl(this.executor, ['daemon-reload']); } catch { /* best-effort restore the loaded units */ } + throw error; + } + } + status() { const manifest = readInstallManifest(this.repositoryRoot); const units = (manifest.external ?? []).filter((entry) => entry.type === 'systemd-user-unit'); @@ -136,7 +234,8 @@ export class ScheduleLauncherService { installed: units.length > 0, units: units.map((entry) => ({ ...entry, exists: existsSync(entry.path) })), currentConfigDigest: scheduleConfigDigest(this.repositoryRoot), - stale: units.some((entry) => entry.configDigest !== scheduleConfigDigest(this.repositoryRoot)), + stale: units.some((entry) => entry.configDigest !== scheduleConfigDigest(this.repositoryRoot) + || !existsSync(entry.path) || sha256(readFileSync(entry.path)) !== entry.sha256), mutationPerformed: false, }; } diff --git a/src/schedules/service.mjs b/src/schedules/service.mjs index ea6bcfa..b3b4e2f 100644 --- a/src/schedules/service.mjs +++ b/src/schedules/service.mjs @@ -2,6 +2,10 @@ import { randomUUID } from 'node:crypto'; import { spawnSync } from 'node:child_process'; import { loadFleetDefinition } from '../kernel/worktrees.mjs'; import { TorchError } from '../kernel/errors.mjs'; +import { queueManagerCheckIn } from '../observability/manager-checkin.mjs'; +import { CheckService } from '../checks/service.mjs'; +import { IntegrationService } from '../integration/service.mjs'; +import { OwnerDigestService } from '../observability/owner-digest.mjs'; function cronPartMatches(part, value, { sunday = false } = {}) { return part.split(',').some((segment) => { @@ -47,7 +51,7 @@ function validate(definition) { const id = text(definition?.id, 'id'); if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) throw new TorchError(`Invalid schedule id: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); if (!['system', 'session'].includes(definition.lifetime)) throw new TorchError(`Invalid schedule lifetime: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); - if (!['read-only', 'mutating'].includes(definition.behavior)) throw new TorchError(`Invalid schedule behavior: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); + if (!['read-only', 'coordination', 'mutating'].includes(definition.behavior)) throw new TorchError(`Invalid schedule behavior: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); if (!Array.isArray(definition.required_authority) || !definition.required_authority.length) { throw new TorchError(`Schedule has no required authority: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); } @@ -62,9 +66,43 @@ function validate(definition) { && (typeof definition.trigger.expression !== 'string' || definition.trigger.expression.trim().split(/\s+/).length !== 5)) { throw new TorchError(`Invalid schedule cron expression: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); } - if (definition.action?.type !== 'command' || typeof definition.action.command !== 'string' - || !Array.isArray(definition.action.args) || definition.action.args.some((arg) => typeof arg !== 'string')) { - throw new TorchError(`Schedule action must be a shell-free command: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); + if (definition.action?.type === 'command') { + if (typeof definition.action.command !== 'string' || !Array.isArray(definition.action.args) + || definition.action.args.some((arg) => typeof arg !== 'string') || definition.behavior === 'coordination') { + throw new TorchError(`Schedule action/behavior is invalid: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); + } + } else if (definition.action?.type === 'manager-check-in') { + if (definition.behavior !== 'coordination' || definition.lifetime !== 'system' + || !definition.required_authority.includes('owner')) { + throw new TorchError(`Manager check-in schedules must be owner-authorized system coordination schedules: ${id}`, { + code: 'SCHEDULE_CONFIG_INVALID', details: { id }, + }); + } + } else if (definition.action?.type === 'integration-drain') { + if (definition.behavior !== 'mutating' || definition.lifetime !== 'system' + || !['interval', 'cron'].includes(definition.trigger.type) + || !definition.required_authority.includes('owner') + || (definition.trigger.type === 'interval' && definition.trigger.seconds < 60) + || typeof definition.action.landing_authority_id !== 'string' + || !/^[a-z0-9][a-z0-9-]*$/.test(definition.action.landing_authority_id) + || (definition.action.limit !== undefined + && (!Number.isInteger(definition.action.limit) || definition.action.limit < 1 || definition.action.limit > 500))) { + throw new TorchError(`Integration-drain schedules must be owner-authorized system mutation schedules: ${id}`, { + code: 'SCHEDULE_CONFIG_INVALID', details: { id }, + }); + } + if (definition.retry?.max_attempts !== undefined && definition.retry.max_attempts !== 1) { + throw new TorchError(`Integration-drain schedules cannot retry an ambiguous partial drain: ${id}`, { + code: 'SCHEDULE_CONFIG_INVALID', details: { id }, + }); + } + } else if (definition.action?.type === 'owner-digest') { + if (definition.behavior !== 'coordination' || definition.lifetime !== 'system' + || !definition.required_authority.includes('owner') || (definition.retry?.max_attempts ?? 1) !== 1) { + throw new TorchError('Owner digest requires an owner-authorized system coordination schedule without automatic retry', { code: 'SCHEDULE_CONFIG_INVALID' }); + } + } else { + throw new TorchError(`Unsupported schedule action: ${id}`, { code: 'SCHEDULE_CONFIG_INVALID' }); } const attempts = definition.retry?.max_attempts ?? 1; if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10) { @@ -80,20 +118,56 @@ function bounded(value) { return content.length > 65_536 ? `${content.slice(0, 65_536)}\n[truncated]` : content; } +function managerWakeCapability(prepared, managerId, maxUsd) { + const plan = prepared?.plan; + if (!plan || plan.canProceed !== true) { + const blockers = plan?.blockers ?? []; + const unsupportedCeiling = blockers.some((blocker) => blocker.code?.startsWith('RUNTIME_COST_CEILING_')); + return { + plan, action: null, + reason: unsupportedCeiling ? 'runtime-cost-ceiling-unsupported' : 'manager-runtime-wake-plan-blocked', + blockers, + }; + } + const action = plan.actions?.find((entry) => entry.areaId === managerId); + if (!action || plan.actions.length !== 1) { + return { plan, action: null, reason: 'manager-runtime-wake-plan-invalid', blockers: [] }; + } + if (maxUsd === undefined) return { plan, action, reason: null, blockers: [] }; + const receipt = action?.costCeiling; + if (receipt?.enforced !== true || receipt.maxUsd !== maxUsd || typeof receipt.mode !== 'string' + || !receipt.mode.trim()) { + return { plan, action: null, reason: 'runtime-cost-ceiling-unproven', blockers: [] }; + } + return { plan, action, reason: null, blockers: [] }; +} + export class ScheduleService { constructor({ repositoryRoot, controlPlane, + wakeManager = null, executor = (command, args, options) => spawnSync(command, args, options), clock = () => new Date(), idFactory = randomUUID, } = {}) { this.repositoryRoot = repositoryRoot; this.controlPlane = controlPlane; + this.wakeManager = wakeManager; this.executor = executor; this.clock = clock; this.idFactory = idFactory; const { config } = loadFleetDefinition(repositoryRoot); + this.integrationPolicy = config.integration; + this.runtimeWakeBudget = config.runtime_wake_budget?.max_invocations_per_day ?? 0; this.definitions = new Map((config.schedules ?? []).map((definition) => { const normalized = validate(definition); + if (normalized.action.type === 'integration-drain') { + if (!this.integrationPolicy.landing_authority.includes(normalized.action.landing_authority_id)) { + throw new TorchError(`Integration-drain schedule ${normalized.id} names an identity without landing authority`, { + code: 'SCHEDULE_CONFIG_INVALID', details: { landingAuthorityId: normalized.action.landing_authority_id }, + }); + } + this.controlPlane.assertIdentity(normalized.action.landing_authority_id); + } return [normalized.id, normalized]; })); } @@ -115,15 +189,137 @@ export class ScheduleService { ); CREATE INDEX IF NOT EXISTS schedule_runs_schedule_time ON schedule_runs(schedule_id, started_at, id); + CREATE TABLE IF NOT EXISTS schedule_wake_reservations ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + schedule_id TEXT NOT NULL, + manager_id TEXT NOT NULL, + message_id TEXT NOT NULL UNIQUE, + budget_day TEXT NOT NULL, + reserved_at TEXT NOT NULL, + outcome TEXT NOT NULL DEFAULT 'reserved' + ); + CREATE INDEX IF NOT EXISTS schedule_wake_project_budget_day + ON schedule_wake_reservations(project_id, budget_day); `); } + #reserveWake(definition, managerId, messageId, at) { + const cap = this.runtimeWakeBudget; + if (!definition.action.wake?.enabled || !Number.isInteger(cap) || cap < 1) { + return { allowed: false, reason: 'wake-disabled' }; + } + const database = this.controlPlane.database; + const budgetDay = at.toISOString().slice(0, 10); + this.#initialize(); + database.exec('BEGIN IMMEDIATE'); + try { + const existing = database.prepare(` + SELECT outcome FROM schedule_wake_reservations WHERE message_id = ? + `).get(messageId); + if (existing) { + database.exec('COMMIT'); + return { allowed: false, reason: 'manager-wake-already-attempted', outcome: existing.outcome }; + } + const pending = database.prepare(` + SELECT id FROM schedule_wake_reservations + WHERE project_id = ? AND manager_id = ? AND outcome = 'reserved' + `).get(this.controlPlane.projectId, managerId); + const state = this.controlPlane.identity(managerId).state; + if (pending || !['offline', 'idle', 'waiting', 'stale'].includes(state)) { + database.exec('COMMIT'); + return { allowed: false, reason: 'manager-runtime-already-active', managerState: state }; + } + const count = database.prepare(` + SELECT COUNT(*) AS count FROM schedule_wake_reservations + WHERE project_id = ? AND budget_day = ? + `).get(this.controlPlane.projectId, budgetDay).count; + if (count >= cap) { + database.exec('COMMIT'); + return { allowed: false, reason: 'daily-invocation-budget-exhausted', used: count, limit: cap }; + } + database.prepare(` + INSERT INTO schedule_wake_reservations ( + id, project_id, schedule_id, manager_id, message_id, budget_day, reserved_at, outcome + ) VALUES (?, ?, ?, ?, ?, ?, ?, 'reserved') + `).run(this.idFactory(), this.controlPlane.projectId, definition.id, managerId, + messageId, budgetDay, at.toISOString()); + database.exec('COMMIT'); + return { allowed: true, used: count + 1, limit: cap, budgetDay }; + } catch (error) { + try { database.exec('ROLLBACK'); } catch { /* retain the original failure */ } + throw error; + } + } + #definition(id) { const definition = this.definitions.get(id); if (!definition) throw new TorchError(`Unknown schedule: ${id}`, { code: 'SCHEDULE_NOT_FOUND' }); return definition; } + wakeReservations({ managerId, limit = 100 } = {}) { + if (managerId) this.controlPlane.assertIdentity(managerId); + const database = this.controlPlane.database; + if (!database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schedule_wake_reservations'").get()) return []; + const safeLimit = Number.isInteger(limit) && limit > 0 && limit <= 1000 ? limit : 100; + const rows = managerId + ? database.prepare(`SELECT * FROM schedule_wake_reservations + WHERE project_id = ? AND manager_id = ? ORDER BY reserved_at DESC, id DESC LIMIT ?`) + .all(this.controlPlane.projectId, managerId, safeLimit) + : database.prepare(`SELECT * FROM schedule_wake_reservations + WHERE project_id = ? ORDER BY reserved_at DESC, id DESC LIMIT ?`) + .all(this.controlPlane.projectId, safeLimit); + return rows.map((row) => ({ + id: row.id, scheduleId: row.schedule_id, managerId: row.manager_id, + messageId: row.message_id, budgetDay: row.budget_day, + reservedAt: row.reserved_at, outcome: row.outcome, + blocksManagerWake: row.outcome === 'reserved', + managerState: this.controlPlane.identity(row.manager_id).state, + runtimeStoppedVerified: false, + })); + } + + recoverWake({ reservationId, actorId, approved = false, runtimeStopped = false, note } = {}) { + this.controlPlane.assertOwnerActor(actorId); + if (!approved || runtimeStopped !== true || typeof note !== 'string' || !note.trim()) { + throw new TorchError('Wake recovery requires owner approval, a stopped-runtime attestation and an evidence note.', { + code: 'WAKE_RECOVERY_APPROVAL_REQUIRED', + }); + } + const database = this.controlPlane.database; + this.#initialize(); + database.exec('BEGIN IMMEDIATE'); + try { + const row = database.prepare('SELECT * FROM schedule_wake_reservations WHERE id = ? AND project_id = ?') + .get(reservationId, this.controlPlane.projectId); + if (!row) throw new TorchError('Unknown wake reservation', { code: 'WAKE_RESERVATION_NOT_FOUND' }); + if (row.outcome === 'recovered-stopped') { + database.exec('COMMIT'); + return { id: row.id, outcome: row.outcome, replay: true, invoked: false }; + } + if (row.outcome !== 'reserved' || this.controlPlane.identity(row.manager_id).state !== 'offline') { + throw new TorchError('Only an interrupted reservation for an offline manager can be recovered.', { + code: 'WAKE_RECOVERY_BLOCKED', details: { outcome: row.outcome, managerId: row.manager_id }, + }); + } + database.prepare("UPDATE schedule_wake_reservations SET outcome = 'recovered-stopped' WHERE id = ?") + .run(row.id); + this.controlPlane.auditOwnerAction({ actorId, operation: 'schedule.wake.recover', + entityType: 'wake-reservation', entityId: row.id, + details: { managerId: row.manager_id, messageId: row.message_id, note: note.trim(), + runtimeStoppedAttested: true, runtimeStoppedVerified: false, + budgetChargeRetained: true, messageReplayBlocked: true }, + }); + database.exec('COMMIT'); + return { id: row.id, outcome: 'recovered-stopped', replay: false, invoked: false, + budgetChargeRetained: true, messageReplayBlocked: true }; + } catch (error) { + try { database.exec('ROLLBACK'); } catch { /* retain original failure */ } + throw error; + } + } + runs({ scheduleId, limit = 100 } = {}) { const table = this.controlPlane.database.prepare(` SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schedule_runs' @@ -143,6 +339,7 @@ export class ScheduleService { plan({ scheduleId, actorId, at = this.clock() } = {}) { const definition = this.#definition(scheduleId); const blockers = []; + if (definition.action.type === 'owner-digest' && loadFleetDefinition(this.repositoryRoot).config.owner_digest?.enabled !== true) blockers.push('owner-digest-disabled'); if (!definition.required_authority.includes(actorId)) blockers.push('actor-not-authorized'); if (actorId !== 'owner') { try { this.controlPlane.assertIdentity(actorId); } catch { blockers.push('actor-not-a-fleet-identity'); } @@ -164,6 +361,14 @@ export class ScheduleService { } return { schedule: definition, actorId, due, nextAt, blockers, + wakeEnabled: definition.action.type === 'manager-check-in' && definition.action.wake?.enabled === true, + wakeAvailable: typeof this.wakeManager?.plan === 'function' + && typeof this.wakeManager?.invoke === 'function', + wakeMaxUsdPerInvocation: definition.action.type === 'manager-check-in' + ? definition.action.wake?.max_usd_per_invocation ?? null : null, + wakeBudgetMode: definition.action.type === 'manager-check-in' + ? definition.action.wake?.budget_mode + ?? (definition.action.wake?.max_usd_per_invocation !== undefined ? 'usd-hard-cap' : null) : null, canRun: blockers.length === 0, mutationPerformed: false, }; } @@ -171,18 +376,117 @@ export class ScheduleService { run({ scheduleId, actorId, approved = false } = {}) { const plan = this.plan({ scheduleId, actorId }); if (!plan.canRun) throw new TorchError(`Schedule cannot run: ${scheduleId}`, { code: 'SCHEDULE_BLOCKED', details: plan.blockers }); - if (plan.schedule.behavior === 'mutating' && approved !== true) { - throw new TorchError('Mutating schedule requires explicit approval', { code: 'APPROVAL_REQUIRED' }); + if (plan.schedule.behavior !== 'read-only' && approved !== true) { + throw new TorchError('Mutating or coordination schedule requires explicit approval', { code: 'APPROVAL_REQUIRED' }); } const startedAt = this.clock().toISOString(); let execution; let attempts = 0; + let terminalAttempt = false; do { attempts += 1; - execution = this.executor(plan.schedule.action.command, plan.schedule.action.args, { - cwd: this.repositoryRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: false, - }); - } while ((execution.status !== 0 || execution.error) && attempts < plan.schedule.retry.max_attempts); + if (plan.schedule.action.type === 'manager-check-in') { + try { + const checkIn = queueManagerCheckIn({ + repositoryRoot: this.repositoryRoot, controlPlane: this.controlPlane, + managerId: plan.schedule.action.manager_id, at: this.clock(), + staleWork: plan.schedule.action.stale_work ?? null, + }); + const needsWake = checkIn.queued + || (checkIn.reason === 'check-in-already-pending' && Boolean(checkIn.messageId)); + let wake = { attempted: false, reason: checkIn.queued ? 'wake-disabled' : checkIn.reason }; + const managerState = this.controlPlane.identity(plan.schedule.action.manager_id).state; + const managerCanBeWoken = ['offline', 'idle', 'waiting', 'stale'].includes(managerState); + const hasWakeManager = typeof this.wakeManager?.plan === 'function' + && typeof this.wakeManager?.invoke === 'function'; + if (needsWake && plan.schedule.action.wake?.enabled && !hasWakeManager) { + wake = { attempted: false, reason: 'manager-runtime-waker-unavailable' }; + } else if (needsWake && plan.schedule.action.wake?.enabled && !managerCanBeWoken) { + wake = { attempted: false, reason: 'manager-runtime-already-active', managerState }; + } else if (needsWake && plan.schedule.action.wake?.enabled) { + const managerId = plan.schedule.action.manager_id; + const budgetMode = plan.wakeBudgetMode; + const maxUsd = budgetMode === 'usd-hard-cap' + ? plan.schedule.action.wake.max_usd_per_invocation : undefined; + let prepared; + try { + prepared = this.wakeManager.plan({ managerId, maxUsd, scheduleId }); + } catch (error) { + wake = { attempted: false, reason: 'manager-runtime-wake-plan-failed', error: error.message }; + } + if (!wake.error) { + const capability = managerWakeCapability(prepared, managerId, maxUsd); + if (capability.reason) { + wake = { + attempted: false, reason: capability.reason, + ...(capability.blockers.length ? { blockers: capability.blockers } : {}), + }; + } else { + const reservation = this.#reserveWake( + plan.schedule, managerId, checkIn.messageId, this.clock(), + ); + if (!reservation.allowed) { + wake = { attempted: false, ...reservation }; + } else { + try { + const result = this.wakeManager.invoke({ + managerId, checkIn, scheduleId, reservation, maxUsd, prepared, + }); + wake = { + attempted: true, ...reservation, budgetMode, + dollarCapEnforced: maxUsd !== undefined, + ...(maxUsd === undefined ? {} : { maxUsd, costCeiling: capability.action.costCeiling }), result, + }; + this.controlPlane.database.prepare(` + UPDATE schedule_wake_reservations SET outcome = 'invoked' + WHERE message_id = ? + `).run(checkIn.messageId); + } catch (error) { + this.controlPlane.database.prepare(` + UPDATE schedule_wake_reservations SET outcome = 'failed' + WHERE message_id = ? + `).run(checkIn.messageId); + terminalAttempt = true; + throw error; + } + } + } + } + } + execution = { status: 0, stdout: JSON.stringify({ checkIn, wake }), stderr: '' }; + } catch (error) { + execution = { status: 1, stdout: '', stderr: error.message, error }; + } + } else if (plan.schedule.action.type === 'owner-digest') { + try { + const digest = new OwnerDigestService({ repositoryRoot: this.repositoryRoot, controlPlane: this.controlPlane, clock: this.clock }); + const published = digest.publish({ actorId: 'session-manager' }); + execution = { status: 0, stdout: JSON.stringify({ digestId: published.id, generatedAt: published.report.generatedAt, + externalDeliveryPerformed: false }), stderr: '' }; + } catch (error) { execution = { status: 1, stdout: '', stderr: error.message, error }; } + } else if (plan.schedule.action.type === 'integration-drain') { + try { + const checkService = new CheckService({ + repositoryRoot: this.repositoryRoot, controlPlane: this.controlPlane, + }); + const integration = new IntegrationService({ + repositoryRoot: this.repositoryRoot, controlPlane: this.controlPlane, checkService, + }); + const drain = integration.drain({ + actorId: plan.schedule.action.landing_authority_id, + limit: plan.schedule.action.limit ?? 50, + }); + execution = { status: 0, stdout: JSON.stringify(drain), stderr: '' }; + } catch (error) { + execution = { status: 1, stdout: '', stderr: error.message, error }; + } + } else { + execution = this.executor(plan.schedule.action.command, plan.schedule.action.args, { + cwd: this.repositoryRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: false, + }); + } + } while ((execution.status !== 0 || execution.error) + && attempts < plan.schedule.retry.max_attempts && !terminalAttempt); const succeeded = execution.status === 0 && !execution.error && !execution.signal; const run = { id: this.idFactory(), scheduleId, actorId, startedAt, finishedAt: this.clock().toISOString(), diff --git a/src/self-host/service.mjs b/src/self-host/service.mjs index 2d0a48d..6326374 100644 --- a/src/self-host/service.mjs +++ b/src/self-host/service.mjs @@ -22,15 +22,31 @@ export const CANDIDATE_ACCEPTANCE_SCENARIOS = Object.freeze([ 'review-install-roster', 'branches-worktrees', 'runtime-identities', + 'runtime-profiles', + 'console-runtime-profile', + 'console-schedule-launcher', + 'runtime-adapters', + 'runtime-plugin-trust', + 'organization-graph', + 'manager-check-ins', + 'manager-check-in-console', + 'structured-approvals', + 'console-owner-approval', + 'hierarchy-evolution', + 'hierarchy-cli-review', 'durable-messaging', 'ownership-query', 'unsafe-worktree-detection', 'backlog-integration', + 'forge-sync', 'resource-lifecycle', + 'candidate-acceptance-isolation', + 'package-distribution', 'capture-stop-resume', 'detach-uninstall', 'combatrig-compatibility', 'product-surface', + 'artifact-review', 'operations-observability', ]); @@ -131,15 +147,20 @@ function gitSourceDescriptor(root) { const tracked = new Set(execFileSync('git', ['-C', root, 'ls-files', '-z'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }).split('\0').filter(Boolean)); + const excludedUntracked = execFileSync('git', ['-C', root, 'ls-files', '--others', '--exclude-standard', '-z'], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + }).split('\0').filter(Boolean).sort(); const include = (name, entry) => name === 'node_modules' || name.startsWith('node_modules/') || tracked.has(name) || (entry.isDirectory() && [...tracked].some((path) => path.startsWith(`${name}/`))); - return { mode: 'git-commit', commit, include }; + return { mode: 'git-commit', commit, include, excludedUntracked }; } function sourceDescriptor(root) { - return gitSourceDescriptor(root) ?? { mode: 'artifact', commit: null, include: () => true }; + return gitSourceDescriptor(root) ?? { + mode: 'artifact', commit: null, include: () => true, excludedUntracked: [], + }; } function validateReleaseMetadata(root, requestedVersion) { @@ -226,6 +247,7 @@ export class VersionService { digest: treeHash(sourceRoot, records), files: records.length, bytes: records.reduce((total, record) => total + record.bytes, 0), sourceMode: descriptor.mode, sourceCommit: descriptor.commit, + excludedUntracked: descriptor.excludedUntracked, compatibility: metadata.release.state, conflicts: existsSync(destination) ? [{ code: 'VERSION_ALREADY_INSTALLED', version: metadata.version }] : [], mutationPerformed: false, diff --git a/test/fleet/artifacts.test.mjs b/test/fleet/artifacts.test.mjs new file mode 100644 index 0000000..f0ab3d5 --- /dev/null +++ b/test/fleet/artifacts.test.mjs @@ -0,0 +1,477 @@ +import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { existsSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, join } from 'node:path'; +import test from 'node:test'; +import { ArtifactService, readPublishedArtifact } from '../../src/artifacts/service.mjs'; +import { BacklogService } from '../../src/backlog/service.mjs'; +import { createConsoleServer } from '../../src/console/server.mjs'; +import { openControlPlane } from '../../src/control-plane/service.mjs'; +import { observeProject } from '../../src/observability/snapshot.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 { createWorktrees } from '../../src/kernel/worktrees.mjs'; +import { callTorchTool } from '../../src/mcp/tools.mjs'; + +const ONE_PIXEL_PNG = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p6sAAAAASUVORK5CYII=', + 'base64', +); +const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname; + +function installedFixture() { + const root = mkdtempSync(join(tmpdir(), 'torch-artifact-')); + 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, 'index.js'), 'export const ready = true;\n'); + writeFileSync(join(root, 'index.test.js'), 'export const scenario = true;\n'); + execFileSync('git', ['-C', root, 'add', '.']); + execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']); + + 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: [], + }; + const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) }; + installProject({ repository, proposal, env, projectId: 'artifact-fixture' }); + execFileSync('git', ['-C', root, 'add', '.torch']); + execFileSync('git', ['-C', root, 'commit', '-m', 'install torch']); + const created = createWorktrees({ + repository: inspectRepository(root), parentOverride: join(tmpdir(), `${basename(root)}-managed-worktrees`), + }); + const controlPlane = openControlPlane({ repositoryRoot: root, env }); + const backlog = new BacklogService({ repositoryRoot: root, controlPlane }); + const worker = proposal.domains[0].id; + const workerWorktree = created.created.find((entry) => entry.area === worker); + const task = backlog.create({ + actorId: 'session-manager', title: 'Review image fixture', description: 'Publish a review image.', + affectedDomains: [worker], acceptanceCriteria: ['The image has exact provenance.'], + }); + const ready = backlog.transition({ taskId: task.id, actorId: 'session-manager', to: 'ready', expectedRevision: 1 }); + const assigned = backlog.transition({ + taskId: task.id, actorId: 'session-manager', to: 'assigned', expectedRevision: ready.revision, owner: worker, + }); + backlog.transition({ taskId: task.id, actorId: worker, to: 'in_progress', expectedRevision: assigned.revision }); + writeFileSync(join(workerWorktree.path, 'review.png'), ONE_PIXEL_PNG); + controlPlane.reportStatus({ + areaId: worker, state: 'working', runtime: 'fixture-runtime', runtimeSessionId: 'fixture-session', task: task.id, + }); + const service = new ArtifactService({ repositoryRoot: root, controlPlane, backlogService: backlog }); + const commit = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); + return { + root, env, controlPlane, backlog, worker, workerWorktree, task, service, commit, + close: () => controlPlane.close(), + }; +} + +test('SCN-artifact-review / SCN-console-priority-confirmation: evidence feedback and owner reprioritization require fresh confirmed previews', async (context) => { + const fixture = installedFixture(); + context.after(() => fixture.close()); + const { service, backlog, worker, task, commit } = fixture; + const beforeMessages = fixture.controlPlane.database.prepare('SELECT COUNT(*) AS count FROM messages').get().count; + const published = spawnSync(process.execPath, [CLI, 'artifacts', 'publish', + '--area', worker, '--task', task.id, '--title', 'Building approach', + '--alt', 'A one-pixel test image.', '--file', 'review.png', '--commit', commit, '--json'], { + cwd: fixture.root, env: fixture.env, encoding: 'utf8', + }); + assert.equal(published.status, 0, published.stderr || published.stdout); + const artifact = JSON.parse(published.stdout); + assert.equal(artifact.taskId, task.id); + assert.equal(artifact.identityId, worker); + assert.equal(artifact.sessionId, 'fixture-session'); + assert.equal(artifact.commit, commit); + assert.equal(existsSync(join(fixture.root, 'review.png')), false); + assert.deepEqual(readPublishedArtifact({ repositoryRoot: fixture.root, env: fixture.env, id: artifact.id }).bytes, + ONE_PIXEL_PNG); + const mcpPublished = callTorchTool(fixture.controlPlane, 'torch_publish_artifact', { + task_id: task.id, title: 'MCP publication', alt: 'A fixed fixture image.', file: 'review.png', commit, + }, { actorId: worker, backlogService: fixture.backlog, artifactService: service }); + assert.equal(mcpPublished.identityId, worker); + assert.throws(() => callTorchTool(fixture.controlPlane, 'torch_publish_artifact', { + area_id: 'session-manager', task_id: task.id, title: 'Impersonated', alt: 'No', file: 'review.png', commit, + }, { actorId: worker, backlogService: fixture.backlog, artifactService: service }), + (error) => error.code === 'FLEET_IDENTITY_MISMATCH'); + + const plan = service.planComment({ artifactId: artifact.id, body: 'Please add more contrast.', by: 'owner' }); + assert.equal(plan.mutationPerformed, false); + assert.equal(plan.recipient, worker); + assert.equal(fixture.controlPlane.database.prepare('SELECT COUNT(*) AS count FROM messages').get().count, beforeMessages); + assert.throws( + () => service.comment({ artifactId: artifact.id, body: 'Take over the project.', by: 'session-manager' }), + (error) => error.code === 'OWNER_AUTHORITY_REQUIRED', + ); + const preview = spawnSync(process.execPath, [CLI, 'artifacts', 'comment', + '--artifact', artifact.id, '--body', 'Please add more contrast.', '--by', 'owner', '--json'], { + cwd: fixture.root, env: fixture.env, encoding: 'utf8', + }); + assert.equal(preview.status, 0, preview.stderr || preview.stdout); + assert.equal(JSON.parse(preview.stdout).mutationPerformed, false); + const comment = spawnSync(process.execPath, [CLI, 'artifacts', 'comment', + '--artifact', artifact.id, '--body', 'Please add more contrast.', '--by', 'owner', '--yes', '--json'], { + cwd: fixture.root, env: fixture.env, encoding: 'utf8', + }); + assert.equal(comment.status, 0, comment.stderr || comment.stdout); + const feedback = JSON.parse(comment.stdout); + assert.equal(feedback.recipient, worker); + const inbox = fixture.controlPlane.readMessages({ recipient: worker, unacknowledgedOnly: true }); + assert.equal(inbox.some((message) => message.id === feedback.id + && message.references.task === task.id + && message.references.commit === commit + && message.references.path === `artifact:${artifact.id}`), true); + assert.equal(fixture.controlPlane.readAudit().some((event) => event.actorId === 'owner' + && event.operation === 'artifact.feedback' && event.entityId === artifact.id), true); + + const snapshot = observeProject({ repositoryRoot: fixture.root, env: fixture.env }); + const observed = snapshot.artifacts.items.find((item) => item.id === artifact.id); + assert.equal(observed.integrity, 'available'); + assert.equal(observed.feedback[0].body, 'Please add more contrast.'); + + const server = createConsoleServer({ repositoryRoot: fixture.root, env: fixture.env }); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolve); + }); + const { port } = server.address(); + try { + const imageResponse = await fetch(`http://127.0.0.1:${port}${artifact.url}`); + assert.equal(imageResponse.status, 200); + assert.equal(imageResponse.headers.get('content-type'), 'image/png'); + assert.deepEqual(Buffer.from(await imageResponse.arrayBuffer()), ONE_PIXEL_PNG); + const writeResponse = await fetch(`http://127.0.0.1:${port}/api/snapshot`, { method: 'POST' }); + assert.equal(writeResponse.status, 405); + + const origin = `http://127.0.0.1:${port}`; + const feedbackUrl = `${origin}/api/artifacts/${artifact.id}/feedback`; + const countMessages = () => fixture.controlPlane.database.prepare('SELECT COUNT(*) AS count FROM messages').get().count; + const beforeDashboardFeedback = countMessages(); + const previewOptions = (body) => ({ + method: 'POST', + headers: { origin, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' }, + body: JSON.stringify({ body }), + }); + const readOnly = await fetch(`${feedbackUrl}/preview`); + assert.equal(readOnly.status, 405); + const wrongContentType = await fetch(`${feedbackUrl}/preview`, { + method: 'POST', + headers: { origin, 'sec-fetch-site': 'same-origin' }, + body: 'body=untrusted', + }); + assert.equal(wrongContentType.status, 415); + const previewResponse = await fetch(`${feedbackUrl}/preview`, previewOptions('Reduce the glow on the sign.')); + assert.equal(previewResponse.status, 200); + const preview = await previewResponse.json(); + assert.equal(preview.preview.mutationPerformed, false); + assert.equal(preview.preview.recipient, worker); + assert.equal(preview.preview.taskId, task.id); + assert.equal(preview.preview.commit, commit); + assert.match(preview.token, /^[a-f0-9-]{36}$/i); + assert.equal(countMessages(), beforeDashboardFeedback); + + const crossOrigin = await fetch(`${feedbackUrl}/preview`, { + method: 'POST', headers: { + origin: 'http://attacker.invalid', 'content-type': 'application/json', + 'sec-fetch-site': 'cross-site', + }, body: JSON.stringify({ body: 'Cross-origin requests must not create owner actions.' }), + }); + assert.equal(crossOrigin.status, 403); + assert.equal((await crossOrigin.json()).error, 'SAME_ORIGIN_REQUIRED'); + assert.equal(countMessages(), beforeDashboardFeedback); + + const priorityUrl = `${origin}/api/backlog/tasks/${task.id}/priority`; + const priorityOptions = (payload) => ({ + method: 'POST', headers: { origin, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' }, + body: JSON.stringify(payload), + }); + const beforePriority = backlog.get(task.id); + const priorityPlanResponse = await fetch(priorityUrl + '/preview', priorityOptions({ + priority: 'high', reason: 'The active review blocks the milestone.', expectedRevision: beforePriority.revision, + })); + assert.equal(priorityPlanResponse.status, 200); + const priorityPlan = await priorityPlanResponse.json(); + assert.equal(priorityPlan.plan.mutationPerformed, false); + assert.equal(priorityPlan.plan.from, 'normal'); + assert.equal(backlog.get(task.id).priority, 'normal', 'preview must not alter the task'); + const rejectedPriority = await fetch(priorityUrl + '/preview', { + method: 'POST', headers: { + origin: 'http://attacker.invalid', 'content-type': 'application/json', 'sec-fetch-site': 'cross-site', + }, body: JSON.stringify({ priority: 'urgent', reason: 'Cross-origin owner action.', expectedRevision: beforePriority.revision }), + }); + assert.equal(rejectedPriority.status, 403); + assert.equal((await rejectedPriority.json()).error, 'SAME_ORIGIN_REQUIRED'); + assert.equal(backlog.get(task.id).priority, 'normal'); + backlog.setPriority({ + taskId: task.id, actorId: 'owner', expectedRevision: beforePriority.revision, + priority: 'low', reason: 'Fixture creates a concurrent owner change.', + }); + const stalePriority = await fetch(priorityUrl, priorityOptions({ + priority: 'high', reason: priorityPlan.plan.reason, expectedRevision: priorityPlan.plan.revision, + token: priorityPlan.token, planHash: priorityPlan.planHash, + })); + assert.equal(stalePriority.status, 409); + assert.equal((await stalePriority.json()).error, 'BACKLOG_REVISION_CONFLICT'); + const currentPriority = backlog.get(task.id); + const refreshedPriorityResponse = await fetch(priorityUrl + '/preview', priorityOptions({ + priority: 'high', reason: 'The active review blocks the milestone.', expectedRevision: currentPriority.revision, + })); + const refreshedPriority = await refreshedPriorityResponse.json(); + const appliedPriority = await fetch(priorityUrl, priorityOptions({ + priority: refreshedPriority.plan.to, reason: refreshedPriority.plan.reason, + expectedRevision: refreshedPriority.plan.revision, + token: refreshedPriority.token, planHash: refreshedPriority.planHash, + })); + assert.equal(appliedPriority.status, 200); + const appliedRecord = (await appliedPriority.json()).task; + assert.equal(appliedRecord.priority, 'high'); + assert.equal(appliedRecord.state, beforePriority.state); + assert.equal(appliedRecord.owner, beforePriority.owner); + assert.deepEqual(appliedRecord.dependencies, beforePriority.dependencies); + assert.deepEqual(appliedRecord.evidence, beforePriority.evidence); + assert.equal(appliedRecord.revision, currentPriority.revision + 1); + assert.equal(appliedRecord.priorityHistory.at(-1).to, 'high'); + const priorityReplay = await fetch(priorityUrl, priorityOptions({ + priority: refreshedPriority.plan.to, reason: refreshedPriority.plan.reason, + expectedRevision: refreshedPriority.plan.revision, + token: refreshedPriority.token, planHash: refreshedPriority.planHash, + })); + assert.equal(priorityReplay.status, 409); + assert.equal((await priorityReplay.json()).error, 'PRIORITY_PREVIEW_EXPIRED'); + + const stale = await fetch(feedbackUrl, { + method: 'POST', + headers: { origin, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' }, + body: JSON.stringify({ body: 'Edited after preview.', token: preview.token, planHash: preview.planHash }), + }); + assert.equal(stale.status, 409); + assert.equal((await stale.json()).error, 'FEEDBACK_PREVIEW_STALE'); + assert.equal(countMessages(), beforeDashboardFeedback); + + const refreshedPreviewResponse = await fetch(`${feedbackUrl}/preview`, previewOptions('Reduce the glow on the sign.')); + const refreshedPreview = await refreshedPreviewResponse.json(); + const sent = await fetch(feedbackUrl, { + method: 'POST', + headers: { origin, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' }, + body: JSON.stringify({ + body: refreshedPreview.preview.body, + token: refreshedPreview.token, + planHash: refreshedPreview.planHash, + }), + }); + assert.equal(sent.status, 200); + const sentRecord = (await sent.json()).feedback; + assert.equal(sentRecord.recipient, worker); + assert.equal(sentRecord.taskId, task.id); + assert.equal(sentRecord.commit, commit); + assert.equal(fixture.controlPlane.readMessages({ recipient: worker, unacknowledgedOnly: true }) + .some((message) => message.id === sentRecord.id + && message.references.task === task.id + && message.references.path === `artifact:${artifact.id}` + && message.references.commit === commit), true); + assert.equal(countMessages(), beforeDashboardFeedback + 1); + const replay = await fetch(feedbackUrl, { + method: 'POST', + headers: { origin, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' }, + body: JSON.stringify({ + body: refreshedPreview.preview.body, + token: refreshedPreview.token, + planHash: refreshedPreview.planHash, + }), + }); + assert.equal(replay.status, 409); + assert.equal((await replay.json()).error, 'FEEDBACK_PREVIEW_EXPIRED'); + assert.equal(countMessages(), beforeDashboardFeedback + 1); + } finally { + await new Promise((resolve) => server.close(resolve)); + } + + const storedPath = join(fixture.controlPlane.stateRoot, 'artifacts', `${artifact.id}.png`); + writeFileSync(storedPath, Buffer.from('tampered image')); + assert.throws( + () => readPublishedArtifact({ repositoryRoot: fixture.root, env: fixture.env, id: artifact.id }), + (error) => error.code === 'ARTIFACT_INTEGRITY_FAILED', + ); + assert.equal(readFileSync(storedPath).toString(), 'tampered image'); +}); + +test('SCN-artifact-boundaries: unsupported provenance, task ownership, and symlink paths fail closed', (context) => { + const fixture = installedFixture(); + context.after(() => fixture.close()); + const { service, worker, task, commit, workerWorktree } = fixture; + assert.throws( + () => service.publish({ areaId: worker, taskId: 'TASK-NOT-REAL', title: 'x', alt: 'x', file: 'review.png', commit }), + (error) => error.code === 'BACKLOG_TASK_NOT_FOUND', + ); + assert.throws( + () => service.publish({ areaId: worker, taskId: task.id, title: 'x', alt: 'x', file: 'review.png', commit: commit.slice(0, 12) }), + (error) => error.code === 'ARTIFACT_COMMIT_INVALID', + ); + const outside = join(fixture.root, 'outside.png'); + writeFileSync(outside, ONE_PIXEL_PNG); + symlinkSync(outside, join(workerWorktree.path, 'linked.png')); + assert.throws( + () => service.publish({ areaId: worker, taskId: task.id, title: 'x', alt: 'x', file: 'linked.png', commit }), + (error) => error.code === 'ARTIFACT_PATH_INVALID', + ); + assert.throws( + () => service.publish({ areaId: worker, taskId: task.id, title: 'x', alt: 'x', file: '../outside.png', commit }), + (error) => error.code === 'ARTIFACT_PATH_INVALID', + ); +}); + +test('SCN-console-task-create-confirmation: owner task creation previews routing and replays idempotently without dispatch', async (context) => { + const fixture = installedFixture(); + context.after(() => fixture.close()); + const server = createConsoleServer({ repositoryRoot: fixture.root, env: fixture.env }); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolve); + }); + const { port } = server.address(); + const origin = `http://127.0.0.1:${port}`; + const endpoint = `${origin}/api/backlog/tasks`; + const options = (payload, requestOrigin = origin) => ({ + method: 'POST', + headers: { origin: requestOrigin, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin' }, + body: JSON.stringify(payload), + }); + const input = { + title: 'Add project-specific task proposal', + description: 'Let the owner introduce work while preserving Session Manager triage.', + priority: 'high', affectedDomains: [fixture.worker], dependencies: [], + acceptanceCriteria: ['The task is proposed and unassigned.', 'No runtime launches.'], + feature: 'Owner dashboard', milestone: '', + }; + const before = fixture.backlog.list().length; + const runtimeSessionBefore = fixture.controlPlane.identity(fixture.worker).runtimeSessionId; + try { + const rejectedOrigin = await fetch(`${endpoint}/preview`, options(input, 'http://attacker.invalid')); + assert.equal(rejectedOrigin.status, 403); + assert.equal(fixture.backlog.list().length, before); + + const previewResponse = await fetch(`${endpoint}/preview`, options(input)); + assert.equal(previewResponse.status, 200); + const preview = await previewResponse.json(); + assert.equal(preview.plan.mutationPerformed, false); + assert.equal(preview.plan.task.state, 'proposed'); + assert.equal(preview.plan.task.owner, null); + assert.deepEqual(preview.plan.task.affectedDomains, [fixture.worker]); + assert.equal(fixture.backlog.list().length, before); + + const stale = await fetch(endpoint, options({ + ...input, title: 'Changed after owner preview', token: preview.token, planHash: preview.planHash, + })); + assert.equal(stale.status, 409); + assert.equal((await stale.json()).error, 'TASK_CREATE_PREVIEW_STALE'); + assert.equal(fixture.backlog.list().length, before); + + const freshResponse = await fetch(`${endpoint}/preview`, options(input)); + const fresh = await freshResponse.json(); + const confirmation = { ...input, token: fresh.token, planHash: fresh.planHash }; + const createdResponse = await fetch(endpoint, options(confirmation)); + assert.equal(createdResponse.status, 201); + const created = await createdResponse.json(); + assert.equal(created.mutationPerformed, true); + assert.equal(created.task.state, 'proposed'); + assert.equal(created.task.owner, null); + assert.deepEqual(created.task.affectedDomains, [fixture.worker]); + assert.equal(fixture.backlog.list().length, before + 1); + assert.equal(fixture.controlPlane.readAudit().some((event) => event.actorId === 'owner' + && event.operation === 'backlog.create' && event.entityId === created.task.id), true); + + const replay = await fetch(endpoint, options(confirmation)); + assert.equal(replay.status, 201); + assert.equal((await replay.json()).task.id, created.task.id); + assert.equal(fixture.backlog.list().length, before + 1); + assert.equal(fixture.controlPlane.identity(fixture.worker).runtimeSessionId, runtimeSessionBefore); + } finally { + await new Promise((resolve) => server.close(resolve)); + } +}); + +test('SCN-console-owner-agent-request: an owner request is previewed, durable, inbox-only, and idempotent', async (context) => { + const fixture = installedFixture(); + context.after(() => fixture.close()); + const server = createConsoleServer({ repositoryRoot: fixture.root, env: fixture.env }); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', resolve); + }); + const { port } = server.address(); + const origin = `http://127.0.0.1:${port}`; + const endpoint = `${origin}/api/owner-requests`; + const options = (payload, requestOrigin = origin) => ({ + method: 'POST', headers: { + origin: requestOrigin, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin', + }, body: JSON.stringify(payload), + }); + const body = 'Please inspect the exact-commit checks for this task and report whether the evidence is still current.'; + const backlogBefore = fixture.backlog.get(fixture.task.id); + const identityBefore = fixture.controlPlane.identity(fixture.worker); + const messageCount = () => fixture.controlPlane.database.prepare('SELECT COUNT(*) AS count FROM messages').get().count; + const messagesBefore = messageCount(); + try { + const crossOrigin = await fetch(`${endpoint}/preview`, options({ recipient: fixture.worker, taskId: fixture.task.id, body }, 'http://attacker.invalid')); + assert.equal(crossOrigin.status, 403); + assert.equal(messageCount(), messagesBefore); + + const previewResponse = await fetch(`${endpoint}/preview`, options({ recipient: fixture.worker, taskId: fixture.task.id, body })); + assert.equal(previewResponse.status, 200); + const preview = await previewResponse.json(); + assert.equal(preview.plan.mutationPerformed, false); + assert.equal(preview.plan.owner, 'owner'); + assert.equal(preview.plan.recipient, fixture.worker); + assert.equal(preview.plan.task.id, fixture.task.id); + assert.equal(preview.plan.body, body); + assert.match(preview.plan.effect, /does not create or change a task.*wake or start a session/); + assert.equal(messageCount(), messagesBefore); + + const changedBody = await fetch(endpoint, options({ + recipient: fixture.worker, taskId: fixture.task.id, body: `${body} Changed after preview.`, + token: preview.token, planHash: preview.planHash, + })); + assert.equal(changedBody.status, 409); + assert.equal((await changedBody.json()).error, 'OWNER_REQUEST_PREVIEW_STALE'); + assert.equal(messageCount(), messagesBefore); + + writeFileSync(join(fixture.root, 'new-evidence.md'), '# Repository changed after preview\n'); + execFileSync('git', ['-C', fixture.root, 'add', 'new-evidence.md']); + execFileSync('git', ['-C', fixture.root, 'commit', '-m', 'advance repository after owner preview']); + const staleHead = await fetch(endpoint, options({ + recipient: fixture.worker, taskId: fixture.task.id, body, token: preview.token, planHash: preview.planHash, + })); + assert.equal(staleHead.status, 409); + assert.equal((await staleHead.json()).error, 'OWNER_REQUEST_PREVIEW_STALE'); + + const freshResponse = await fetch(`${endpoint}/preview`, options({ recipient: fixture.worker, taskId: fixture.task.id, body })); + const fresh = await freshResponse.json(); + const confirmation = { recipient: fixture.worker, taskId: fixture.task.id, body, token: fresh.token, planHash: fresh.planHash }; + const sentResponse = await fetch(endpoint, options(confirmation)); + assert.equal(sentResponse.status, 201); + const sent = await sentResponse.json(); + assert.equal(sent.mutationPerformed, true); + assert.equal(sent.message.sender, 'owner'); + assert.equal(sent.message.recipient, fixture.worker); + assert.equal(sent.message.body, body); + assert.equal(sent.message.references.task, fixture.task.id); + assert.equal(messageCount(), messagesBefore + 1); + assert.equal(fixture.controlPlane.readMessages({ recipient: fixture.worker }).some((message) => message.id === sent.message.id), true); + assert.equal(fixture.controlPlane.readAudit().some((event) => event.actorId === 'owner' + && event.operation === 'agent.request' && event.entityId === sent.message.id), true); + assert.deepEqual(fixture.backlog.get(fixture.task.id), backlogBefore); + assert.deepEqual(fixture.controlPlane.identity(fixture.worker), identityBefore); + + const replay = await fetch(endpoint, options(confirmation)); + assert.equal(replay.status, 201); + assert.equal((await replay.json()).message.id, sent.message.id); + assert.equal(messageCount(), messagesBefore + 1); + assert.throws(() => fixture.controlPlane.sendOwnerRequest({ + actorId: 'session-manager', recipient: fixture.worker, body, + }), (error) => error.code === 'OWNER_AUTHORITY_REQUIRED'); + } finally { + await new Promise((resolve) => server.close(resolve)); + } +}); diff --git a/test/fleet/backlog-activity.test.mjs b/test/fleet/backlog-activity.test.mjs new file mode 100644 index 0000000..4b49d1c --- /dev/null +++ b/test/fleet/backlog-activity.test.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { commitTaskReferences, observeTaskActivity } from '../../src/backlog/activity.mjs'; + +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'torch-task-activity-')); + const git = (args, date = '2026-09-20T12:00:00Z') => execFileSync('git', ['-C', root, ...args], { + encoding: 'utf8', env: { ...process.env, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date }, + }).trim(); + git(['init', '-b', 'main']); + git(['config', 'user.email', 'torch-test@example.invalid']); + git(['config', 'user.name', 'TORCH Test']); + writeFileSync(join(root, 'app.js'), 'export const value=1;\n'); + git(['add', 'app.js']); + git(['commit', '-m', 'Initial source']); + return { root, git }; +} + +function task(id, { ownerRequested = false, state = 'ready', createdAt = '2026-09-21T00:00:00Z' } = {}) { + return { id, title: id, state, createdAt, updatedAt: '2026-09-30T00:00:00Z', + history: [{ actorId: ownerRequested ? 'owner' : 'session-manager' }], owner: null }; +} + +test('SCN-backlog-commit-activity: named commits show real progress, metadata edits do not hide stale owner requests, closure stays intent', () => { + const input = fixture(); + input.git(['branch', 'worker']); + input.git(['checkout', 'worker']); + input.git(['commit', '--allow-empty', '-m', 'Implement TASK-recent\n\nCloses: TASK-recent'], '2026-09-29T12:00:00Z'); + const commit = input.git(['rev-parse', 'HEAD']); + input.git(['checkout', 'main']); + input.git(['branch', 'unmanaged']); + input.git(['checkout', 'unmanaged']); + input.git(['commit', '--allow-empty', '-m', 'Mention TASK-owner on unrelated branch'], '2026-09-29T14:00:00Z'); + input.git(['checkout', 'main']); + const tasks = [task('TASK-recent'), task('TASK-old'), task('TASK-owner', { ownerRequested: true }), + task('TASK-waiting', { state: 'blocked' }), task('TASK-done', { state: 'completed' })]; + const before = JSON.stringify(tasks); + const result = observeTaskActivity({ repositoryRoot: input.root, tasks, branches: ['main', 'worker'], + now: new Date('2026-09-30T00:00:00Z') }); + assert.equal(result.complete, true); + assert.equal(result.tasks[0].taskId, 'TASK-owner'); + assert.equal(result.stale.some((item) => item.taskId === 'TASK-owner'), true); + assert.equal(result.stale.some((item) => item.taskId === 'TASK-done'), false); + assert.equal(result.tasks.find((item) => item.taskId === 'TASK-waiting').expectedWaiting, true); + const recent = result.tasks.find((item) => item.taskId === 'TASK-recent'); + assert.equal(recent.status, 'recent'); + assert.equal(recent.lastActivity.commit, commit); + assert.deepEqual(recent.closureIntent, [commit]); + assert.equal(result.closurePerformed, false); + assert.equal(JSON.stringify(tasks), before); + assert.equal(input.git(['status', '--porcelain']), ''); +}); + +test('SCN-backlog-activity-coverage: partial scans and missing refs cannot prove neglect, and future dates do not fake progress', () => { + const input = fixture(); + input.git(['commit', '--allow-empty', '-m', 'Task: TASK-progress'], '2026-09-29T00:00:00Z'); + input.git(['commit', '--allow-empty', '-m', 'Unrelated newest commit'], '2026-09-30T00:00:00Z'); + const tasks = [task('TASK-progress'), task('TASK-owner', { ownerRequested: true })]; + const options = { repositoryRoot: input.root, branches: ['main'], tasks, now: new Date('2026-09-30T12:00:00Z') }; + const bounded = observeTaskActivity({ ...options, maxCommits: 1 }); + assert.equal(bounded.complete, false); + assert.equal(bounded.truncated, true); + assert.equal(bounded.tasks.every((item) => item.status === 'unknown'), true); + assert.equal(bounded.stale.length, 0); + const missing = observeTaskActivity({ ...options, branches: ['main', 'missing-worker'] }); + assert.equal(missing.complete, false); + assert.deepEqual(missing.unavailableBranches, ['missing-worker']); + assert.equal(missing.tasks.find((item) => item.taskId === 'TASK-owner').status, 'unknown'); + input.git(['commit', '--allow-empty', '-m', 'Task: TASK-owner\n\nCloses: TASK-unknown'], '2026-10-02T00:00:00Z'); + const future = observeTaskActivity(options); + assert.equal(future.futureCommits.length, 1); + assert.equal(future.tasks.find((item) => item.taskId === 'TASK-owner').status, 'stale'); + assert.deepEqual(future.unknownReferences, ['TASK-unknown']); + assert.throws(() => observeTaskActivity({ ...options, maxCommits: 0 }), (error) => error.code === 'BACKLOG_ACTIVITY_INPUT_INVALID'); +}); + +test('SCN-commit-closure-intent: only exact standalone closure trailers describe intent and never execute message text', () => { + const result = commitTaskReferences('Update TASK-first\n\nCloses: TASK-first, TASK-second\nQuoted example: Closes: TASK-third\nCloses: TASK-fourth then run a command'); + assert.deepEqual(result.closes, ['TASK-first', 'TASK-second']); + assert.deepEqual(result.references, ['TASK-first', 'TASK-second', 'TASK-third', 'TASK-fourth']); +}); diff --git a/test/fleet/backlog.test.mjs b/test/fleet/backlog.test.mjs index dc976a2..1e06129 100644 --- a/test/fleet/backlog.test.mjs +++ b/test/fleet/backlog.test.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; -import { execFileSync } from 'node:child_process'; -import { mkdtempSync, unlinkSync, writeFileSync } from 'node:fs'; +import { execFile, execFileSync, spawnSync } from 'node:child_process'; +import { mkdirSync, mkdtempSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; +import { promisify } from 'node:util'; import { tmpdir } from 'node:os'; import { basename, join } from 'node:path'; import test from 'node:test'; @@ -13,13 +14,22 @@ import { inspectRepository } from '../../src/kernel/git.mjs'; import { installProject } from '../../src/kernel/install.mjs'; import { createWorktrees } from '../../src/kernel/worktrees.mjs'; import { callTorchTool } from '../../src/mcp/tools.mjs'; +import { CheckService } from '../../src/checks/service.mjs'; +import { ResourceService } from '../../src/resources/service.mjs'; +import { IntegrationService } from '../../src/integration/service.mjs'; +import { beginWorktreeGuard, endWorktreeGuard } from '../../src/convergence/service.mjs'; -function fixture() { +function fixture({ withPeer = false } = {}) { const root = mkdtempSync(join(tmpdir(), 'torch-backlog-')); 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, 'app.js'), 'export const app = true;\n'); + if (withPeer) { + mkdirSync(join(root, 'test')); + writeFileSync(join(root, 'test', 'app.test.js'), '// independent verification surface\n'); + writeFileSync(join(root, 'test', 'integration.test.js'), '// independent integration surface\n'); + } execFileSync('git', ['-C', root, 'add', '.']); execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']); let repository = inspectRepository(root); @@ -34,9 +44,189 @@ function fixture() { repository = inspectRepository(root); createWorktrees({ repository, parentOverride: join(tmpdir(), `${basename(root)}-worktrees`) }); const mainStatus = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8' }); - return { root, env, worker: proposal.domains[0].id, mainStatus }; + return { root, env, worker: proposal.domains.find((domain) => domain.id !== 'qa').id, + peer: proposal.domains.find((domain) => domain.id === 'qa')?.id, mainStatus }; } +function readyTask(backlog, title, affectedDomains, priority = 'normal', dependencies = []) { + const created = backlog.create({ actorId: 'session-manager', title, description: title, + acceptanceCriteria: ['Observed and verified.'], affectedDomains, priority, dependencies }); + return backlog.transition({ taskId: created.id, actorId: 'session-manager', to: 'ready', expectedRevision: created.revision }); +} + +function allowSelfClaim(context, areas) { + const path = join(context.root, '.torch', 'torch.yaml'); + const config = JSON.parse(readFileSync(path, 'utf8')); + config.backlog = { self_claim: { enabled: true, areas } }; + writeFileSync(path, JSON.stringify(config)); + return { config, path }; +} + +test('SCN-backlog-self-claim: approved routed work is claimed in priority order, resumes first and never grants manager authority', () => { + const context = fixture(); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control }); + const low = readyTask(backlog, 'Earlier low priority', [context.worker], 'low'); + const high = readyTask(backlog, 'Owner prioritized work', [context.worker], 'high'); + assert.equal(backlog.next({ areaId: context.worker }).task.id, high.id); + const refused = backlog.claimNext({ areaId: context.worker }); + assert.equal(refused.mutationPerformed, false); + assert.equal(refused.blockers.some((blocker) => blocker.code === 'BACKLOG_SELF_CLAIM_DISABLED'), true); + assert.equal(backlog.get(high.id).state, 'ready'); + const { config, path } = allowSelfClaim(context, [context.worker]); + const generic = readyTask(backlog, 'Unrouted owner request', [], 'urgent'); + const dependency = readyTask(backlog, 'Unfinished dependency', [], 'normal'); + const gated = readyTask(backlog, 'Dependent work', [context.worker], 'urgent', [dependency.id]); + const options = { actorId: context.worker, backlogService: backlog }; + assert.equal(callTorchTool(control, 'torch_plan_backlog_claim', {}, options).task.id, high.id); + assert.equal(backlog.get(high.id).state, 'ready', 'preview does not assign'); + const claimed = callTorchTool(control, 'torch_claim_next_backlog_task', {}, options); + assert.equal(claimed.disposition, 'claimed'); + assert.equal(claimed.task.owner, context.worker); + assert.equal(claimed.task.state, 'assigned'); + assert.equal(claimed.task.history.at(-1).actorId, context.worker); + assert.equal(backlog.get(generic.id).state, 'ready'); + assert.equal(backlog.get(gated.id).state, 'ready'); + assert.equal(control.readMessages({ recipient: context.worker }).some((message) => message.body.includes(`Assigned ${high.id}`)), false, + 'routine self-claim does not generate acknowledgement-only messages'); + assert.throws(() => backlog.transition({ taskId: low.id, actorId: context.worker, to: 'assigned', + owner: context.worker, expectedRevision: low.revision, selfClaim: true }), + (error) => error.code === 'BACKLOG_AUTHORITY_REQUIRED'); + config.backlog.self_claim.enabled = false; + writeFileSync(path, JSON.stringify(config)); + const resume = backlog.claimNext({ areaId: context.worker }); + assert.equal(resume.disposition, 'resume'); + assert.equal(resume.task.id, high.id); + assert.equal(resume.mutationPerformed, false); + assert.equal(control.readAudit({ actorId: context.worker }).filter((event) => event.operation === 'backlog.self-claim').length, 1); + assert.throws(() => backlog.planClaim({ areaId: 'session-manager' }), (error) => error.code === 'BACKLOG_OWNER_INVALID'); + backlog.transition({ taskId: high.id, actorId: 'session-manager', to: 'cancelled', expectedRevision: claimed.task.revision }); + assert.equal(backlog.claimNext({ areaId: context.worker }).blockers.some((blocker) => blocker.code === 'BACKLOG_SELF_CLAIM_DISABLED'), true); + control.close(); +}); + +test('SCN-backlog-self-claim-readiness: dirty work, active guards, queued tests, resources, approvals and integration prevent new claims', () => { + const context = fixture({ withPeer: true }); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control }); + const ready = readyTask(backlog, 'Routed work', [context.worker]); + const { config, path } = allowSelfClaim(context, [context.worker]); + config.checks.push({ id: 'frozen', command: 'node', args: ['-e', 'process.exit(0)'], + snapshot: { paths: ['app.js', 'build-sha'], source_commit_file: 'build-sha' } }); + config.resources.push({ id: 'browser', capacity: 1, queue: 'fifo' }); + writeFileSync(path, JSON.stringify(config)); + const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control }); + const worktree = checks.worktrees.get(context.worker).path; + const blockedBy = (code) => { + const result = backlog.claimNext({ areaId: context.worker }); + assert.equal(result.disposition, 'blocked'); + assert.equal(result.blockers.some((blocker) => blocker.code === code), true, JSON.stringify(result.blockers)); + assert.equal(backlog.get(ready.id).state, 'ready'); + }; + writeFileSync(join(worktree, 'unsaved.js'), '// unfinished\n'); + blockedBy('WORKTREE_DIRTY'); + unlinkSync(join(worktree, 'unsaved.js')); + beginWorktreeGuard(control, { areaId: context.worker, type: 'measurement', reason: 'Outstanding measurement.' }); + blockedBy('WORKTREE_GUARD_ACTIVE'); + endWorktreeGuard(control, { areaId: context.worker, type: 'measurement' }); + writeFileSync(join(worktree, '.gitignore'), 'build-sha\n'); + execFileSync('git', ['-C', worktree, 'add', '.gitignore']); + execFileSync('git', ['-C', worktree, 'commit', '-m', 'ignore marker']); + writeFileSync(join(worktree, 'build-sha'), execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()); + const prepared = checks.prepare({ checkId: 'frozen', areaId: context.worker }); + blockedBy('CHECKS_PENDING'); + checks.cancelPrepared({ preparedId: prepared.id, areaId: context.worker }); + const resources = new ResourceService({ repositoryRoot: context.root, controlPlane: control }); + resources.acquire({ resourceId: 'browser', areaId: context.worker }); + blockedBy('RESOURCE_HELD'); + resources.release({ resourceId: 'browser', areaId: context.worker }); + resources.acquire({ resourceId: 'browser', areaId: context.peer }); + resources.acquire({ resourceId: 'browser', areaId: context.worker }); + blockedBy('RESOURCE_WAITING'); + resources.cancel({ resourceId: 'browser', areaId: context.worker }); + resources.release({ resourceId: 'browser', areaId: context.peer }); + const approval = control.requestApproval({ requester: context.worker, approver: 'owner', title: 'Owner decision', summary: 'Need a decision.' }); + blockedBy('APPROVAL_PENDING'); + control.decideApproval({ approvalId: approval.id, decidedBy: 'owner', decision: 'rejected', expectedRevision: approval.revision, note: 'Do not proceed.' }); + assert.equal(backlog.planClaim({ areaId: context.worker }).canProceed, true); + const integration = new IntegrationService({ repositoryRoot: context.root, controlPlane: control, checkService: checks }); + integration.request({ areaId: context.worker }); + blockedBy('INTEGRATION_PENDING'); + control.close(); +}); + +test('SCN-backlog-self-claim-race: competing specialist processes cannot assign two tasks to one owner', async () => { + const context = fixture(); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control }); + readyTask(backlog, 'First work', [context.worker]); + readyTask(backlog, 'Second work', [context.worker]); + allowSelfClaim(context, [context.worker]); + const script = `import { openControlPlane } from ${JSON.stringify(new URL('../../src/control-plane/service.mjs', import.meta.url).href)}; + import { createBacklogService } from ${JSON.stringify(new URL('../../src/backlog/service.mjs', import.meta.url).href)}; + const control=openControlPlane({repositoryRoot:process.cwd(),env:process.env}); + try { console.log(JSON.stringify(createBacklogService({repositoryRoot:process.cwd(),controlPlane:control}).claimNext({areaId:${JSON.stringify(context.worker)}}))); } + catch(error) { console.log(JSON.stringify({error:error.code})); } finally { control.close(); }`; + const results = await Promise.all([0, 1].map(() => promisify(execFile)(process.execPath, + ['--input-type=module', '-e', script], { cwd: context.root, env: context.env }))); + const outputs = results.map((result) => JSON.parse(result.stdout)); + assert.equal(outputs.filter((output) => output.disposition === 'claimed').length, 1); + assert.equal(outputs.every((output) => ['claimed', 'resume'].includes(output.disposition) + || output.error === 'BACKLOG_ASSIGNMENT_LOCKED'), true, JSON.stringify(outputs)); + assert.equal(backlog.list({ owner: context.worker }).filter((task) => task.state === 'assigned').length, 1); + assert.equal(backlog.list({ state: 'ready' }).length, 1); + assert.equal(backlog.claimNext({ areaId: context.worker }).disposition, 'resume'); + control.close(); +}); + +test('SCN-cli-backlog-self-claim: dry-run, explicit action and identity-bound MCP preserve policy and revisions', () => { + const context = fixture({ withPeer: true }); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control }); + const task = readyTask(backlog, 'CLI work', [context.worker]); + allowSelfClaim(context, [context.worker]); + const cli = (args) => spawnSync(process.execPath, [new URL('../../bin/torch.mjs', import.meta.url).pathname, + 'backlog', 'claim-next', '--area', context.worker, ...args, '--json'], { cwd: context.root, env: context.env, encoding: 'utf8' }); + assert.equal(cli([]).status, 2); + assert.equal(backlog.planClaim({ areaId: context.peer }).blockers.some((blocker) => + blocker.code === 'BACKLOG_SELF_CLAIM_DISABLED'), true); + const preview = cli(['--dry-run']); + assert.equal(preview.status, 0, preview.stdout || preview.stderr); + assert.equal(JSON.parse(preview.stdout).task.id, task.id); + assert.equal(backlog.get(task.id).state, 'ready'); + assert.throws(() => callTorchTool(control, 'torch_claim_next_backlog_task', { area_id: context.worker }, + { actorId: context.peer, backlogService: backlog }), (error) => error.code === 'FLEET_IDENTITY_MISMATCH'); + const result = cli(['--yes']); + assert.equal(result.status, 0, result.stdout || result.stderr); + assert.equal(JSON.parse(result.stdout).disposition, 'claimed'); + assert.equal(backlog.get(task.id).revision, task.revision + 1); + assert.equal(JSON.parse(cli(['--yes']).stdout).disposition, 'resume'); + control.close(); +}); + +test('SCN-backlog-blocked-resume-serialization: a resolved blocked task cannot become a second active assignment', () => { + const context = fixture(); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control }); + const first = readyTask(backlog, 'First work', [context.worker]); + readyTask(backlog, 'Second work', [context.worker]); + allowSelfClaim(context, [context.worker]); + const claimed = backlog.claimNext({ areaId: context.worker }); + assert.equal(claimed.task.id, first.id); + const blocked = backlog.transition({ taskId: first.id, actorId: context.worker, + to: 'blocked', expectedRevision: claimed.task.revision, blockedReason: 'External dependency.' }); + const second = backlog.claimNext({ areaId: context.worker }); + assert.equal(second.disposition, 'claimed'); + assert.notEqual(second.task.id, first.id); + assert.throws(() => backlog.transition({ taskId: first.id, actorId: context.worker, + to: 'in_progress', expectedRevision: blocked.revision }), (error) => error.code === 'BACKLOG_OWNER_BUSY'); + assert.equal(backlog.get(first.id).state, 'blocked'); + backlog.transition({ taskId: second.task.id, actorId: 'session-manager', to: 'cancelled', expectedRevision: second.task.revision }); + assert.equal(backlog.transition({ taskId: first.id, actorId: context.worker, + to: 'in_progress', expectedRevision: blocked.revision }).state, 'in_progress'); + control.close(); +}); + test('SCN-backlog-health: unusual queue conditions are reported without repair', () => { const base = { schema: 'torch.dev/backlog-item/v1alpha1', priority: 'normal', affectedDomains: ['runtime'], @@ -228,3 +418,132 @@ test('SCN-backlog-lifecycle: tracked tasks enforce authority, revisions, depende ); control.close(); }); + +test('SCN-backlog-classification: feature and milestone labels are revisioned manager metadata, not another queue', () => { + const context = fixture(); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control }); + const task = backlog.create({ + actorId: 'session-manager', title: 'Build terrain traversal', description: 'Connect world data to movement.', + affectedDomains: [context.worker], acceptanceCriteria: ['Traversal test passes.'], + feature: 'World pipeline', milestone: 'Alpha', + }); + assert.equal(task.feature, 'World pipeline'); + assert.equal(task.milestone, 'Alpha'); + assert.equal(backlog.list()[0].state, 'proposed'); + assert.throws( + () => backlog.classify({ + taskId: task.id, actorId: context.worker, expectedRevision: task.revision, + feature: 'Unauthorized change', reason: 'Worker cannot own program planning.', + }), + (error) => error.code === 'BACKLOG_AUTHORITY_REQUIRED', + ); + const classified = callTorchTool(control, 'torch_classify_backlog_task', { + task_id: task.id, expected_revision: task.revision, + feature: 'World generation', reason: 'Group recurring terrain and traversal tasks.', + }, { actorId: 'session-manager', backlogService: backlog }); + assert.equal(classified.mutationPerformed, true); + assert.equal(classified.feature, 'World generation'); + assert.equal(classified.milestone, 'Alpha'); + assert.equal(classified.revision, task.revision + 1); + assert.equal(classified.state, 'proposed'); + assert.equal(classified.owner, null); + assert.equal(classified.classificationHistory[0].from.feature, 'World pipeline'); + assert.throws( + () => backlog.classify({ + taskId: task.id, actorId: 'session-manager', expectedRevision: task.revision, + milestone: 'Beta', reason: 'Stale update.', + }), + (error) => error.code === 'BACKLOG_REVISION_CONFLICT', + ); + const cleared = backlog.classify({ + taskId: task.id, actorId: 'session-manager', expectedRevision: classified.revision, + clearMilestone: true, reason: 'Move this task out of the retired milestone.', + }); + assert.equal(cleared.feature, 'World generation'); + assert.equal(cleared.milestone, null); + assert.equal(cleared.revision, classified.revision + 1); + assert.throws( + () => backlog.classify({ + taskId: task.id, actorId: 'session-manager', expectedRevision: cleared.revision, + feature: 'World generation', clearFeature: true, reason: 'Conflicting request.', + }), + (error) => error.code === 'INVALID_BACKLOG_CLASSIFICATION', + ); + assert.equal(control.readAudit().some((entry) => entry.operation === 'backlog.classify'), true); + control.close(); +}); + +test('SCN-owner-backlog-priority: only the owner may make a revision-checked priority-only change', () => { + const context = fixture(); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control }); + const task = backlog.create({ + actorId: 'session-manager', title: 'Improve navigation cache', description: 'Reduce route recomputation.', + affectedDomains: [context.worker], acceptanceCriteria: ['Cache validity is tested.'], priority: 'normal', + }); + assert.throws(() => backlog.planPriorityChange({ + taskId: task.id, actorId: context.worker, expectedRevision: task.revision, + priority: 'urgent', reason: 'Specialist cannot change owner priority.', + }), (error) => error.code === 'OWNER_AUTHORITY_REQUIRED'); + const plan = backlog.planPriorityChange({ + taskId: task.id, actorId: 'owner', expectedRevision: task.revision, + priority: 'high', reason: 'Unblock the current integration milestone.', + }); + assert.equal(plan.mutationPerformed, false); + assert.equal(plan.from, 'normal'); + assert.equal(plan.to, 'high'); + assert.equal(backlog.get(task.id).priority, 'normal'); + const changed = backlog.setPriority({ + taskId: task.id, actorId: 'owner', expectedRevision: task.revision, + priority: 'high', reason: plan.reason, + }); + assert.equal(changed.priority, 'high'); + assert.equal(changed.revision, task.revision + 1); + assert.equal(changed.state, task.state); + assert.equal(changed.owner, task.owner); + assert.deepEqual(changed.evidence, task.evidence); + assert.deepEqual(changed.dependencies, task.dependencies); + assert.equal(changed.priorityHistory[0].reason, plan.reason); + assert.throws(() => backlog.setPriority({ + taskId: task.id, actorId: 'owner', expectedRevision: task.revision, + priority: 'urgent', reason: 'Stale preview must not apply.', + }), (error) => error.code === 'BACKLOG_REVISION_CONFLICT'); + assert.equal(control.readAudit().some((entry) => entry.operation === 'backlog.priority' + && entry.entityId === task.id), true); + control.close(); +}); + +test('SCN-owner-backlog-create: the owner may propose audited work but cannot assign or dispatch it', () => { + const context = fixture(); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const backlog = createBacklogService({ repositoryRoot: context.root, controlPlane: control }); + const input = { + title: 'Expose a new project decision', description: 'Give the owner a bounded, reviewable task path.', + priority: 'high', affectedDomains: [context.worker], dependencies: [], + acceptanceCriteria: ['The task remains proposed and unassigned.'], feature: '', milestone: '', + }; + assert.throws(() => backlog.planOwnerCreate({ ...input, actorId: context.worker }), + (error) => error.code === 'OWNER_AUTHORITY_REQUIRED'); + const before = backlog.list().length; + assert.throws(() => backlog.create({ ...input, actorId: 'owner' }), + (error) => ['BACKLOG_AUTHORITY_REQUIRED', 'UNKNOWN_FLEET_IDENTITY'].includes(error.code)); + const plan = backlog.planOwnerCreate({ ...input, actorId: 'owner' }); + assert.equal(plan.mutationPerformed, false); + assert.equal(plan.task.state, 'proposed'); + assert.equal(plan.task.owner, null); + assert.deepEqual(plan.task.affectedDomains, [context.worker]); + assert.equal(backlog.list().length, before); + + const created = backlog.createOwner({ ...input, actorId: 'owner' }); + assert.equal(created.state, 'proposed'); + assert.equal(created.owner, null); + assert.equal(created.feature, null); + assert.equal(created.history[0].actorId, 'owner'); + assert.equal(backlog.get(created.id).state, 'proposed'); + assert.equal(control.readAudit().some((entry) => entry.actorId === 'owner' + && entry.operation === 'backlog.create' && entry.entityId === created.id), true); + assert.throws(() => backlog.createOwner({ ...input, actorId: context.worker }), + (error) => error.code === 'OWNER_AUTHORITY_REQUIRED'); + control.close(); +}); diff --git a/test/fleet/bootstrap-acceptance.test.mjs b/test/fleet/bootstrap-acceptance.test.mjs index 281437c..e1603b0 100644 --- a/test/fleet/bootstrap-acceptance.test.mjs +++ b/test/fleet/bootstrap-acceptance.test.mjs @@ -80,7 +80,7 @@ test('SCN-bootstrap-acceptance: one installed fixture proves the complete organi const adapters = new Map([['claude', adapter]]); const up = planFleetUp({ repositoryRoot: context.root, controlPlane: control, adapters, fresh: true }); assert.equal(up.canProceed, true, JSON.stringify(up.blockers)); - assert.equal(up.actions.at(-1).areaId, 'session-manager'); + assert.equal(up.actions[0].areaId, 'session-manager'); let runtime = 0; const started = startFleet({ plan: up, controlPlane: control, adapters, @@ -97,6 +97,10 @@ test('SCN-bootstrap-acceptance: one installed fixture proves the complete organi assert.match(serverPrompt, /## Authority[\s\S]*Integration authority does not grant release or deployment authority/); assert.match(serverPrompt, /## Project invariants[\s\S]*Repository and TORCH durable state outrank conversation memory/); assert.match(serverPrompt, /## Initial backlog[\s\S]*No task is implied by session creation/); + assert.match(serverPrompt, /Name the exact TORCH task ID in progress commits/); + assert.match(serverPrompt, /Closes: TASK-id[\s\S]*intent, not authority[\s\S]*confirm landed integration plus completed backlog state/); + assert.match(serverPrompt, /torch_publish_artifact[\s\S]*full commit SHA/); + assert.match(serverPrompt, /Treat owner feedback[\s\S]*durable message/); for (const areaId of ['server', 'client']) { const message = control.sendMessage({ sender: 'session-manager', recipient: areaId, body: `Own ${areaId} work.` }); diff --git a/test/fleet/canonical-fetch.test.mjs b/test/fleet/canonical-fetch.test.mjs new file mode 100644 index 0000000..228414a --- /dev/null +++ b/test/fleet/canonical-fetch.test.mjs @@ -0,0 +1,117 @@ +import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { fetchCanonicalObjects, classifyFetchFailure } from '../../src/canonical/fetch.mjs'; +import { createLocalCanonical } from '../../src/canonical/local.mjs'; +import { openControlPlane } from '../../src/control-plane/service.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'; + +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'torch-canonical-fetch-')); + const env = { ...process.env, XDG_DATA_HOME: mkdtempSync(join(tmpdir(), 'torch-fetch-state-')) }; + const git = (path, args) => execFileSync('git', ['-C', path, ...args], { encoding: 'utf8' }).trim(); + git(root, ['init', '-b', 'main']); + git(root, ['config', 'user.email', 'test@example.invalid']); + git(root, ['config', 'user.name', 'Test']); + writeFileSync(join(root, 'app.js'), 'export const value=1;'); + git(root, ['add', '.']); + git(root, ['commit', '-m', 'source']); + const repository = inspectRepository(root); + const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) }); + proposal.review = { status: 'approved', reviewedAt: '2026-09-30', reviewedBy: 'owner', notes: [] }; + installProject({ repository, proposal, env, projectId: 'fetch-fixture' }); + git(root, ['add', '.torch']); + git(root, ['commit', '-m', 'Install fixture']); + const canonical = createLocalCanonical({ repositoryRoot: root }); + git(root, ['push', 'torch-canonical', 'main']); + const producer = mkdtempSync(join(tmpdir(), 'torch-fetch-producer-')); + git(producer, ['clone', '--branch', 'main', canonical.path, '.']); + git(producer, ['config', 'user.email', 'test@example.invalid']); + git(producer, ['config', 'user.name', 'Test']); + writeFileSync(join(producer, 'remote.js'), 'export const remote=2;'); + git(producer, ['add', '.']); + git(producer, ['commit', '-m', 'remote source']); + git(producer, ['push', 'origin', 'main']); + const commit = git(producer, ['rev-parse', 'HEAD']); + const control = openControlPlane({ repositoryRoot: root, env }); + return { root, env, git, commit, control }; +} + +test('SCN-canonical-fetch: transient object import retries are bounded, recorded, private and never move branches or publish', () => { + const f = fixture(); + try { + const refs = f.git(f.root, ['show-ref']); + const status = f.git(f.root, ['status', '--porcelain']); + let calls = 0; + const result = fetchCanonicalObjects({ repositoryRoot: f.root, controlPlane: f.control, + executor: (root, args, timeout) => { + calls += 1; + assert.equal(args[0], 'fetch'); + assert.ok(args.includes('--refmap=')); + assert.ok(args.includes('--no-write-fetch-head')); + assert.equal(args.at(-1), f.commit); + if (calls === 1) return { status: 128, stderr: 'connection reset https://secret:password@example.invalid' }; + return spawnSync('git', ['-C', root, ...args], { encoding: 'utf8', timeout }); + } }); + assert.equal(result.succeeded, true); + assert.equal(calls, 2); + assert.equal(result.attempts[0].classification, 'transient-network'); + assert.equal(result.attempts[1].state, 'succeeded'); + assert.equal(f.git(f.root, ['show-ref']), refs); + assert.equal(f.git(f.root, ['status', '--porcelain']), status); + f.git(f.root, ['cat-file', '-e', `${f.commit}^{commit}`]); + assert.doesNotMatch(JSON.stringify(result), /password|secret/); + assert.equal(f.control.database.prepare('SELECT state FROM canonical_fetches WHERE id = ?').get(result.id).state, 'succeeded'); + assert.equal(f.control.database.prepare('SELECT COUNT(*) AS n FROM canonical_fetch_attempts').get().n, 2); + } finally { f.control.close(); } +}); + +test('SCN-canonical-fetch-boundaries: authentication, unknown outcomes, bad limits and non-owner actors cannot create unsafe retries', () => { + const f = fixture(); + try { + for (const attempts of [0, 6, 1.5]) assert.throws(() => fetchCanonicalObjects({ repositoryRoot: f.root, + controlPlane: f.control, attempts }), (error) => error.code === 'CANONICAL_FETCH_INPUT_INVALID'); + assert.throws(() => fetchCanonicalObjects({ repositoryRoot: f.root, controlPlane: f.control, actorId: 'session-manager' })); + for (const stderr of ['Permission denied', 'Unrecognized failure']) { + let calls = 0; + const result = fetchCanonicalObjects({ repositoryRoot: f.root, controlPlane: f.control, + executor: () => { calls += 1; return { status: 128, stderr }; } }); + assert.equal(result.succeeded, false); + assert.equal(calls, 1); + } + let calls = 0; + const exhausted = fetchCanonicalObjects({ repositoryRoot: f.root, controlPlane: f.control, attempts: 3, + executor: () => { calls += 1; return { status: 128, stderr: 'connection reset' }; } }); + assert.equal(calls, 3); + assert.equal(exhausted.state, 'failed'); + assert.equal(classifyFetchFailure({ status: 128, stderr: 'Permission denied; connection reset' }), 'permanent'); + assert.equal(classifyFetchFailure({ status: null, error: { code: 'ETIMEDOUT' } }), 'transient-network'); + const cli = spawnSync(process.execPath, [new URL('../../bin/torch.mjs', import.meta.url).pathname, + 'forge', 'fetch', '--json'], { cwd: f.root, env: f.env, encoding: 'utf8' }); + assert.notEqual(cli.status, 0); + assert.equal(JSON.parse(cli.stdout).error, 'APPROVAL_REQUIRED'); + const confirmed = spawnSync(process.execPath, [new URL('../../bin/torch.mjs', import.meta.url).pathname, + 'forge', 'fetch', '--yes', '--attempts', '1', '--json'], { cwd: f.root, env: f.env, encoding: 'utf8' }); + assert.equal(confirmed.status, 0, confirmed.stdout || confirmed.stderr); + assert.equal(JSON.parse(confirmed.stdout).succeeded, true); + const status = spawnSync(process.execPath, [new URL('../../bin/torch.mjs', import.meta.url).pathname, + 'forge', 'fetch', 'status', '--json'], { cwd: f.root, env: f.env, encoding: 'utf8' }); + assert.equal(status.status, 0, status.stdout || status.stderr); + assert.equal(JSON.parse(status.stdout).fetches[0].state, 'succeeded'); + let policyCalls = 0; + const changed = fetchCanonicalObjects({ repositoryRoot: f.root, controlPlane: f.control, + executor: () => { + policyCalls += 1; + f.git(f.root, ['remote', 'set-url', 'torch-canonical', '/unavailable/changed-remote']); + return { status: 128, stderr: 'connection reset' }; + } }); + assert.equal(changed.state, 'policy-changed'); + assert.equal(policyCalls, 1, 'a changed destination must stop another attempt'); + } finally { f.control.close(); } +}); diff --git a/test/fleet/check-conditions.test.mjs b/test/fleet/check-conditions.test.mjs new file mode 100644 index 0000000..e8b9ec7 --- /dev/null +++ b/test/fleet/check-conditions.test.mjs @@ -0,0 +1,123 @@ +import assert from 'node:assert/strict'; +import { spawnSync } 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 { executeConditionedCheck } from '../../src/checks/conditions.mjs'; + +function fixture() { + const cwd = mkdtempSync(join(tmpdir(), 'torch-test-conditions-')); + const subject = { commit: 'a'.repeat(40), inputDigest: 'b'.repeat(64) }; + const report = { + schema: 'torch.dev/check-conditions/v1alpha1', subject: { ...subject }, + conditions: { 'clock-moving': true, 'gpu-active': true, visible: true, invincible: false }, + }; + writeFileSync(join(cwd, 'report.json'), JSON.stringify(report)); + const definition = { + command: 'node', args: ['-e', 'require("node:fs").writeFileSync("measured", "yes"); console.log("measurement finished");'], + conditions: { + command: 'node', args: ['-e', 'console.log(require("node:fs").readFileSync("report.json","utf8"));'], + required: [ + { id: 'clock-moving', equals: true }, { id: 'gpu-active', equals: true }, + { id: 'visible', equals: true }, { id: 'invincible', equals: false }, + ], + }, + }; + const options = { cwd, subject, definition, clock: () => new Date('2026-09-30T06:00:00Z'), + executor: (command, args, settings) => spawnSync(command, args, settings) }; + return { ...options, report }; +} + +test('SCN-check-conditions-contract: valid conditions precede measurements and are checked again afterward', () => { + const input = fixture(); + const output = executeConditionedCheck(input); + assert.equal(output.invalidReason, null); + assert.equal(output.result.status, 0); + assert.equal(output.conditions.before.valid, true); + assert.equal(output.conditions.after.valid, true); + assert.equal(output.conditions.provenance, 'project-reported'); + assert.equal(existsSync(join(input.cwd, 'measured')), true); + assert.equal(output.result.stdout.split('\n')[0].startsWith('TORCH_CHECK_CONDITIONS '), true); + assert.equal(output.result.stdout.includes('measurement finished'), true); +}); + +test('SCN-check-measurement-subject: real measurement commands receive the same exact identity as probes, even without conditions', () => { + for (const conditioned of [true, false]) { + const input = fixture(); + if (!conditioned) delete input.definition.conditions; + input.definition.args = ['-e', 'console.log(JSON.stringify({commit:process.env.TORCH_CHECK_COMMIT,inputDigest:process.env.TORCH_CHECK_INPUT_DIGEST}));']; + const output = executeConditionedCheck(input); + assert.equal(output.result.status, 0); + assert.equal(output.invalidReason, null); + const measured = output.result.stdout.split('\n').find((line) => line.startsWith('{')); + assert.deepEqual(JSON.parse(measured), input.subject); + } +}); + +test('SCN-check-conditions-fail-closed: false, absent, malformed and wrong-subject reports never execute measurements', () => { + const variants = [ + (report) => { report.conditions['clock-moving'] = false; }, + (report) => { delete report.conditions.visible; }, + (report) => { report.conditions['gpu-active'] = 'true'; }, + (report) => { report.conditions.invincible = true; }, + (report) => { report.subject.commit = 'c'.repeat(40); }, + (report) => { report.subject.inputDigest = 'c'.repeat(64); }, + (report) => { report.schema = 'unknown'; }, + ]; + for (const change of variants) { + const input = fixture(); + change(input.report); + writeFileSync(join(input.cwd, 'report.json'), JSON.stringify(input.report)); + const output = executeConditionedCheck(input); + assert.equal(output.invalidReason, 'test-conditions-invalid-before'); + assert.equal(output.measurementsExecuted, false); + assert.equal(output.conditions.before.valid, false); + assert.equal(existsSync(join(input.cwd, 'measured')), false); + } + for (const raw of ['not-json', '{}', 'x'.repeat(65_537)]) { + const input = fixture(); + writeFileSync(join(input.cwd, 'report.json'), raw); + const output = executeConditionedCheck(input); + assert.equal(output.measurementsExecuted, false); + assert.equal(existsSync(join(input.cwd, 'measured')), false); + } + const failed = fixture(); + failed.definition.conditions.args = ['-e', 'process.exit(9)']; + assert.equal(executeConditionedCheck(failed).conditions.before.reason, 'condition-probe-failed'); +}); + +test('SCN-check-condition-drift: a green measurement cannot conceal changing test conditions', () => { + const input = fixture(); + input.definition.args = ['-e', 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync("report.json"));r.conditions.visible=false;fs.writeFileSync("report.json",JSON.stringify(r));console.log("green measurements");']; + const output = executeConditionedCheck(input); + assert.equal(output.result.status, 0); + assert.equal(output.invalidReason, 'test-conditions-invalid-after'); + assert.equal(output.conditions.after.reason, 'condition-mismatch:visible'); + assert.equal(JSON.parse(readFileSync(join(input.cwd, 'report.json'))).conditions.visible, false); +}); + +test('SCN-check-runtime-continuity: reported runtime replacement or disappearance invalidates a green result', () => { + for (const replacement of ['second-runtime', null]) { + const input = fixture(); + input.report.runtimeId = 'first-runtime'; + writeFileSync(join(input.cwd, 'report.json'), JSON.stringify(input.report)); + input.definition.args = ['-e', `const fs=require("node:fs");const r=JSON.parse(fs.readFileSync("report.json")); + ${replacement ? 'r.runtimeId="second-runtime";' : 'delete r.runtimeId;'} + fs.writeFileSync("report.json",JSON.stringify(r));console.log("green");`]; + const output = executeConditionedCheck(input); + assert.equal(output.result.status, 0); + assert.equal(output.conditions.before.valid, true); + assert.equal(output.conditions.after.valid, false); + assert.equal(output.conditions.after.reason, 'condition-runtime-changed'); + assert.equal(output.invalidReason, 'test-conditions-invalid-after'); + } + for (const identity of ['', ' ', 7, 'x'.repeat(257)]) { + const input = fixture(); + input.report.runtimeId = identity; + writeFileSync(join(input.cwd, 'report.json'), JSON.stringify(input.report)); + const output = executeConditionedCheck(input); + assert.equal(output.measurementsExecuted, false); + assert.equal(output.conditions.before.reason, 'condition-runtime-identity-invalid'); + } +}); diff --git a/test/fleet/check-snapshot.test.mjs b/test/fleet/check-snapshot.test.mjs new file mode 100644 index 0000000..49e0efb --- /dev/null +++ b/test/fleet/check-snapshot.test.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, statSync, symlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { captureCheckSnapshot, disposeCheckSnapshot, verifyCheckSnapshot } from '../../src/checks/snapshot.mjs'; + +function fixture() { + const root = mkdtempSync(join(tmpdir(), 'torch-frozen-check-')); + const worktree = join(root, 'worktree'); + const stateRoot = join(root, 'state'); + mkdirSync(worktree); + mkdirSync(stateRoot); + mkdirSync(join(worktree, 'dist')); + const commit = 'a'.repeat(40); + writeFileSync(join(worktree, 'dist', 'source-commit'), `${commit}\n`); + writeFileSync(join(worktree, 'dist', 'index.html'), '

    original build

    '); + return { + root, worktree, stateRoot, commit, + definition: { snapshot: { paths: ['dist'], source_commit_file: 'dist/source-commit' } }, + }; +} + +test('SCN-frozen-check-inputs: queued inputs survive in-place rebuilds, prove identity and have owned cleanup', () => { + const input = fixture(); + const snapshot = captureCheckSnapshot(input); + const frozenPage = join(snapshot.inputRoot, 'dist', 'index.html'); + assert.equal(readFileSync(frozenPage, 'utf8'), '

    original build

    '); + assert.notEqual(statSync(frozenPage).ino, statSync(join(input.worktree, 'dist', 'index.html')).ino, + 'a snapshot must never be a hardlink to mutable build output'); + // Deliberately write in-place: even a non-atomic builder cannot mutate the copy. + writeFileSync(join(input.worktree, 'dist', 'index.html'), '

    new build

    '); + writeFileSync(join(input.worktree, 'dist', 'source-commit'), 'b'.repeat(40)); + assert.deepEqual(verifyCheckSnapshot(snapshot, input), { + verified: true, commit: input.commit, digest: snapshot.digest, + }); + assert.equal(readFileSync(frozenPage, 'utf8'), '

    original build

    '); + assert.throws(() => disposeCheckSnapshot({ ...snapshot, root: input.worktree }, input), + (error) => error.code === 'CHECK_SNAPSHOT_INVALID'); + assert.equal(existsSync(input.worktree), true); + assert.equal(disposeCheckSnapshot(snapshot, input).removed, true); + assert.equal(existsSync(snapshot.root), false); + assert.equal(readFileSync(join(input.worktree, 'dist', 'index.html'), 'utf8'), '

    new build

    '); +}); + +test('SCN-frozen-check-rejection: stale builds, traversal, symlinks and tampered copies cannot qualify', () => { + const input = fixture(); + assert.throws(() => captureCheckSnapshot({ ...input, commit: 'b'.repeat(40) }), + (error) => error.code === 'CHECK_SNAPSHOT_STALE_BUILD'); + for (const path of ['../state', '/tmp', '.', 'dist/../dist', '.git/config']) { + assert.throws(() => captureCheckSnapshot({ + ...input, definition: { snapshot: { ...input.definition.snapshot, paths: [path] } }, + }), (error) => error.code === 'CHECK_SNAPSHOT_INVALID'); + } + symlinkSync(input.stateRoot, join(input.worktree, 'external')); + assert.throws(() => captureCheckSnapshot({ + ...input, definition: { snapshot: { ...input.definition.snapshot, paths: ['dist', 'external'] } }, + }), (error) => error.code === 'CHECK_SNAPSHOT_INVALID'); + const snapshot = captureCheckSnapshot(input); + const frozenPage = join(snapshot.inputRoot, 'dist', 'index.html'); + chmodSync(frozenPage, 0o600); + writeFileSync(frozenPage, 'tampered'); + assert.throws(() => verifyCheckSnapshot(snapshot, input), + (error) => error.code === 'CHECK_SNAPSHOT_INVALID'); + // Cleanup requires ownership, not a passing content check. + disposeCheckSnapshot(snapshot, input); +}); diff --git a/test/fleet/cli-scenarios.test.mjs b/test/fleet/cli-scenarios.test.mjs index 7797ec8..1833193 100644 --- a/test/fleet/cli-scenarios.test.mjs +++ b/test/fleet/cli-scenarios.test.mjs @@ -4,6 +4,8 @@ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileS import { tmpdir } from 'node:os'; import { basename, join } from 'node:path'; import test from 'node:test'; +import { openControlPlane } from '../../src/control-plane/service.mjs'; +import { CANDIDATE_ACCEPTANCE_EVIDENCE } from '../../src/cli.mjs'; const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname; @@ -35,23 +37,8 @@ function releaseCandidate(root, version) { mkdirSync(join(candidateRoot, 'bin'), { recursive: true }); writeFileSync(join(candidateRoot, 'bin', 'torch.mjs'), '#!/usr/bin/env node\n'); chmodSync(join(candidateRoot, 'bin', 'torch.mjs'), 0o755); - writeFileSync(join(candidateRoot, 'acceptance.mjs'), `console.log(${JSON.stringify([ - 'SCN-init-read-only', 'SCN-cli-domain-review', 'SCN-worktree-bootstrap', 'SCN-mixed-runtime', - 'SCN-spec-fleet-design', 'SCN-cli-spec-design', - 'SCN-ai-fleet-bootstrap', - 'SCN-ai-fleet-planning', - 'SCN-fleet-evolution', 'SCN-cli-fleet-evolution', - 'SCN-fleet-boundary-evolution', - 'SCN-cli-lifecycle-surface', - 'SCN-forge-migration', - 'SCN-delivery-lifecycle', - 'SCN-durable-message', 'SCN-ownership-handoff', 'SCN-worktree-purge-safety', - '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', - 'SCN-context-locality', 'SCN-schedule-boundaries', 'SCN-cli-schedules', - ].join('\n'))});\n`); + const acceptanceMarkers = [...new Set(Object.values(CANDIDATE_ACCEPTANCE_EVIDENCE).flat())]; + writeFileSync(join(candidateRoot, 'acceptance.mjs'), `console.log(${JSON.stringify(acceptanceMarkers.join('\n'))});\n`); writeFileSync(join(candidateRoot, 'package.json'), `${JSON.stringify({ name: 'torch-agent-fleet', version, bin: { torch: 'bin/torch.mjs' }, scripts: { @@ -108,6 +95,29 @@ test('SCN-cli-domain-review: a generated proposal must be approved before instal assert.equal(existsSync(join(root, '.torch', 'domain-proposal.approved.json')), true); }); +test('SCN-cli-install-provider-default: generated roles can install with Codex alone or choose a default from mixed adapters', () => { + const root = repo(); + const proposalPath = join(root, 'fleet-proposal.json'); + const generated = run(root, ['domains', '--output', proposalPath, '--json']); + assert.equal(generated.status, 0, generated.stderr || generated.stdout); + const proposal = JSON.parse(readFileSync(proposalPath, 'utf8')); + proposal.review = { status: 'approved', reviewedAt: '2026-09-29T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] }; + writeFileSync(proposalPath, JSON.stringify(proposal)); + const codex = run(root, ['install', '--proposal', proposalPath, '--runtime', 'codex', '--dry-run', '--json']); + assert.equal(codex.status, 0, codex.stderr || codex.stdout); + assert.equal(JSON.parse(codex.stdout).defaultRuntime, 'codex'); + assert.equal(existsSync(join(root, '.torch')), false); + const result = run(root, ['install', '--proposal', proposalPath, '--runtime', 'claude,codex', + '--default-runtime', 'codex', '--yes', '--json']); + assert.equal(result.status, 0, result.stderr || result.stdout); + const config = JSON.parse(readFileSync(join(root, '.torch', 'torch.yaml'), 'utf8')); + assert.equal(config.runtimes.default, 'codex'); + assert.equal(config.session_manager.runtime, 'codex'); + assert.ok(config.domains.every((domain) => domain.runtime === 'codex')); + assert.equal(config.runtimes.codex.model, 'gpt-6-luna'); + assert.equal(config.runtimes.claude.model, 'sonnet'); +}); + test('SCN-cli-lifecycle-surface: runtime selection, selective startup, capture, brief, list, and detach are public', () => { const root = repo(); const proposalPath = join(root, 'fleet-proposal.json'); @@ -173,6 +183,32 @@ test('SCN-cli-lifecycle-surface: runtime selection, selective startup, capture, assert.equal(JSON.parse(unapprovedPurge.stdout).error, 'APPROVAL_REQUIRED'); }); +test('SCN-cli-integration-drain: the FIFO lander requires explicit confirmation and is available as a one-shot command', () => { + const root = repo(); + const proposalPath = join(root, 'fleet-proposal.json'); + assert.equal(run(root, ['domains', '--output', proposalPath, '--json']).status, 0); + const proposal = JSON.parse(readFileSync(proposalPath, 'utf8')); + proposal.review = { + status: 'approved', reviewedAt: '2026-09-29T00:00:00Z', reviewedBy: 'fixture-owner', notes: [], + }; + writeFileSync(proposalPath, `${JSON.stringify(proposal, null, 2)}\n`); + const installed = run(root, ['install', '--proposal', proposalPath, '--yes', '--json']); + assert.equal(installed.status, 0, installed.stderr || installed.stdout); + execFileSync('git', ['-C', root, 'add', '.torch', 'fleet-proposal.json']); + execFileSync('git', ['-C', root, 'commit', '-m', 'install torch for queue test']); + + const before = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); + const unconfirmed = run(root, ['integrate', 'drain', '--area', 'session-manager', '--json']); + assert.equal(unconfirmed.status, 2); + assert.equal(JSON.parse(unconfirmed.stdout).error, 'APPROVAL_REQUIRED'); + assert.equal(execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(), before); + + const drained = run(root, ['integrate', 'drain', '--area', 'session-manager', '--yes', '--json']); + assert.equal(drained.status, 0, drained.stderr || drained.stdout); + assert.equal(JSON.parse(drained.stdout).processed, 0); + assert.equal(execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(), before); +}); + test('SCN-cli-control-plane: CLI messages, acknowledgements, ownership, and status share durable state', () => { const root = repo(); const proposalPath = join(root, 'fleet-proposal.json'); @@ -206,6 +242,39 @@ test('SCN-cli-control-plane: CLI messages, acknowledgements, ownership, and stat assert.equal(ownership.owners.length > 0, true); }); +test('SCN-cli-owner-approval: owners can review scoped requests and must explicitly confirm decisions', () => { + const root = repo(); + const proposalPath = join(root, 'approval-proposal.json'); + assert.equal(run(root, ['domains', '--output', proposalPath, '--json']).status, 0); + const proposal = JSON.parse(readFileSync(proposalPath, 'utf8')); + proposal.review = { status: 'approved', reviewedAt: '2026-09-28T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] }; + writeFileSync(proposalPath, `${JSON.stringify(proposal, null, 2)}\n`); + assert.equal(run(root, ['install', '--proposal', proposalPath, '--yes', '--json']).status, 0); + + const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-data`) }; + const control = openControlPlane({ repositoryRoot: root, env }); + const request = control.requestApproval({ + requester: proposal.domains[0].id, approver: 'owner', title: 'Review scope', + summary: 'The requested work crosses an approved boundary.', task: 'TASK-OWNER-APPROVAL', + }); + control.close(); + + const listed = run(root, ['approvals', 'list', '--area', 'owner', '--json']); + assert.equal(listed.status, 0, listed.stderr || listed.stdout); + assert.equal(JSON.parse(listed.stdout).approvals[0].id, request.id); + const unconfirmed = run(root, [ + 'approvals', 'decide', '--id', request.id, '--by', 'owner', '--decision', 'approved', '--json', + ]); + assert.equal(unconfirmed.status, 2); + assert.equal(JSON.parse(unconfirmed.stdout).error, 'APPROVAL_REQUIRED'); + const confirmed = run(root, [ + 'approvals', 'decide', '--id', request.id, '--by', 'owner', '--decision', 'approved', + '--revision', String(request.revision), '--yes', '--json', + ]); + assert.equal(confirmed.status, 0, confirmed.stderr || confirmed.stdout); + assert.equal(JSON.parse(confirmed.stdout).status, 'approved'); +}); + test('SCN-cli-self-host: candidate acceptance, atomic upgrade, and rollback require explicit approval', () => { const root = repo(); const v1 = releaseCandidate(root, '1.0.0'); @@ -257,10 +326,21 @@ test('SCN-cli-backlog: public commands create, transition, assign, and list trac const created = run(root, [ 'backlog', 'create', '--area', 'session-manager', '--title', 'CLI task', '--description', 'Exercise public lifecycle.', '--accept', 'Assigned to owner', - '--domains', proposal.domains[0].id, '--json', + '--domains', proposal.domains[0].id, '--feature', 'World pipeline', '--milestone', 'Alpha', '--json', ]); assert.equal(created.status, 0, created.stderr || created.stdout); let task = JSON.parse(created.stdout); + assert.equal(task.feature, 'World pipeline'); + assert.equal(task.milestone, 'Alpha'); + const classified = run(root, [ + 'backlog', 'classify', '--task', task.id, '--area', 'session-manager', + '--revision', String(task.revision), '--milestone', 'Beta', + '--reason', 'Resequence this work to the next checkpoint.', '--json', + ]); + assert.equal(classified.status, 0, classified.stderr || classified.stdout); + task = JSON.parse(classified.stdout); + assert.equal(task.feature, 'World pipeline'); + assert.equal(task.milestone, 'Beta'); for (const transition of [ ['ready'], ['assigned', '--owner', proposal.domains[0].id], diff --git a/test/fleet/combatrig-import.test.mjs b/test/fleet/combatrig-import.test.mjs index 5fdc0a1..90bccae 100644 --- a/test/fleet/combatrig-import.test.mjs +++ b/test/fleet/combatrig-import.test.mjs @@ -46,6 +46,12 @@ function fixture() { writeFileSync(join(root, 'docs', 'agents', 'backlog', 'anim-1.json'), JSON.stringify({ id: 'anim-1', area: 'animation', status: 'done', title: 'Animation task', })); + writeFileSync(join(root, 'docs', 'agents', 'backlog', 'mgr-1.json'), JSON.stringify({ + id: 'mgr-1', area: 'ai-sessions', status: 'assigned', assignedTo: 'Legacy dispatcher', title: 'Manager task', + })); + writeFileSync(join(root, 'docs', 'agents', 'backlog', 'retired-1.json'), JSON.stringify({ + id: 'retired-1', area: 'retired-domain', status: 'open', title: 'Retired area task', + })); writeFileSync(join(root, 'package.json'), `${JSON.stringify({ name: 'combatrig-fixture', scripts: { 'test:sessions': 'node sessions.mjs', build: 'node build.mjs', test: 'node all.mjs', 'test:terrain': 'node terrain.mjs', @@ -69,7 +75,17 @@ test('SCN-combatrig-import: legacy fleet evidence converts without mutation and assert.equal(proposal.session_manager.legacy_id, 'ai-sessions'); assert.deepEqual(proposal.domains.map((domain) => domain.id), ['terrain', 'animation']); assert.equal(proposal.resources[0].capacity, 2); - assert.deepEqual(proposal.compatibility.backlog.byStatus, { done: 1, assigned: 1 }); + assert.deepEqual(proposal.compatibility.backlog.byStatus, { done: 1, assigned: 2, open: 1 }); + assert.equal(proposal.compatibility.backlog.migration.mode, 'assessment-only'); + assert.deepEqual(proposal.compatibility.backlog.migration.unmappedAreas, [{ area: 'retired-domain', count: 1 }]); + assert.equal(proposal.compatibility.backlog.migration.sessionManagerAreaItems, 1); + assert.equal(proposal.compatibility.backlog.migration.freeformAssigneeLabels, 1); + assert.equal(proposal.compatibility.backlog.migration.legacyDoneItems, 1); + assert.equal(proposal.compatibility.backlog.migration.canImportAutomatically, false); + assert.match(proposal.compatibility.backlog.migration.note, /does not create Torch backlog tasks/); + assert.deepEqual(proposal.compatibility.backlog.byArea, { + terrain: 1, animation: 1, 'ai-sessions': 1, 'retired-domain': 1, + }); assert.equal(proposal.compatibility.schedules.every((schedule) => schedule.detected), true); assert.equal(proposal.compatibility.readyForApproval, false); assert.equal(proposal.compatibility.migrationGaps.some((gap) => diff --git a/test/fleet/console-check-evidence.test.mjs b/test/fleet/console-check-evidence.test.mjs new file mode 100644 index 0000000..8f0e01e --- /dev/null +++ b/test/fleet/console-check-evidence.test.mjs @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict'; +import { DatabaseSync } from 'node:sqlite'; +import test from 'node:test'; +import { observeCheckEvidence } from '../../src/observability/snapshot.mjs'; +import '../../site/check-evidence.js'; + +test('SCN-console-check-evidence: read-only projection isolates projects and preserves legacy, invalid and interrupted evidence', () => { + const database = new DatabaseSync(':memory:'); + try { + assert.deepEqual(observeCheckEvidence(database, 'project'), { receipts: [], prepared: [] }); + database.exec(`CREATE TABLE check_receipts (id TEXT, project_id TEXT, check_id TEXT, area_id TEXT, + commit_sha TEXT, result TEXT, finished_at TEXT, invalid_reason TEXT); + CREATE TABLE prepared_checks (id TEXT, project_id TEXT, check_id TEXT, area_id TEXT, + state TEXT, created_at TEXT, receipt_id TEXT, snapshot_json TEXT);`); + database.prepare('INSERT INTO check_receipts VALUES (?,?,?,?,?,?,?,?)') + .run('receipt', 'project', 'browser', 'interface', 'a'.repeat(40), 'invalid', '2026-09-30', 'condition-mismatch:clock'); + const insert = database.prepare('INSERT INTO prepared_checks VALUES (?,?,?,?,?,?,?,?)'); + insert.run('finished', 'project', 'browser', 'interface', 'finished', '2026-09-30', 'receipt', + JSON.stringify({ commit: 'a'.repeat(40), digest: 'sha256:capture', copyStrategy: 'copy', + inputRoot: '/private/build/path', files: [{ path: 'private-name' }] })); + insert.run('interrupted', 'project', 'browser', 'interface', 'running', '2026-09-30', null, '{bad'); + insert.run('foreign', 'other', 'secret', 'other', 'running', '2026-09-30', null, '{}'); + const before = database.prepare('SELECT * FROM prepared_checks').all(); + const legacy = observeCheckEvidence(database, 'project'); + assert.equal(legacy.receipts[0].conditions, null); + assert.equal(legacy.receipts[0].snapshot.digest, 'sha256:capture'); + assert.equal(legacy.receipts[0].snapshot.fileCount, 1); + assert.doesNotMatch(JSON.stringify(legacy), /private-name|private\/build/); + assert.equal(legacy.prepared.length, 2); + assert.equal(legacy.prepared.find((item) => item.id === 'interrupted').snapshot.unavailable, true); + database.exec('ALTER TABLE check_receipts ADD COLUMN conditions_json TEXT'); + database.prepare('UPDATE check_receipts SET conditions_json = ? WHERE id = ?').run(JSON.stringify({ + before: { valid: false, reason: 'condition-mismatch:clock', report: { conditions: { clock: false } } }, after: null, + }), 'receipt'); + const evidence = observeCheckEvidence(database, 'project'); + const html = globalThis.TorchCheckEvidence.render(evidence); + assert.match(html, /Running — completion unconfirmed/); + assert.match(html, /Frozen-input evidence unreadable/); + assert.match(html, /sha256:capture/); + assert.match(html, /clock.*false/s); + assert.match(html, /After measurement: not recorded/); + assert.match(html, /not independent sensor truth/); + assert.doesNotMatch(html, /foreign|secret/); + assert.deepEqual(database.prepare('SELECT * FROM prepared_checks').all(), before); + } finally { database.close(); } +}); + +test('SCN-console-check-evidence: untrusted project observations are escaped and absent conditions never imply verified runtime', () => { + const input = { prepared: [{ id: 'waiting', checkId: '', areaId: 'world', state: 'prepared', snapshot: null }], + receipts: [{ checkId: 'legacy', result: 'passed', commit: 'abc', conditions: null }, + { checkId: 'probe', conditions: { before: { valid: true, report: { conditions: { '', observedRunner: { pid: 777 }, executorsStopped: true, + cleanupPending: true, cleanupReason: 'CHECK_SNAPSHOT_INVALID' }; + insert.run('pending', 'project', 'interrupted', 'qa', 'abandoned', '2026-09-30', null, '{}', JSON.stringify(recovery)); + insert.run('clean', 'project', 'completed-cleanup', 'qa', 'abandoned', '2026-09-30', null, '{}', + JSON.stringify({ ...recovery, cleanupPending: false, cleanupReason: null })); + insert.run('unknown', 'project', 'old-recovery', 'qa', 'abandoned', '2026-09-30', null, '{}', '{broken'); + insert.run('foreign', 'foreign', 'secret-check', 'secret', 'abandoned', '2026-09-30', null, '{}', '{}'); + const before = database.prepare('SELECT * FROM prepared_checks').all(); + const observation = observeCheckEvidence(database, 'project'); + assert.equal(observation.prepared.length, 3); + assert.equal(observation.prepared.find((item) => item.id === 'pending').recovery.cleanupStatus, 'pending'); + assert.equal(observation.prepared.find((item) => item.id === 'clean').recovery.cleanupStatus, 'completed'); + assert.equal(observation.prepared.find((item) => item.id === 'unknown').recovery.cleanupStatus, 'unconfirmed'); + assert.doesNotMatch(JSON.stringify(observation), /private|777|foreign|secret/); + const html = globalThis.TorchCheckEvidence.render(observation); + assert.match(html, /Abandoned — not qualified/); + assert.match(html, /cleanup pending/); + assert.match(html, /cleanup completed \(recorded\)/); + assert.match(html, /cleanup unconfirmed/); + assert.match(html, /owner-attested, not independently verified descendant termination/); + assert.doesNotMatch(html, /', state: 'abandoned', + recovery: { cleanupReason: '', recordedAt: '' } }] }); + assert.doesNotMatch(unsafe, //); + assert.match(unsafe, /<script>/); + } finally { database.close(); } +}); diff --git a/test/fleet/console-live-refresh.test.mjs b/test/fleet/console-live-refresh.test.mjs new file mode 100644 index 0000000..d9d5285 --- /dev/null +++ b/test/fleet/console-live-refresh.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import '../../site/live-refresh.js'; + +test('SCN-console-live-refresh: requests are single-flight, explicit newer requests supersede stale responses and background ticks do not starve progress', async () => { + const pending = []; + const applied = []; + const errors = []; + const scheduler = globalThis.TorchLiveRefresh.createScheduler({ + load: () => new Promise((resolve, reject) => pending.push({ resolve, reject })), + apply: (snapshot) => applied.push(snapshot), onError: (error) => errors.push(error.message), + }); + const first = scheduler.request(); + assert.equal(scheduler.request({ supersede: false }), first); + assert.equal(pending.length, 1); + assert.equal(scheduler.request(), first); + pending[0].resolve('old'); + await Promise.resolve(); + assert.deepEqual(applied, []); + assert.equal(pending.length, 2); + scheduler.request({ supersede: false }); + pending[1].resolve('new'); + await first; + assert.deepEqual(applied, ['new']); + const failed = scheduler.request(); + pending[2].reject(new Error('offline')); + await failed; + assert.deepEqual(applied, ['new'], 'a failed read must retain displayed evidence'); + assert.deepEqual(errors, ['offline']); + const recovered = scheduler.request(); + pending[3].resolve('recovered'); + await recovered; + assert.deepEqual(applied, ['new', 'recovered']); +}); diff --git a/test/fleet/console-operation-outcomes.test.mjs b/test/fleet/console-operation-outcomes.test.mjs new file mode 100644 index 0000000..394fe55 --- /dev/null +++ b/test/fleet/console-operation-outcomes.test.mjs @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { DatabaseSync } from 'node:sqlite'; +import test from 'node:test'; +import { observeDeliveryEvidence } from '../../src/observability/snapshot.mjs'; +import '../../site/operation-outcomes.js'; + +test('SCN-console-operation-outcomes: project-scoped read-only receipt projection omits raw secrets and distinguishes uncertain outcomes', () => { + const database = new DatabaseSync(':memory:'); + try { + assert.deepEqual(observeDeliveryEvidence(database, 'project').deliveryOperations, []); + database.exec(`CREATE TABLE deliveries (id TEXT, project_id TEXT); + CREATE TABLE delivery_operations (id TEXT, delivery_id TEXT, commit_sha TEXT, actor TEXT, + provider TEXT, operation TEXT, state TEXT, created_at TEXT, updated_at TEXT); + CREATE TABLE delivery_attempts (id TEXT, operation_id TEXT, ordinal INTEGER, state TEXT, + started_at TEXT, finished_at TEXT, receipt_json TEXT);`); + database.prepare('INSERT INTO deliveries VALUES (?,?)').run('delivery', 'project'); + database.prepare('INSERT INTO deliveries VALUES (?,?)').run('foreign', 'other'); + const operation = database.prepare('INSERT INTO delivery_operations VALUES (?,?,?,?,?,?,?,?,?)'); + operation.run('op', 'delivery', 'abc', 'owner', 'custom', 'deploy', 'unknown', '2026-09-30', '2026-09-30'); + operation.run('foreign-op', 'foreign', 'secret-commit', 'owner', 'private', 'deploy', 'succeeded', '2026-09-30', '2026-09-30'); + database.prepare('INSERT INTO delivery_attempts VALUES (?,?,?,?,?,?,?)').run('attempt', 'op', 1, 'unknown', + '2026-09-30', '2026-09-30', JSON.stringify({ status: 'failed', reference: 'https://secret:password@example.invalid', + failure: { classification: 'transient', effects: 'unknown', code: '' } })); + const before = database.prepare('SELECT * FROM delivery_attempts').all(); + const projection = observeDeliveryEvidence(database, 'project'); + assert.equal(projection.deliveryOperations.length, 1); + assert.equal(projection.deliveryAttempts[0].receiptSummary.effects, 'unknown'); + assert.doesNotMatch(JSON.stringify(projection), /password|secret|foreign-op/); + const html = globalThis.TorchOperationOutcomes.render(projection); + assert.match(html, /Outcome unknown — review required/); + assert.match(html, /transient failure.*effects unknown/s); + assert.match(html, /not independent live verification/); + assert.doesNotMatch(html, / { + const input = { canonicalFetches: [{ remote: '', state: 'succeeded', attempts: [{ ordinal: 1, state: 'succeeded' }] }], + deliveryOperations: [{ id: 'running', state: 'running', operation: 'deploy' }, { id: 'ok', state: 'succeeded', operation: 'deploy' }], + deliveryAttempts: [] }; + const before = JSON.stringify(input); + const html = globalThis.TorchOperationOutcomes.render(input); + assert.match(html, /Object import succeeded — not deployment/); + assert.match(html, /Running — completion unconfirmed/); + assert.match(html, /Adapter succeeded — verify lifecycle and live evidence/); + assert.match(html, /<img src=x>/); + assert.doesNotMatch(html, / ({ available: true, count: items.length, items, truncated: false }); +function publication() { + return { id: 'report-1', report: { + schema: 'torch.dev/owner-digest/v1alpha1', generatedAt: '2026-09-30T08:12:00Z', + window: { since: '2026-09-29T08:12:00Z', until: '2026-09-30T08:12:00Z', hours: 24 }, + needsOwner: section([{ id: 'approval-1', title: '', requester: 'interface' }]), + deliveryReview: section([]), shipped: section([]), landed: section([{ sourceArea: 'services', commit: 'abc' }]), + completed: section([]), blocked: section([]), decisions: section([]), neglectedRequests: section([]), + lastDeployment: { label: 'Public beta', commit: 'abc', state: 'unknown', failureReason: '' }, + activityCoverage: { complete: false }, coverage: ['Structured decisions only'], + markdown: '', + } }; +} + +test('SCN-console-owner-digest: published briefing is safe, owner-first, evidence-labelled and never a live shipping claim', () => { + const input = publication(); + const before = JSON.stringify(input); + const html = views.render(input, { now }); + assert.ok(html.indexOf('Needs you') < html.indexOf('Shipping and integration')); + assert.match(html, /Outcome unknown/); + assert.match(html, /not independent live verification/); + assert.match(html, /activity history is incomplete/i); + assert.match(html, /href="#flow-watch"/); + assert.match(html, /Landed.*not necessarily deployed/s); + assert.doesNotMatch(html, / [bad](javascript:evil)', + description: 'Owner request', acceptanceCriteria: ['review'], affectedDomains: [context.worker] }); + const approval = context.control.requestApproval({ requester: context.worker, approver: 'owner', task: task.id, + title: 'Approve direction', summary: 'Owner decision needed' }); + context.control.requestApproval({ requester: context.worker, approver: 'session-manager', title: 'Manager approval', summary: 'Not owner approval' }); + const delivery = new DeliveryService({ repositoryRoot: context.root, controlPlane: context.control, clock: now }); + delivery.create({ sourceArea: context.worker, label: 'Implementation only', evidence: ['source'] }); + const service = new OwnerDigestService({ repositoryRoot: context.root, controlPlane: context.control, backlogService: context.backlog, clock: now }); + const before = JSON.stringify(context.backlog.list()); + const auditCount = context.control.database.prepare('SELECT COUNT(*) AS count FROM audit_events').get().count; + const report = service.build(); + assert.equal(report.needsOwner.items[0].id, approval.id); + assert.equal(report.needsOwner.count, 1); + assert.equal(report.shipped.count, 0); + assert.equal(report.neglectedRequests.items[0].taskId, task.id); + assert.equal(report.lastDeployment, null); + assert.equal(report.mutationPerformed, false); + assert.ok(report.markdown.indexOf('## Needs you') < report.markdown.indexOf('## Shipped')); + assert.doesNotMatch(report.markdown, /`); + resources.acquire({ resourceId: 'browser', areaId: context.worker }); + const prepared = checks.prepare({ checkId: 'same-runtime-browser', areaId: context.worker }); + const loadedPromise = nextMessage(); + daemon.send({ action: 'load', mode, subject: { root: prepared.snapshot.inputRoot, + commit, inputDigest: prepared.snapshot.digest } }); + const loaded = await loadedPromise; + assert.equal(loaded.event, 'loaded'); + const receipt = checks.runPrepared({ preparedId: prepared.id, areaId: context.worker }); + const artifact = JSON.parse(readFileSync(receipt.artifactPath, 'utf8')); + const status = await (await fetch(`${listening.base}/status`)).json(); + if (mode === 'replaced') assert.notEqual(status.runtimeId, loaded.runtimeId); + else assert.equal(status.runtimeId, loaded.runtimeId); + assert.equal(receipt.conditions.before.report.runtimeId, loaded.runtimeId); + assert.equal(status.measurements, mode === 'frozen' ? 0 : 1); + assert.equal(artifact.measurementsExecuted, mode !== 'frozen'); + assert.equal(receipt.result, mode === 'healthy' ? 'pass' : 'incomplete', artifact.stderr); + assert.equal(checks.exactPasses({ commit, requiredChecks: ['same-runtime-browser'] }), mode === 'healthy'); + if (mode === 'healthy') { + // Historical green flags cannot bypass today's continuity contract. + const historical = structuredClone(receipt.conditions); + historical.after.report.runtimeId = 'different-historical-runtime'; + control.database.prepare('UPDATE check_receipts SET conditions_json = ? WHERE id = ?') + .run(JSON.stringify(historical), receipt.id); + assert.equal(checks.exactPasses({ commit, requiredChecks: ['same-runtime-browser'] }), false); + } + if (mode === 'frozen') { + assert.equal(receipt.invalidReason, 'test-conditions-invalid-before'); + assert.equal(receipt.conditions.before.reason, 'condition-mismatch:clock-moving'); + } else { + if (mode === 'replaced') { + assert.notEqual(receipt.conditions.after.report.runtimeId, loaded.runtimeId); + assert.equal(receipt.conditions.after.reason, 'condition-runtime-changed'); + assert.equal(receipt.invalidReason, 'test-conditions-invalid-after'); + assert.equal(receipt.conditions.after.report.conditions['clock-moving'], true); + } else assert.equal(receipt.conditions.after.report.runtimeId, loaded.runtimeId); + const measured = artifact.stdout.split('\n').find((line) => line.startsWith('{')); + assert.equal(JSON.parse(measured).runtimeId, loaded.runtimeId); + assert.equal(receipt.exitStatus, 0, 'green measurement output alone cannot prove valid conditions'); + if (mode === 'drift') { + assert.equal(receipt.invalidReason, 'test-conditions-invalid-after'); + assert.equal(receipt.conditions.after.reason, 'condition-mismatch:clock-moving'); + } + } + assert.equal(existsSync(prepared.snapshot.root), false); + resources.release({ resourceId: 'browser', areaId: context.worker }); + } finally { + control?.close(); + if (daemon.exitCode === null && daemon.connected) { + const exited = once(daemon, 'exit'); + daemon.send({ action: 'close' }); + const [code] = await exited; + assert.equal(code, 0, stderr); + } + } + } +}); + +test('SCN-frozen-check-mutating-executor: changed inputs and thrown execution cannot create passing receipts', () => { + const context = fixture(); + const path = join(context.root, '.torch', 'torch.yaml'); + const config = JSON.parse(readFileSync(path, 'utf8')); + config.checks.push({ id: 'frozen', command: 'node', snapshot: { paths: ['app.js', 'build-sha'], source_commit_file: 'build-sha' } }); + writeFileSync(path, JSON.stringify(config)); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + let checks = new CheckService({ repositoryRoot: context.root, controlPlane: control }); + const worktree = checks.worktrees.get(context.worker).path; + writeFileSync(join(worktree, '.gitignore'), 'build-sha\n'); + execFileSync('git', ['-C', worktree, 'add', '.gitignore']); + execFileSync('git', ['-C', worktree, 'commit', '-m', 'ignore marker']); + const commit = execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); + writeFileSync(join(worktree, 'build-sha'), commit); + checks = new CheckService({ repositoryRoot: context.root, controlPlane: control, executor: (_command, _args, { cwd }) => { + chmodSync(join(cwd, 'app.js'), 0o600); + writeFileSync(join(cwd, 'app.js'), 'changed input'); + return { status: 0 }; + } }); + const changed = checks.run({ checkId: 'frozen', areaId: context.worker }); + assert.equal(changed.result, 'incomplete'); + assert.equal(changed.invalidReason, 'snapshot-changed-during-check'); + assert.equal(checks.exactPasses({ commit, requiredChecks: ['frozen'] }), false); + checks.executor = () => { throw new Error('fixture executor launch failed'); }; + const thrown = checks.run({ checkId: 'frozen', areaId: context.worker }); + assert.equal(thrown.result, 'incomplete'); + assert.equal(thrown.invalidReason, 'executor-failed'); + assert.equal(checks.prepared({ areaId: context.worker }).every((item) => item.state === 'finished'), true); + control.close(); +}); + +test('SCN-browser-descendant-interruption: runner death leaves Chromium live, owner recovery follows explicit observed browser shutdown', { + skip: process.platform !== 'linux' || process.env.TORCH_QUALIFY_INTERRUPTION !== '1' + || process.env.TORCH_QUALIFY_BROWSER !== '1', +}, async () => { + const context = fixture(); + const configPath = join(context.root, '.torch', 'torch.yaml'); + const config = JSON.parse(readFileSync(configPath, 'utf8')); + config.checks.push({ id: 'browser-interruption', command: process.execPath, resources: ['browser'], + args: [new URL('../../scripts/interrupted-browser-harness.mjs', import.meta.url).pathname], + snapshot: { paths: ['dist'], source_commit_file: 'dist/source-commit' } }); + writeFileSync(configPath, JSON.stringify(config)); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const resources = new ResourceService({ repositoryRoot: context.root, controlPlane: control }); + const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control, resourceService: resources }); + const worktree = checks.worktrees.get(context.worker).path; + writeFileSync(join(worktree, '.gitignore'), 'dist/\n'); + execFileSync('git', ['-C', worktree, 'add', '.gitignore']); + execFileSync('git', ['-C', worktree, 'commit', '-m', 'ignore interrupted browser output']); + const commit = execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); + mkdirSync(join(worktree, 'dist')); + writeFileSync(join(worktree, 'dist', 'source-commit'), commit); + writeFileSync(join(worktree, 'dist', 'index.html'), `
    Captured
    `); + resources.acquire({ resourceId: 'browser', areaId: context.worker }); + const prepared = checks.prepare({ checkId: 'browser-interruption', areaId: context.worker }); + const runner = fork(new URL('../../scripts/interrupted-check-runner.mjs', import.meta.url), + [context.root, prepared.id, context.worker], { env: context.env, stdio: ['ignore', 'pipe', 'pipe', 'ipc'] }); + let ready; + let stopped = false; + let watcher; + try { + ready = await new Promise((resolve, reject) => { + let output = ''; + runner.stdout.on('data', (chunk) => { output += chunk; if (output.includes('\n')) resolve(JSON.parse(output.trim())); }); + runner.once('error', reject); + runner.once('exit', (code) => reject(new Error(`Browser runner exited before readiness: ${code}`))); + }); + assert.equal(ready.event, 'browser-ready'); + const exited = once(runner, 'exit'); runner.kill('SIGKILL'); await exited; + const live = await (await fetch(`${ready.base}/status`)).json(); + assert.equal(live.build, 'captured'); + assert.equal(live.commit, commit); + assert.equal(live.visible, true, 'real browser remains usable after runner death'); + assert.equal(checks.planPreparedRecovery({ preparedId: prepared.id, actorId: 'owner' }).runner.state, 'stopped'); + assert.throws(() => checks.recoverPrepared({ preparedId: prepared.id, actorId: 'owner', approved: true, + executorsStopped: false, evidence: 'Only runner stopped; browser remains live.' }), + (error) => error.code === 'PREPARED_CHECK_STOP_EVIDENCE_REQUIRED'); + assert.equal(existsSync(prepared.snapshot.root), true); + watcher = spawn('python3', [new URL('../../scripts/wait-owned-browser-group.py', import.meta.url).pathname, + String(ready.browserPid), ready.browserStart, String(ready.executorPid), ready.executorStart], { stdio: ['ignore', 'pipe', 'pipe'] }); + let watcherError = ''; + watcher.stderr.on('data', (chunk) => { watcherError += chunk; }); + const watchedExit = once(watcher, 'exit'); + const watching = await new Promise((resolve, reject) => { + let output = ''; + watcher.stdout.on('data', (chunk) => { output += chunk; if (output.includes('\n')) resolve(JSON.parse(output.split('\n')[0])); }); + watcher.once('error', reject); + watcher.once('exit', (code) => reject(new Error(`Watcher exited before handles: ${code} ${watcherError}`))); + }); + assert.equal(watching.event, 'watching'); + assert.ok(watching.observed > 1); + const response = await fetch(`${ready.base}/stop`, { method: 'POST', headers: { 'x-fixture-token': ready.token } }); + assert.equal(response.status, 200); + assert.equal((await response.json()).stopped, true); + stopped = true; + const [code] = await watchedExit; + assert.equal(code, 0, watcherError); + // Watcher captured both browser group and harness handles before shutdown, + // so terminal notification cannot race with PID disappearance or reuse. + const result = checks.recoverPrepared({ preparedId: prepared.id, actorId: 'owner', approved: true, + executorsStopped: true, evidence: 'Runner exit observed; explicit harness shutdown and captured isolated Chromium group pidfd terminal states confirmed.' }); + assert.equal(result.state, 'abandoned'); + assert.equal(result.cleanupPending, false); + assert.equal(checks.receipts({ checkId: 'browser-interruption' }).length, 0); + assert.equal(checks.exactPasses({ commit, requiredChecks: ['browser-interruption'] }), false); + assert.equal(resources.hasActiveLease({ resourceId: 'browser', areaId: context.worker }), true); + assert.match(readFileSync(join(worktree, 'dist', 'index.html'), 'utf8'), /Captured/); + resources.release({ resourceId: 'browser', areaId: context.worker }); + } finally { + if (ready && !stopped) await fetch(`${ready.base}/stop`, { method: 'POST', headers: { 'x-fixture-token': ready.token } }); + if (runner.exitCode === null && runner.signalCode === null) { const exited = once(runner, 'exit'); runner.kill('SIGKILL'); await exited; } + if (watcher && watcher.exitCode === null && watcher.signalCode === null) await once(watcher, 'exit'); + control.close(); + } +}); + +test('SCN-hard-killed-prepared-check: stopped runners retain evidence, recovery requires owner confirmation and never creates a pass or releases resources', { + skip: process.platform !== 'linux' || process.env.TORCH_QUALIFY_INTERRUPTION !== '1', +}, async () => { + const context = fixture(); + const path = join(context.root, '.torch', 'torch.yaml'); + const config = JSON.parse(readFileSync(path, 'utf8')); + config.checks.push({ id: 'interrupted', command: process.execPath, resources: ['browser'], + args: ['-e', 'console.log(JSON.stringify({event:"executor-started",pid:process.pid}));Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0);'], + snapshot: { paths: ['app.js', 'build-sha'], source_commit_file: 'build-sha' } }); + writeFileSync(path, JSON.stringify(config)); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const resources = new ResourceService({ repositoryRoot: context.root, controlPlane: control }); + const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control, resourceService: resources }); + const worktree = checks.worktrees.get(context.worker).path; + writeFileSync(join(worktree, '.gitignore'), 'build-sha\n'); + execFileSync('git', ['-C', worktree, 'add', '.gitignore']); + execFileSync('git', ['-C', worktree, 'commit', '-m', 'ignore interruption marker']); + const commit = execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); + writeFileSync(join(worktree, 'build-sha'), commit); + resources.acquire({ resourceId: 'browser', areaId: context.worker }); + const prepared = checks.prepare({ checkId: 'interrupted', areaId: context.worker }); + const runner = fork(new URL('../../scripts/interrupted-check-runner.mjs', import.meta.url), + [context.root, prepared.id, context.worker], { env: context.env, stdio: ['ignore', 'pipe', 'pipe', 'ipc'] }); + let executorPid; + try { + const started = await new Promise((resolve, reject) => { + let output = ''; + runner.stdout.on('data', (chunk) => { + output += chunk; + if (output.includes('\n')) resolve(JSON.parse(output.trim())); + }); + runner.once('error', reject); + runner.once('exit', (code) => reject(new Error(`Runner exited before handshake: ${code}`))); + }); + assert.equal(started.event, 'executor-started'); + executorPid = started.pid; + const running = checks.prepared({ areaId: context.worker }).find((item) => item.id === prepared.id); + assert.equal(running.state, 'running'); + assert.equal(running.runner.pid, runner.pid); + assert.equal(checks.planPreparedRecovery({ preparedId: prepared.id, actorId: 'owner' }).runner.state, 'live'); + assert.throws(() => checks.recoverPrepared({ preparedId: prepared.id, actorId: 'owner', approved: true, + executorsStopped: true, evidence: 'Invalid claim while runner still lives.' }), (error) => error.code === 'PREPARED_CHECK_RUNNER_LIVE'); + const exited = once(runner, 'exit'); + runner.kill('SIGKILL'); + await exited; + // Stop this exact disposable executor via a kernel pidfd and await terminal + // notification, not a sleep or a guessed PID timeout. No unrelated PID is targeted. + execFileSync('python3', ['-c', 'import os,signal,select,sys; fd=os.pidfd_open(int(sys.argv[1])); signal.pidfd_send_signal(fd,signal.SIGKILL); assert select.select([fd],[],[],10)[0]; os.close(fd)', String(executorPid)], { timeout: 15000 }); + executorPid = null; + const resumed = new CheckService({ repositoryRoot: context.root, controlPlane: control, resourceService: resources }); + assert.equal(resumed.planPreparedRecovery({ preparedId: prepared.id, actorId: 'owner' }).runner.state, 'stopped'); + assert.equal(existsSync(prepared.snapshot.root), true); + assert.throws(() => resumed.runPrepared({ preparedId: prepared.id, areaId: context.worker }), + (error) => error.code === 'PREPARED_CHECK_UNAVAILABLE'); + assert.throws(() => resumed.recoverPrepared({ preparedId: prepared.id, actorId: 'owner', evidence: 'stopped' }), + (error) => error.code === 'APPROVAL_REQUIRED'); + assert.throws(() => resumed.recoverPrepared({ preparedId: prepared.id, actorId: 'owner', approved: true }), + (error) => error.code === 'PREPARED_CHECK_STOP_EVIDENCE_REQUIRED'); + assert.throws(() => resumed.planPreparedRecovery({ preparedId: prepared.id, actorId: context.worker }), + (error) => error.code === 'OWNER_AUTHORITY_REQUIRED'); + const input = { preparedId: prepared.id, actorId: 'owner', approved: true, + executorsStopped: true, evidence: 'Owned runner SIGKILL exit observed; owned measurement pidfd termination confirmed.' }; + const marker = join(prepared.snapshot.root, 'snapshot.json'); + const originalMarker = readFileSync(marker, 'utf8'); + writeFileSync(marker, '{}'); + const pendingCleanup = resumed.recoverPrepared(input); + assert.equal(pendingCleanup.state, 'abandoned'); + assert.equal(pendingCleanup.cleanupPending, true); + assert.equal(pendingCleanup.cleanupReason, 'CHECK_SNAPSHOT_INVALID'); + assert.equal(new CheckService({ repositoryRoot: context.root, controlPlane: control }) + .prepared().find((item) => item.id === prepared.id).recovery.cleanupPending, true); + assert.equal(existsSync(prepared.snapshot.root), true, 'unowned cleanup must preserve the directory'); + writeFileSync(marker, originalMarker); + const recovered = resumed.recoverPrepared(input); + assert.equal(recovered.state, 'abandoned'); + assert.equal(recovered.cleanupPending, false); + assert.equal(new CheckService({ repositoryRoot: context.root, controlPlane: control }) + .prepared().find((item) => item.id === prepared.id).recovery.cleanupPending, false); + assert.equal(recovered.receiptCreated, false); + assert.equal(recovered.executionPerformed, false); + assert.equal(existsSync(prepared.snapshot.root), false); + assert.equal(resumed.receipts({ checkId: 'interrupted' }).length, 0); + assert.equal(resumed.exactPasses({ commit, requiredChecks: ['interrupted'] }), false); + assert.equal(resources.hasActiveLease({ resourceId: 'browser', areaId: context.worker }), true); + assert.equal(resumed.recoverPrepared(input).mutationPerformed, false); + assert.equal(control.readAudit().filter((event) => event.operation === 'check.recover-prepared' + && event.entityId === prepared.id).length, 1, 'cleanup replay retains a single original recovery audit'); + assert.equal(readFileSync(join(worktree, 'app.js'), 'utf8'), 'export const app = true;\n'); + const cli = (args) => spawnSync(process.execPath, [new URL('../../bin/torch.mjs', import.meta.url).pathname, + 'checks', ...args, '--json'], { cwd: context.root, env: context.env, encoding: 'utf8' }); + assert.equal(cli(['recovery-plan', '--prepared', prepared.id]).status, 0); + assert.equal(cli(['recover-prepared', '--prepared', prepared.id]).status, 2); + assert.equal(cli(['recover-prepared', '--prepared', prepared.id, '--executors-stopped', + '--evidence', input.evidence, '--yes']).status, 0); + resources.release({ resourceId: 'browser', areaId: context.worker }); + } finally { + if (runner.exitCode === null && runner.signalCode === null) { + const exited = once(runner, 'exit'); runner.kill('SIGKILL'); await exited; + } + if (executorPid) { + execFileSync('python3', ['-c', 'import os,signal,select,sys; fd=os.pidfd_open(int(sys.argv[1])); signal.pidfd_send_signal(fd,signal.SIGKILL); assert select.select([fd],[],[],10)[0]; os.close(fd)', String(executorPid)], { timeout: 15000 }); + } + control.close(); + } +}); + +test('SCN-condition-receipts: live and frozen checks persist validated condition evidence and reject bad environment as incomplete', () => { + const context = fixture(); + const path = join(context.root, '.torch', 'torch.yaml'); + const config = JSON.parse(readFileSync(path, 'utf8')); + const probe = 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync("conditions.json","utf8"));console.log(JSON.stringify({schema:"torch.dev/check-conditions/v1alpha1",subject:{commit:r.commit,inputDigest:process.env.TORCH_CHECK_INPUT_DIGEST||undefined},conditions:{"clock-moving":r.moving,visible:r.visible}}));'; + const conditions = { command: 'node', args: ['-e', probe], required: [ + { id: 'clock-moving', equals: true }, { id: 'visible', equals: true }, + ] }; + const measured = join(context.root, 'measurement-evidence'); + for (const id of ['condition-live', 'condition-frozen']) config.checks.push({ + id, command: 'node', args: ['-e', `require("node:fs").writeFileSync(${JSON.stringify(measured)},"executed");`], + conditions, ...(id === 'condition-frozen' ? { + snapshot: { paths: ['app.js', 'build-sha', 'conditions.json'], source_commit_file: 'build-sha' }, + } : {}), + }); + writeFileSync(path, JSON.stringify(config)); + const control = openControlPlane({ repositoryRoot: context.root, env: context.env }); + const checks = new CheckService({ repositoryRoot: context.root, controlPlane: control }); + const worktree = checks.worktrees.get(context.worker).path; + writeFileSync(join(worktree, '.gitignore'), 'conditions.json\nbuild-sha\n'); + execFileSync('git', ['-C', worktree, 'add', '.gitignore']); + execFileSync('git', ['-C', worktree, 'commit', '-m', 'ignore diagnostic inputs']); + const commit = execFileSync('git', ['-C', worktree, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); + writeFileSync(join(worktree, 'build-sha'), commit); + const report = { commit, moving: false, visible: true }; + writeFileSync(join(worktree, 'conditions.json'), JSON.stringify(report)); + const invalid = checks.run({ checkId: 'condition-live', areaId: context.worker }); + assert.equal(invalid.result, 'incomplete'); + assert.equal(invalid.invalidReason, 'test-conditions-invalid-before'); + assert.equal(existsSync(measured), false); + assert.equal(checks.exactPasses({ commit, requiredChecks: ['condition-live'] }), false); + report.moving = true; + writeFileSync(join(worktree, 'conditions.json'), JSON.stringify(report)); + for (const id of ['condition-live', 'condition-frozen']) { + const passed = checks.run({ checkId: id, areaId: context.worker }); + assert.equal(passed.result, 'pass'); + assert.equal(passed.conditions.before.valid, true); + assert.equal(passed.conditions.after.valid, true); + assert.equal(checks.receipts({ checkId: id }).some((receipt) => receipt.conditions?.before.valid), true); + assert.equal(JSON.parse(readFileSync(passed.artifactPath, 'utf8')).conditions.before.valid, true); + if (id === 'condition-frozen') assert.equal(passed.conditions.subject.inputDigest, passed.snapshot.inputDigest); + } + const changed = new CheckService({ repositoryRoot: context.root, controlPlane: control }); + const validEvidence = checks.receipts({ checkId: 'condition-live' }).find((receipt) => receipt.result === 'pass'); + control.database.prepare('UPDATE check_receipts SET conditions_json = NULL WHERE id = ?').run(validEvidence.id); + assert.equal(checks.exactPasses({ commit, requiredChecks: ['condition-live'] }), false, + 'passing status alone cannot replace a missing condition receipt'); + config.checks.find((check) => check.id === 'condition-live').conditions.required.push({ id: 'gpu-active', equals: true }); + writeFileSync(path, JSON.stringify(config)); + assert.equal(changed.exactPasses({ commit, requiredChecks: ['condition-live'] }), false, + 'a condition-policy change invalidates prior receipts'); + control.close(); +});