Checkpoint portable TORCH fleet, provider adapters and owner dashboard

This commit is contained in:
thePR0M3TH3AN committed 2026-09-30 13:28:40 -04:00
1 parent d31437a4fb
commit 03fcbdd23f
140 files changed
+26152 -757

No files matched your search

+2
View File
@@ -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
+400 -24
View File
@@ -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 <delivery-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 <id> --by
owner --runtime-stopped --evidence <reference> --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 <id> --by owner --dry-run --json`, then confirm
with `--runtime-stopped --evidence <provider-proof> --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 <id> --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 <returned-id> --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 <id> --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 <id> --review <json-path> --from session-manager --yes`; inspect
the durable history with `torch fleet hierarchy-pilot-reviews --proposal <id>`.
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
<id> --decision adopt --reason "<owner decision>"`, then confirm with
`torch fleet hierarchy-conclude` using the same arguments plus
`--plan-hash <reviewed digest> --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 <id> --runtime-stopped --note
"<inspection evidence>" --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 <artifact-id> --body "Keep the compact header" --by owner --json
torch artifacts comment --artifact <artifact-id> --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 <hash-from-preview>
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
+327 -22
View File
@@ -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.<adapter>`) 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.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+856 -11
View File
@@ -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 <manager> --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
+16
View File
@@ -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"
+19 -4
View File
@@ -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",
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
+102
View File
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+65 -3
View File
@@ -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
+15
View File
@@ -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));
+86
View File
@@ -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}` });
+53
View File
@@ -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()));
}
+54
View File
@@ -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));
}
+15
View File
@@ -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(); }
+247
View File
@@ -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));
}
+57
View File
@@ -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)
+6 -5
View File
@@ -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();
+40
View File
@@ -0,0 +1,40 @@
(function () {
const text = (value) => String(value ?? '').slice(0, 4000).replace(/[&<>"']/g,
(character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
const value = (input) => text(typeof input === 'object' ? JSON.stringify(input) : input);
function frozen(snapshot) {
if (!snapshot) return '<p>Frozen-input evidence not recorded.</p>';
if (snapshot.unavailable) return '<p>Frozen-input evidence unreadable.</p>';
return `<p>Frozen input commit <code>${text(snapshot.commit)}</code></p>
<p>Input digest <code>${text(snapshot.digest ?? snapshot.inputDigest)}</code></p>
<p>Copy strategy: ${text(snapshot.copyStrategy ?? 'not recorded')}. Recorded capture, not a new integrity check.</p>`;
}
function phase(label, observation) {
if (!observation) return `<p>${label}: not recorded.</p>`;
const entries = Object.entries(observation.report?.conditions ?? {}).slice(0, 100);
return `<h4>${label}: ${observation.valid === true ? 'contract valid' : 'invalid or unknown'}</h4>
${observation.reason ? `<p>${text(observation.reason)}</p>` : ''}
<dl>${entries.map(([key, observed]) => `<div><dt>${text(key)}</dt><dd>${value(observed)}</dd></div>`).join('')}</dl>`;
}
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 `<p>Recorded check evidence. Running records do not prove a live executor; interrupted runs need review, not automatic rerun.</p>
${pending.map((item) => `<details class="check-evidence"><summary>${text(item.checkId)} · ${text(item.areaId)} · ${item.state === 'running' ? 'Running — completion unconfirmed' : item.state === 'abandoned' ? 'Abandoned — not qualified' : 'Prepared — awaiting execution'}</summary>
<p>Preparation ${text(item.id)} · ${text(item.createdAt)}</p>${frozen(item.snapshot)}
${item.state === 'abandoned' ? `<p>${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.'}</p>
${item.recovery?.recordedAt ? `<p>Recovery recorded ${text(item.recovery.recordedAt)}</p>` : ''}
${item.recovery?.cleanupReason ? `<p>Cleanup reason: ${text(item.recovery.cleanupReason)}</p>` : ''}
<p>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.</p>` : ''}</details>`).join('')}
${receipts.length ? receipts.map((receipt) => `<details class="check-evidence"><summary>${text(receipt.checkId)} · ${text(receipt.areaId)} · ${text(receipt.result)}</summary>
<p>Commit <code>${text(receipt.commit)}</code> · ${text(receipt.finishedAt)}</p>
${receipt.invalidReason ? `<p>Qualification invalid: ${text(receipt.invalidReason)}</p>` : ''}${frozen(receipt.snapshot)}
${receipt.conditions ? (receipt.conditions.unavailable ? '<p>Condition evidence unreadable.</p>' : `<p>Conditions are project-reported pre/post observations, not independent sensor truth or continuous monitoring.</p>
${phase('Before measurement', receipt.conditions.before)}${phase('After measurement', receipt.conditions.after)}`)
: '<p>No condition observations recorded; this is not proof that runtime conditions were verified.</p>'}</details>`).join('')
: '<p>No exact-commit check receipts.</p>'}
${checks.coverage ? `<p>${text(checks.coverage)}</p>` : ''}`;
}
globalThis.TorchCheckEvidence = Object.freeze({ render });
})();
+283 -29
View File
@@ -4,41 +4,295 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>TORCH Fleet Console</title>
<meta name="theme-color" content="#101a22">
<title>TORCH Local Fleet Console</title>
<link rel="stylesheet" href="/styles.css">
<link rel="icon" type="image/svg+xml" href="/torch-mark.svg">
<script src="/work-views.js" defer></script>
<script src="/work-progress.js" defer></script>
<script src="/owner-digest.js" defer></script>
<script src="/check-evidence.js" defer></script>
<script src="/task-activity.js" defer></script>
<script src="/operation-outcomes.js" defer></script>
<script src="/live-refresh.js" defer></script>
<script src="/demo.js" defer></script>
<script src="/console.js" defer></script>
</head>
<body class="console-body">
<a class="skip-link" href="#console-main">Skip to fleet status</a>
<aside id="demo-banner" class="console-demo-banner" aria-label="Demo workspace" hidden>
<div><strong>Demo workspace</strong><span>Sample project · Changes stay in this tab</span></div>
<button id="reset-demo" type="button">Reset demo</button>
</aside>
<a class="skip-link" href="#overview">Skip to project overview</a>
<header class="masthead console-nav">
<a class="wordmark" href="/"><span class="mark">T</span> TORCH</a>
<p>Local Fleet Console</p>
<button id="refresh-console" type="button">Refresh snapshot</button>
<a class="wordmark" href="/" aria-label="TORCH home"><img src="/torch-mark.svg" alt="" width="34" height="34"> TORCH</a>
<div class="console-project"><strong id="project-name">Loading project…</strong><span id="project-branch">Reading repository</span></div>
<div class="console-tools"><span id="snapshot-time">Waiting for snapshot</span><button id="refresh-console" type="button">Refresh</button><button id="toggle-live-refresh" type="button" aria-pressed="false">Pause updates</button></div>
</header>
<main id="console-main" class="console-shell" aria-live="polite">
<section class="console-summary">
<div><p>Project</p><strong id="project-name">Loading…</strong></div>
<div><p>Commit</p><strong id="project-commit">—</strong></div>
<div><p>Fleet health</p><strong id="fleet-health">Observing</strong></div>
<div><p>Context locality</p><strong id="context-state">Unmeasured</strong></div>
</section>
<p class="console-error" id="console-error" hidden></p>
<section class="console-grid">
<article class="console-panel agents-panel"><div class="panel-heading"><h1>Fleet</h1><span id="agent-count">0 identities</span></div><div id="agent-list" class="agent-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Ownership</h2><span>Domain graph</span></div><div id="ownership-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Worktrees</h2><span id="recoverability-state">Recoverability</span></div><div id="worktree-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Backlog</h2><span id="task-count">0 active</span></div><div id="task-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Fleet changes</h2><span id="change-count">0 pending</span></div><div id="change-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Integration</h2><span>Exact commits</span></div><div id="integration-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Delivery</h2><span>Release is not deploy</span></div><div id="delivery-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Checks</h2><span>Exact receipts</span></div><div id="check-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Resources</h2><span>FIFO leases</span></div><div id="resource-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Schedules</h2><span>Declared lifetimes</span></div><div id="schedule-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Providers</h2><span>Capability health</span></div><div id="provider-list" class="console-list"></div></article>
<article class="console-panel messages-panel"><div class="panel-heading"><h2>Messages</h2><span id="message-count">0 queued</span></div><div id="message-list" class="console-list"></div></article>
<article class="console-panel"><div class="panel-heading"><h2>Decisions</h2><span>Tracked truth</span></div><div id="decision-list" class="console-list"></div></article>
<article class="console-panel context-panel"><div class="panel-heading"><h2>Context locality</h2><span>Measured, never assumed</span></div><div id="context-detail" class="context-detail"></div></article>
</section>
<main id="console-main" class="console-layout">
<nav class="console-rail" aria-label="Project views">
<a href="#overview">Overview</a>
<a href="#owner-briefing">Briefing</a>
<a href="#flow-watch">Flow watch</a>
<a href="#work">Work</a>
<a href="#initiative-progress">Progress</a>
<a href="#fleet">Fleet</a>
<a href="#communications">Conversations</a>
<a href="#organization">Organization</a>
<a href="#evidence">Evidence</a>
<a href="#delivery">Release gates</a>
<p class="rail-note">Local project view<br><span id="project-commit">—</span></p>
</nav>
<div class="console-workspace">
<p class="console-error" id="console-error" role="alert" hidden></p>
<p id="live-refresh-status" role="status" aria-live="polite">Waiting for the first snapshot.</p>
<section id="overview" class="pulse-zone" aria-labelledby="pulse-title">
<div class="pulse-main">
<div class="pulse-heading"><div><p class="section-kicker">Project pulse</p><h1 id="pulse-title">Work in motion</h1></div><span id="fleet-health" class="health-flag">Observing</span></div>
<p id="project-summary" class="pulse-summary">TORCH is gathering the latest repository and fleet evidence.</p>
<div class="pulse-readouts" aria-label="Project state">
<div><span>Repository</span><strong id="project-dirty">—</strong></div>
<div><span>Fleet</span><strong id="agent-count">0 identities</strong></div>
<div><span>Context</span><strong id="context-state">Unmeasured</strong></div>
<div><span>Recoverability</span><strong id="recoverability-state">Unknown</strong></div>
</div>
</div>
<aside class="attention-panel" aria-labelledby="attention-title">
<div class="panel-heading"><h2 id="attention-title">Needs your attention</h2><span id="attention-count">0 items</span></div>
<div id="attention-list" class="attention-list"></div>
</aside>
</section>
<section id="owner-briefing" class="console-section owner-briefing" aria-labelledby="owner-briefing-title">
<div class="section-heading"><h2 id="owner-briefing-title">Owner briefing</h2><span>Published evidence</span></div>
<div id="owner-briefing-content"><p>Reading the latest local publication…</p></div>
</section>
<section id="flow-watch" class="console-section flow-watch-section" aria-labelledby="flow-watch-title">
<div class="section-heading"><div><p class="section-kicker">Approvals and reporting lines</p><h2 id="flow-watch-title">Flow watch</h2></div><span>Owner decisions gated</span></div>
<p class="flow-watch-note">See which teams need attention and who owns each decision. A configured cadence does not confirm that a system timer is installed.</p>
<div class="flow-watch-grid">
<article class="flow-column" aria-labelledby="manager-checkins-title">
<div class="panel-heading"><h3 id="manager-checkins-title">Manager check-ins</h3><span id="manager-checkin-count">Not available</span></div>
<div id="manager-checkin-list" class="manager-checkin-list" aria-live="polite"></div>
</article>
<article class="flow-column approvals-column" aria-labelledby="approval-requests-title">
<div class="panel-heading"><h3 id="approval-requests-title">Open approvals</h3><span id="approval-request-count">Not available</span></div>
<div id="approval-request-list" class="approval-request-list" aria-live="polite"></div>
<p class="flow-watch-footnote">Only the named approver can decide. Owner-addressed requests have a review-and-confirm flow; requests assigned to an AI stay with that AI.</p>
</article>
</div>
</section>
<section id="work" class="board-section" aria-labelledby="work-title">
<div class="section-heading"><div><p class="section-kicker">Authoritative backlog</p><h2 id="work-title">Work board</h2></div><span id="task-count">0 active</span></div>
<div class="work-view-tools" role="group" aria-label="Backlog views and filters">
<div class="work-filter-row">
<label class="saved-view-picker" for="work-view-select">Saved view
<select id="work-view-select" aria-label="Choose a saved backlog view"><option value="">All active work</option></select>
</label>
<label class="work-filter search-filter" for="work-view-search">Find work
<input id="work-view-search" type="search" maxlength="120" autocomplete="off" placeholder="Task, owner, or phrase">
</label>
<label class="work-filter" for="work-view-owner">Owner
<select id="work-view-owner"><option value="">Anyone</option></select>
</label>
<label class="work-filter" for="work-view-state">State
<select id="work-view-state"><option value="">Any state</option></select>
</label>
<label class="work-filter" for="work-view-priority">Priority
<select id="work-view-priority"><option value="">Any priority</option></select>
</label>
<label class="work-filter" for="work-view-domain">Domain
<select id="work-view-domain"><option value="">Any domain</option></select>
</label>
<label class="work-filter" for="work-view-feature">Feature
<select id="work-view-feature"><option value="">Any feature</option></select>
</label>
<label class="work-filter" for="work-view-milestone">Milestone
<select id="work-view-milestone"><option value="">Any milestone</option></select>
</label>
<button id="work-view-clear" class="quiet-action" type="button">Clear filters</button>
</div>
<div class="saved-view-actions">
<label for="work-view-name">View name
<input id="work-view-name" type="text" maxlength="64" autocomplete="off" placeholder="e.g. World work at risk">
</label>
<button id="work-view-save" type="button">Save as view</button>
<button id="work-view-update" class="quiet-action" type="button" hidden>Update selected</button>
<button id="work-view-delete" class="quiet-action" type="button" hidden>Delete selected</button>
<span class="view-storage-note">Saved in this browser for this project; filters never change tasks.</span>
</div>
<p id="work-view-status" class="work-view-status" role="status" aria-live="polite"></p>
<p class="owner-control-note">New work remains proposed and unassigned until the Session Manager triages it. Priority changes never reassign a task or advance its state.</p>
<p id="work-view-summary" class="work-view-summary" aria-live="polite">Loading backlog views…</p>
</div>
<details class="task-create-panel">
<summary>Create a task proposal</summary>
<p class="owner-control-note">Describe the outcome and any domain suggestions. The Session Manager still owns triage and assignment; this form never starts an agent.</p>
<form id="task-create-form" class="task-create-form">
<fieldset data-task-create-fields>
<label>Task title<input name="title" maxlength="200" required autocomplete="off" placeholder="A clear outcome to deliver"></label>
<label>Description<textarea name="description" maxlength="2000" required rows="3" placeholder="Context and the problem to solve"></textarea></label>
<div class="task-create-grid">
<label>Priority<select name="priority"><option value="urgent">Urgent</option><option value="high">High</option><option value="normal" selected>Normal</option><option value="low">Low</option></select></label>
<label>Feature<input name="feature" maxlength="120" autocomplete="off" placeholder="Optional label"></label>
<label>Milestone<input name="milestone" maxlength="120" autocomplete="off" placeholder="Optional label"></label>
</div>
<div class="task-create-grid task-create-routing">
<label>Suggested domains<select name="affectedDomains" multiple size="4" aria-describedby="task-create-route-note"></select></label>
<label>Depends on<select name="dependencies" multiple size="4"></select></label>
</div>
<p id="task-create-route-note" class="owner-control-note">These are routing hints, not assignments. Leave empty for Session Manager triage.</p>
<label>Acceptance criteria<textarea name="acceptanceCriteria" maxlength="1800" required rows="3" placeholder="One verifiable result per line"></textarea></label>
<button type="submit">Review task proposal</button>
</fieldset>
<p class="task-create-status" role="status" aria-live="polite"></p>
<div class="task-create-preview" data-task-create-preview hidden></div>
</form>
</details>
<div id="backlog-board" class="backlog-board" aria-label="Backlog tasks by status"></div>
<details class="check-evidence" id="activity-review"><summary>Activity review</summary><div id="activity-review-content"></div></details>
<p id="priority-action-status" class="priority-action-status" role="status" aria-live="polite" hidden></p>
<p id="task-create-action-status" class="priority-action-status" role="status" aria-live="polite" hidden></p>
</section>
<section id="initiative-progress" class="console-section initiative-section" aria-labelledby="initiative-title">
<div class="section-heading"><div><p class="section-kicker">Derived from the single backlog</p><h2 id="initiative-title">Feature and milestone progress</h2></div><span>Task states only · gates remain separate</span></div>
<p class="initiative-note">Progress is calculated from each linked task's current state. It does not mean checks passed, integration landed, or a release is ready.</p>
<div id="initiative-rollup" class="initiative-rollup" aria-live="polite"></div>
</section>
<section id="fleet" class="console-section fleet-section" aria-labelledby="fleet-title">
<div class="section-heading"><div><p class="section-kicker">Persistent identities</p><h2 id="fleet-title">Fleet and ownership</h2></div><span id="agent-health-note">Session state from latest observation</span></div>
<div class="fleet-columns">
<article class="work-surface"><div class="panel-heading"><h3>Sessions</h3><span>One context per identity</span></div><div id="agent-list" class="agent-list"></div>
<details class="runtime-profile-tools">
<summary>Change a next-launch profile</summary>
<p>Choose which local agent and model an identity will use next time it starts. Running sessions are not changed or restarted.</p>
<form id="runtime-profile-form" data-runtime-profile>
<div class="runtime-profile-fields">
<label for="runtime-profile-area">Identity<select id="runtime-profile-area" name="areaId" required disabled><option value="">Loading identities…</option></select></label>
<label for="runtime-profile-runtime">Agent<select id="runtime-profile-runtime" name="runtime" required disabled><option value="">Loading available agents…</option></select></label>
<label for="runtime-profile-model">Model<input id="runtime-profile-model" name="model" maxlength="180" autocomplete="off" disabled placeholder="For example, gpt-6-luna or sonnet"></label>
<label for="runtime-profile-reasoning">Reasoning<input id="runtime-profile-reasoning" name="reasoning" list="runtime-profile-reasoning-levels" maxlength="40" autocomplete="off" disabled placeholder="Use adapter default"></label>
<datalist id="runtime-profile-reasoning-levels"><option value="low"><option value="medium"><option value="high"><option value="xhigh"><option value="max"><option value="ultra"></datalist>
</div>
<div class="runtime-profile-actions"><button type="button" data-runtime-profile-preview disabled>Review profile change</button><p class="runtime-profile-status" role="status" aria-live="polite">Install a reviewed Fleet before changing profiles.</p></div>
<section class="runtime-profile-preview" data-runtime-profile-review aria-live="polite" hidden>
<div class="runtime-profile-comparison"><div><span>Current</span><strong data-profile-before>—</strong></div><span class="profile-shift" aria-hidden="true">to</span><div><span>Next launch</span><strong data-profile-after>—</strong></div></div>
<p data-profile-effect></p>
<div class="runtime-profile-actions"><button type="button" data-runtime-profile-confirm hidden>Confirm profile change</button><button type="button" class="quiet-action" data-runtime-profile-edit hidden>Edit profile</button></div>
</section>
</form>
</details>
<p id="runtime-profile-action-status" class="runtime-profile-action-status" role="status" aria-live="polite" hidden></p>
</article>
<article class="work-surface"><div class="panel-heading"><h3>Ownership</h3><span>Implementation stays with specialists</span></div><div id="ownership-list" class="console-list"></div></article>
</div>
</section>
<section id="organization" class="console-section organization-section" aria-labelledby="organization-title">
<div class="section-heading"><div><p class="section-kicker">Current approved structure</p><h2 id="organization-title">Organization map</h2></div><span id="organization-revision">No active graph</span></div>
<div id="organization-map" class="organization-map" aria-live="polite"></div>
<div id="organization-lifecycle" class="organization-lifecycle" aria-live="polite"></div>
</section>
<section id="organization-proposals" class="console-section" aria-labelledby="proposal-title">
<div class="section-heading"><div><p class="section-kicker">Owner review</p><h2 id="proposal-title">Organization proposals</h2></div><span id="change-count">0 pending</span></div>
<p id="organization-action-status" class="organization-action-status" role="status" aria-live="polite" hidden></p>
<div id="change-list" class="console-list"></div>
</section>
<section id="communications" class="console-section communication-section" aria-labelledby="communications-title">
<div class="section-heading"><div><p class="section-kicker">Durable coordination</p><h2 id="communications-title">Conversations</h2></div><span id="message-count">0 unacknowledged</span></div>
<details class="owner-request-composer">
<summary>Request work from an agent</summary>
<p>This adds one durable inbox message. It will not create a task, change ownership, or wake/start the agent.</p>
<form id="owner-request-form" data-owner-request>
<div class="owner-request-fields">
<label for="owner-request-recipient">Send to
<select id="owner-request-recipient" name="recipient" required disabled><option value="">Loading Fleet identities…</option></select>
</label>
<label for="owner-request-task">Link an existing task <span>(optional)</span>
<select id="owner-request-task" name="taskId" disabled><option value="">No task reference</option></select>
</label>
</div>
<label for="owner-request-body">What should this agent look at?
<textarea id="owner-request-body" name="body" maxlength="4000" rows="4" required disabled placeholder="Describe the request and any boundary or evidence that matters."></textarea>
</label>
<div class="owner-request-actions">
<button type="button" data-owner-request-preview disabled>Preview request</button>
<p class="owner-request-status" role="status" aria-live="polite">Install a reviewed Fleet before sending owner requests.</p>
</div>
<section class="owner-request-preview" data-owner-request-review aria-live="polite" hidden>
<h3 data-owner-request-title></h3>
<p data-owner-request-effect></p>
<blockquote data-owner-request-exact></blockquote>
<div class="owner-request-actions">
<button type="button" data-owner-request-confirm hidden>Send durable request</button>
<button type="button" class="quiet-action" data-owner-request-edit hidden>Edit request</button>
</div>
</section>
</form>
</details>
<p id="owner-request-action-status" class="owner-request-action-status" role="status" aria-live="polite" hidden></p>
<div id="message-list" class="conversation-stream"></div>
</section>
<section id="evidence" class="console-section evidence-section" aria-labelledby="evidence-title">
<div class="section-heading"><div><p class="section-kicker">Provenance first</p><h2 id="evidence-title">Evidence and review</h2></div><span>Exact commit receipts</span></div>
<div class="evidence-columns">
<article class="work-surface"><div class="panel-heading"><h3>Checks</h3><span>Passes are commit-bound</span></div><div id="check-list" class="console-list"></div></article>
<article class="work-surface review-gallery"><div class="panel-heading"><h3>Screenshots and artifacts</h3><span>Provenance-linked review</span></div><p class="gallery-note">Images are copied into private local project state and checked against their recorded digest when opened. Feedback is previewed with its identity, task, and commit before you choose to send it.</p><p id="feedback-action-status" class="feedback-action-status" aria-live="polite" hidden></p><div id="artifact-list" class="artifact-list"></div></article>
</div>
</section>
<section id="delivery" class="console-section delivery-section" aria-labelledby="delivery-title">
<div class="section-heading"><div><p class="section-kicker">Separate authority gates</p><h2 id="delivery-title">Integration and release</h2></div><span>Integration is not deployment</span></div>
<div class="delivery-columns">
<article class="work-surface"><div class="panel-heading"><h3>Integration</h3><span>FIFO queue · source commit</span></div><div id="integration-list" class="console-list"></div></article>
<article class="work-surface"><div class="panel-heading"><h3>Delivery</h3><span>Release readiness</span></div><div id="delivery-list" class="console-list"></div></article>
</div>
<div id="operation-outcomes"></div>
</section>
<details class="console-section more-details">
<summary>Operations, resources, and decisions</summary>
<div class="operations-grid">
<article class="work-surface"><div class="panel-heading"><h3>Worktrees</h3><span>Branches and recoverability</span></div><div id="worktree-list" class="console-list"></div></article>
<article class="work-surface"><div class="panel-heading"><h3>Resources</h3><span>Shared capacity</span></div><div id="resource-list" class="console-list"></div></article>
<article class="work-surface schedule-surface"><div class="panel-heading"><h3>Schedules</h3><span>Declared lifetimes</span></div><div id="schedule-list" class="console-list"></div>
<section id="manager-wakes" aria-labelledby="manager-wakes-title">
<div class="panel-heading"><h4 id="manager-wakes-title">Manager launches</h4><span id="manager-wake-count">Not available</span></div>
<p>Unknown launches block another scheduled wake. Offline presence does not prove that the agent process stopped.</p>
<div id="manager-wake-list" class="console-list"></div>
</section>
<details class="schedule-launcher-tools" data-schedule-launcher>
<summary>Manage the persistent timer</summary>
<p>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.</p>
<div class="schedule-launcher-state" id="schedule-launcher-state" role="status">Loading installed timer state…</div>
<button type="button" data-schedule-launcher-preview disabled>Review timer setup</button>
<p class="schedule-launcher-status" data-schedule-launcher-status role="status" aria-live="polite"></p>
<section class="schedule-launcher-preview" data-schedule-launcher-review aria-live="polite" hidden>
<h4 data-schedule-launcher-title></h4>
<p data-schedule-launcher-effect></p>
<div data-schedule-launcher-schedules></div>
<div data-schedule-launcher-files></div>
<div class="schedule-launcher-actions"><button type="button" data-schedule-launcher-confirm hidden>Install and start timer</button><button type="button" class="quiet-action" data-schedule-launcher-edit hidden>Edit review</button></div>
</section>
<p id="schedule-launcher-action-status" class="schedule-launcher-action-status" role="status" aria-live="polite" hidden></p>
</details>
</article>
<article class="work-surface"><div class="panel-heading"><h3>Providers</h3><span>Configured capabilities</span></div><div id="provider-list" class="console-list"></div></article>
<article class="work-surface"><div class="panel-heading"><h3>Decisions</h3><span>Tracked project truth</span></div><div id="decision-list" class="decision-list"></div></article>
<article class="work-surface"><div class="panel-heading"><h3>Context locality</h3><span>Measured, never assumed</span></div><div id="context-detail" class="context-detail"></div></article>
</div>
</details>
<footer class="console-footer"><span>TORCH observes local project state. It does not expose private model reasoning.</span><span id="snapshot-provenance">Read-only snapshot</span></footer>
</div>
</main>
</body>
</html>
+1876 -87
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="560" viewBox="0 0 960 560">
<rect width="960" height="560" fill="#101a22"/>
<g font-family="sans-serif" fill="#e8eef3">
<text x="40" y="55" font-size="23">Northstar</text><text x="700" y="55" font-size="14" fill="#9bb0c0">Project home · Wireframe</text>
<path d="M30 80H930" stroke="#344654"/>
<rect x="30" y="105" width="175" height="410" rx="8" fill="#172530"/>
<text x="50" y="150" font-size="17">Workspace</text><text x="50" y="199" font-size="15" fill="#f4b15b">Overview</text>
<text x="50" y="244" font-size="15">Active work</text><text x="50" y="289" font-size="15">Reviews</text><text x="50" y="334" font-size="15">Activity</text>
<text x="235" y="140" font-size="28">Your project, taking shape.</text>
<text x="235" y="175" font-size="15" fill="#9bb0c0">Public beta · Current milestone</text>
<rect x="235" y="205" width="420" height="280" rx="8" fill="#172530"/>
<text x="255" y="240" font-size="19">Active work</text>
<text x="255" y="295" font-size="16">Build the activity feed</text><text x="255" y="320" font-size="14" fill="#9bb0c0">Services · In progress</text>
<path d="M255 345H635" stroke="#344654"/>
<text x="255" y="390" font-size="16">Verify offline recovery</text><text x="255" y="415" font-size="14" fill="#9bb0c0">Quality · In review</text>
<rect x="680" y="205" width="250" height="280" rx="8" fill="#24313a"/>
<text x="700" y="240" font-size="19">Needs your review</text>
<text x="700" y="293" font-size="16">Project home layout</text><text x="700" y="321" font-size="14" fill="#9bb0c0">Interface is waiting on you</text>
<rect x="700" y="360" width="175" height="44" rx="6" fill="#f4b15b"/><text x="718" y="388" font-size="15" fill="#101a22">Review proposal</text>
<text x="235" y="530" font-size="13" fill="#9bb0c0">Illustrative design artifact · Demo data</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+10
View File
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex,nofollow">
<meta http-equiv="refresh" content="0; url=/console.html?demo=1">
<title>TORCH Dashboard</title>
</head>
<body><a href="/console.html?demo=1">View dashboard demo</a></body>
</html>
+302
View File
@@ -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();
}
})();
+49 -46
View File
@@ -3,19 +3,22 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="TORCH turns one Git repository into a coordinated team of persistent AI engineering specialists.">
<title>TORCH — one repository, a coordinated AI engineering team</title>
<meta name="description" content="TORCH studies your project, proposes a fit-for-purpose AI fleet, and coordinates sessions across Codex, Claude, Pi, and other local runtimes.">
<meta name="theme-color" content="#101a22">
<title>TORCH — an AI fleet shaped by your project</title>
<link rel="stylesheet" href="/styles.css">
<link rel="icon" type="image/svg+xml" href="/torch-mark.svg">
<script src="/app.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="masthead">
<a class="wordmark" href="/" aria-label="TORCH home"><span class="mark">T</span> TORCH</a>
<a class="wordmark" href="/" aria-label="TORCH home"><img src="/torch-mark.svg" alt="" width="38" height="38"> TORCH</a>
<nav aria-label="Primary">
<a href="#system">System</a>
<a href="#system">How it works</a>
<a href="/console.html?demo=1" target="_blank" rel="noopener">View dashboard demo</a>
<a href="#evidence">Evidence</a>
<a href="#start">Start</a>
<a href="#start">Get started</a>
<a href="/console">Fleet Console</a>
</nav>
</header>
@@ -23,92 +26,92 @@
<main id="main">
<section class="hero" aria-labelledby="hero-title">
<div class="hero-copy">
<p class="status-line"><span></span> Portable fleet alpha</p>
<h1 id="hero-title">One repository.<br>A coordinated AI engineering team.</h1>
<p class="lede">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.</p>
<p class="status-line"><span></span> A project-aware AI fleet</p>
<h1 id="hero-title">An AI team shaped by your project.</h1>
<p class="lede">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.</p>
<div class="hero-actions">
<a class="primary-action" href="#start">Design a fleet</a>
<a class="primary-action" href="#start">See how to start</a>
<a class="text-action" href="https://github.com/PR0M3TH3AN/TORCH">Read the source</a>
</div>
</div>
<div class="dispatch" aria-label="Interactive TORCH routing board">
<div class="dispatch" aria-label="Interactive TORCH workflow">
<div class="dispatch-head">
<p>Example fleet · Request CR-184</p>
<button id="route-toggle" type="button" aria-pressed="false">Run the route</button>
<p>From project brief to checked work</p>
<button id="route-toggle" type="button" aria-pressed="false">Show the flow</button>
</div>
<ol class="route" id="route">
<li class="route-step is-ready" data-step="0"><span class="node">A</span><div><strong>Owner request</strong><small>Change terrain streaming without weakening frame checks.</small></div></li>
<li class="route-step" data-step="1"><span class="node">S</span><div><strong>Session Manager</strong><small>Routes ownership and establishes acceptance.</small></div></li>
<li class="route-step split" data-step="2"><span class="node">T</span><div><strong>Terrain specialist</strong><small>Works in its persistent domain context.</small></div><span class="peer">QA peer</span></li>
<li class="route-step" data-step="3"><span class="node">✓</span><div><strong>Exact-commit checks</strong><small>Receipts bind evidence to the measured SHA.</small></div></li>
<li class="route-step" data-step="4"><span class="node">M</span><div><strong>Controlled main</strong><small>Authorized fast-forward after convergence.</small></div></li>
<li class="route-step is-ready" data-step="0"><span class="node">P</span><div><strong>Project + specification</strong><small>TORCH maps code, tests, constraints, and stated goals.</small></div></li>
<li class="route-step" data-step="1"><span class="node">T</span><div><strong>Proposed team</strong><small>You review domains, responsibilities, and reporting lines.</small></div></li>
<li class="route-step split" data-step="2"><span class="node">AI</span><div><strong>Your runtime for each session</strong><small>Codex, Claude, Pi, or a trusted local adapter.</small></div><span class="peer">Direct peer links</span></li>
<li class="route-step" data-step="3"><span class="node">↔</span><div><strong>Shared coordination</strong><small>Sessions exchange messages, status, tasks, and evidence.</small></div></li>
<li class="route-step" data-step="4"><span class="node">✓</span><div><strong>Checked integration</strong><small>Work converges through an authorized, serialized path.</small></div></li>
</ol>
<p class="route-readout" id="route-readout" aria-live="polite">Ready at owner request.</p>
<p class="route-readout" id="route-readout" aria-live="polite">Start with the repository. The proposed fleet stays yours to shape.</p>
</div>
</section>
<section class="principle-strip" aria-label="TORCH principles">
<p>Git required</p><p>GitHub optional</p><p>Provider independent</p><p>Local first</p><p>Human approved</p>
<p>Built around your repo</p><p>Codex · Claude · Pi</p><p>Choose per session</p><p>Local-first control</p><p>Owner-shaped fleet</p>
</section>
<section class="system-section" id="system">
<div class="section-intro">
<h2>The project decides the team.</h2>
<p>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.</p>
<h2>The project shapes the organization.</h2>
<p>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.</p>
</div>
<div class="blueprint">
<article>
<span class="blueprint-index">Input</span>
<h3>Code and intent</h3>
<p>Architecture, tests, operations, scarce resources, and external Markdown specifications become evidence—not instructions to execute.</p>
<span class="blueprint-index">Understand</span>
<h3>Read the real project</h3>
<p>Architecture, tests, working practices, shared files, and your selected specifications become evidence for a proposed fleet.</p>
</article>
<article>
<span class="blueprint-index">Design</span>
<h3>Explicit boundaries</h3>
<p>Every domain receives scope, exclusions, owned paths, neighbors, checks, runtime policy, and collision rules.</p>
<span class="blueprint-index">Coordinate</span>
<h3>Give each session a lane</h3>
<p>Roles get explicit scope, exclusions, neighbors, checks, and worktree boundaries. Each session can use the runtime and model that fit its job.</p>
</article>
<article>
<span class="blueprint-index">Operate</span>
<h3>Persistent specialists</h3>
<p>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.</p>
<span class="blueprint-index">Grow</span>
<h3>Change shape when needed</h3>
<p>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.</p>
</article>
</div>
</section>
<section class="context-section">
<div class="context-diagram" aria-hidden="true">
<div class="wide-context">general session<div class="churn"></div><div class="churn second"></div><div class="churn third"></div></div>
<div class="wide-context">one roaming session<div class="churn"></div><div class="churn second"></div><div class="churn third"></div></div>
<div class="versus">versus</div>
<div class="focused-context"><span>terrain</span><span>runtime</span><span>verification</span></div>
<div class="focused-context"><span>domain context</span><span>peer coordination</span><span>project evidence</span></div>
</div>
<div>
<h2>Keep useful context hot.</h2>
<p>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.</p>
<p class="truth-note">Context savings are a measured hypothesis, not a marketing claim.</p>
<h2>Keep the right context close to the work.</h2>
<p>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.</p>
<p class="truth-note">Lower cache and context costs are a hypothesis TORCH still needs to measure.</p>
</div>
</section>
<section class="evidence-section" id="evidence">
<div class="section-intro">
<h2>Proof has a commit.</h2>
<p>TORCH distinguishes implementation from qualification. Green output without the right scenario, authority, and SHA is not proof.</p>
<h2>Trust comes from evidence.</h2>
<p>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.</p>
</div>
<div class="ledger" role="table" aria-label="Current TORCH qualification ledger">
<div class="ledger-row ledger-head" role="row"><span role="columnheader">Capability</span><span role="columnheader">Current evidence</span><span role="columnheader">State</span></div>
<div class="ledger-row" role="row"><span data-label="Capability">Portable kernel and reversal</span><span data-label="Evidence">Scenario tested</span><strong class="proven" data-label="State">Proven locally</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">Durable control plane and backlog</span><span data-label="Evidence">SQLite + tracked tasks</span><strong class="proven" data-label="State">Proven locally</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">Checks, resources, integration</span><span data-label="Evidence">Exact-SHA lifecycle</span><strong class="proven" data-label="State">Proven locally</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">Claude and Codex coordination</span><span data-label="Evidence">Virtualized adapters</span><strong class="pending" data-label="State">Live gate open</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">Self-managed successor</span><span data-label="Evidence">Isolated build + activation</span><strong class="pending" data-label="State">Stable boot open</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">COMBATRIG migration</span><span data-label="Evidence">33 areas, 570 tasks inventoried</span><strong class="pending" data-label="State">Boundaries open</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">Project analysis and fleet design</span><span data-label="Evidence">Repository/spec scenarios</span><strong class="proven" data-label="State">Proven locally</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">Shared messages, status, and tasks</span><span data-label="Evidence">Durable local control plane</span><strong class="proven" data-label="State">Proven locally</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">Safe checks and integration</span><span data-label="Evidence">Exact-commit, serialized flow</span><strong class="proven" data-label="State">Proven locally</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">Codex, Claude, Pi, and local adapters</span><span data-label="Evidence">Adapter and coordination scenarios</span><strong class="pending" data-label="State">Live provider gate open</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">TORCH managing its own repository</span><span data-label="Evidence">Isolated candidate checks</span><strong class="pending" data-label="State">Clean install gate open</strong></div>
<div class="ledger-row" role="row"><span data-label="Capability">COMBATRIG import</span><span data-label="Evidence">Read-only inventory; ownership review needed</span><strong class="pending" data-label="State">Migration gated</strong></div>
</div>
</section>
<section class="start-section" id="start">
<div>
<h2>Start with the work you actually have.</h2>
<p>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.</p>
<h2>Start with the project you already have.</h2>
<p>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.</p>
</div>
<div class="command-panel">
<div class="command-tabs" role="tablist" aria-label="Startup input">
@@ -127,7 +130,7 @@
<footer>
<p><strong>TORCH</strong> is an open, local-first agent fleet for Git projects.</p>
<div><a href="https://github.com/PR0M3TH3AN/TORCH">GitHub</a><a href="/console">Local console</a><a href="https://github.com/PR0M3TH3AN/TORCH/blob/main/docs/PORTABLE_AGENT_FLEET_SPEC.md">Specification</a></div>
<div><a href="https://github.com/PR0M3TH3AN/TORCH">GitHub</a><a href="/console">Local console</a><a href="https://github.com/PR0M3TH3AN/TORCH/blob/main/docs/PORTABLE_AGENT_FLEET_SPEC.md">How TORCH works</a></div>
</footer>
</body>
</html>
+51
View File
@@ -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 });
})();
+29
View File
@@ -0,0 +1,29 @@
(() => {
const text = (value) => String(value ?? '').slice(0, 1200).replace(/[&<>"']/g,
(character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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 `<h3>Canonical fetch outcomes</h3><p>Object imports do not move branches or deploy releases. Latest 20 recorded operations.</p>
${fetches.length ? fetches.map((item) => `<details class="check-evidence"><summary>${text(item.remote)} · ${status(item.state, 'fetch')}</summary>
<p>Branch ${text(item.branch)} · commit <code>${text(item.commit)}</code></p><p>${text(item.updatedAt)}</p>
<ul>${(item.attempts ?? []).slice(0, 5).map((attempt) => `<li>Attempt ${text(attempt.ordinal)}: ${status(attempt.state, 'fetch')} · ${text(attempt.classification ?? 'result not recorded')}</li>`).join('')}</ul>
</details>`).join('') : '<p>No canonical fetch receipts recorded.</p>'}
<h3>Delivery operation outcomes</h3><p>Uncertain or interrupted external actions need review. No automatic retry or recovery is offered here.</p>
${operations.length ? operations.map((item) => `<details class="check-evidence"><summary>${text(item.operation)} · ${text(item.provider)} · ${status(item.state, 'delivery')}</summary>
<p>Delivery ${text(item.deliveryId)} · commit <code>${text(item.commit)}</code> · ${text(item.updatedAt)}</p>
<ul>${attempts.filter((attempt) => attempt.operationId === item.id).slice(0, 5).map((attempt) => {
const receipt = attempt.receiptSummary;
return `<li>Attempt ${text(attempt.ordinal)}: ${status(attempt.state, 'delivery')}
<p>${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.'}</p></li>`;
}).join('')}</ul></details>`).join('') : '<p>No delivery operation receipts recorded. Landing is not deployment.</p>'}
<p>${text(snapshot.deliveryEvidenceCoverage ?? 'Receipt history may be incomplete. Inspect CLI evidence for details.')}</p>`;
}
globalThis.TorchOperationOutcomes = Object.freeze({ render });
})();
+50
View File
@@ -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) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
})[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 = '<p class="briefing-muted">Evidence unavailable.</p>';
else if (!items.length) body = '<p class="briefing-muted">No matching recorded items.</p>';
else body = `<ul>${items.map((item) => `<li>${describe(item)}</li>`).join('')}</ul>`;
const total = count(section?.count);
if (section?.truncated || (total !== null && total > items.length)) body += `<p class="briefing-muted">Showing ${items.length} of ${total ?? 'an unknown total'}.</p>`;
return `<div class="briefing-block"><h3>${title}</h3>${body}</div>`;
}
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 '<div class="briefing-empty"><h3>No published briefing</h3><p>Generate a local report with <code>torch digest build</code>. To save it for this view, the owner can review and run <code>torch digest publish --by owner --yes</code>.</p><p class="briefing-muted">Publication does not send a message, deploy work or install a timer.</p></div>';
}
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 `<div class="briefing-publication"><p><strong>${freshness}</strong> <time datetime="${text(report.generatedAt)}">${text(report.generatedAt)}</time></p>
<p class="briefing-muted">Window: ${text(report.window?.since)} to ${text(report.window?.until)} (UTC). Recorded evidence, not independent live verification.</p>
<p class="briefing-muted">This briefing stays as published. Approvals and work may have changed since then.</p>
${report.activityCoverage?.complete === false ? '<p class="briefing-warning">Activity history is incomplete; inactivity is not proven.</p>' : ''}
<div class="briefing-focus">${list('Needs you', report.needsOwner, (item) => `<strong>${text(item.title)}</strong><span>${text(item.requester)} · ${text(item.id)}</span>`)}
${list('Delivery needs review', report.deliveryReview, (item) => `<strong>${text(item.operation)}: ${text(states[item.state] ?? item.state)}</strong><code>${text(item.commit)}</code>`)}</div>
<p><a href="#flow-watch">Review current approvals and session waits</a></p>
<div class="briefing-deployment"><h3>Last deployment</h3>${deployment
? `<p><strong>${text(deployment.label)}</strong> — ${text(states[deployment.state] ?? deployment.state)}</p><code>${text(deployment.commit)}</code>${deployment.failureReason ? `<p>${text(deployment.failureReason)}</p>` : ''}`
: '<p class="briefing-muted">No deployment operation recorded.</p>'}</div>
<details class="briefing-detail"><summary>Shipping and integration</summary>
${list('Shipped — receipt reported', report.shipped, (item) => `<strong>${text(item.label ?? item.deliveryId)}</strong><span>${text(item.state)} · ${text(item.recordedAt)}</span><code>${text(item.commit)}</code>`)}
${list('Landed — not necessarily deployed', report.landed, (item) => `<strong>${text(item.sourceArea)}</strong><code>${text(item.commit)}</code>`)}
${list('Completed tasks', report.completed, (item) => `<strong>${text(item.title)}</strong><span>${text(item.taskId)}</span>`)}</details>
<details class="briefing-detail"><summary>Decisions, blockers and neglected requests</summary>
${list('Recorded approval decisions', report.decisions, (item) => `<strong>${text(item.title)}</strong><span>${text(item.status)} · ${text(item.decidedBy)}</span>`)}
${list('Blocked work', report.blocked, (item) => `<strong>${text(item.taskId)}</strong><span>${text(item.reason)}</span>`)}
${list('Neglected owner requests', report.neglectedRequests, (item) => `<strong>${text(item.title)}</strong><span>${text(item.taskId)} · ${text(item.ageDays)} days${item.expectedWaiting ? ' · recorded blocked wait' : ''}</span>`)}</details>
<details class="briefing-detail"><summary>Evidence coverage</summary><ul>${(Array.isArray(report.coverage) ? report.coverage : []).slice(0, 100).map((note) => `<li>${text(note)}</li>`).join('')}</ul></details></div>`;
}
globalThis.TorchOwnerDigest = Object.freeze({ render });
})();
+521 -70
View File
@@ -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) {
+28
View File
@@ -0,0 +1,28 @@
(() => {
const text = (value) => String(value ?? '').slice(0, 1200).replace(/[&<>"']/g,
(character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
function render(activity) {
if (!activity?.available) return '<p>Commit activity unavailable. No inactivity conclusion can be drawn. Use task history and current session state to review work.</p>';
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) => `<h4>${title} (${items.length})</h4>${items.length ? `<ul>${items.slice(0, 40).map((task) =>
`<li><strong>${text(task.title)}</strong> <code>${text(task.taskId)}</code>
<p>${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.' : ''}</p>
${task.lastActivity ? `<p>Last named commit <code>${text(task.lastActivity.commit)}</code> at ${text(task.lastActivity.committedAt)}.</p>` : ''}
${(task.closureIntent ?? []).length ? '<p>Closure mentioned in a commit; only verified landing can close the task.</p>' : ''}</li>`).join('')}</ul>` : '<p>None in this observation.</p>'}
${items.length > 40 ? `<p>Showing 40 of ${items.length}; inspect CLI activity for the remainder.</p>` : ''}`;
return `<p>Named-commit activity as of ${text(activity.generatedAt)}. Metadata edits do not reset inactivity. This is not a measure of uncommitted work.</p>
<p>${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.</p>
${(activity.unavailableBranches ?? []).length ? `<p>Unavailable branches: ${activity.unavailableBranches.map(text).join(', ')}.</p>` : ''}
${activity.truncated ? '<p>Commit scan truncated.</p>' : ''}
${(activity.futureCommits ?? []).length ? '<p>Future-dated commits excluded from activity; review repository clocks.</p>' : ''}
${group('Stale work — owner requests first', stale)}${group('Unknown activity — owner requests first', unknown)}
<p>Review against the authoritative work board. Observation never changes task state or closes work.</p>`;
}
globalThis.TorchTaskActivity = Object.freeze({ render });
})();
+17
View File
@@ -0,0 +1,17 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="handleGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#4A5568"/>
<stop offset="100%" stop-color="#2D3748"/>
</linearGradient>
<linearGradient id="flameGrad" x1="32" y1="4" x2="32" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#FFD700"/>
<stop offset="40%" stop-color="#FF8C00"/>
<stop offset="100%" stop-color="#FF4500"/>
</linearGradient>
</defs>
<path d="M26 42 L29 60 L35 60 L38 42 Z" fill="url(#handleGrad)"/>
<rect x="24" y="40" width="16" height="4" rx="1" fill="#718096"/>
<path d="M32 2 C32 2 16 18 16 30 C16 38 22 42 32 42 C42 42 48 38 48 30 C48 18 32 2 32 2Z" fill="url(#flameGrad)"/>
<path d="M32 10 C32 10 22 22 22 30 C22 36 26 38 32 38 C38 38 42 36 42 30 C42 22 32 10 32 10Z" fill="#FFF700" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 949 B

+40
View File
@@ -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));
+144
View File
@@ -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));
+41 -3
View File
@@ -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,
+50 -7
View File
@@ -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,
+4
View File
@@ -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;
}
+212
View File
@@ -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);
}
+182
View File
@@ -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 });
}
+229
View File
@@ -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 };
}
+93
View File
@@ -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);
}
+120
View File
@@ -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;
}
+420
View File
@@ -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(); }
}
+90
View File
@@ -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,
};
}
+404 -34
View File
@@ -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}`,
+100
View File
@@ -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' };
}
+100
View File
@@ -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 };
}
+30
View File
@@ -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' }; }
}
+257 -7
View File
@@ -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));
});
}
+144
View File
@@ -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 };
}
+856 -78
View File
File diff suppressed because it is too large. Load diff
+1208 -4
View File
File diff suppressed because it is too large. Load diff
+266 -4
View File
@@ -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
+117
View File
@@ -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 },
});
}
}
+107 -6
View File
@@ -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);
}
}
+122 -1
View File
@@ -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');
+51 -1
View File
@@ -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.',
},
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -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`, {
+80
View File
@@ -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');
+61 -6
View File
@@ -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' },
+165 -35
View File
@@ -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),
};
}
}
+7 -3
View File
@@ -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(),
+195 -3
View File
@@ -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) {
+65 -7
View File
@@ -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 {
+98 -22
View File
@@ -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 ?? '<missing>'} is incomplete`);
+60 -2
View File
@@ -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');
}
+43 -26
View File
@@ -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 });
+242
View File
@@ -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('.') : '<root>',
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,
};
}
+16 -3
View File
@@ -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');
}
+7 -3
View File
@@ -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}`),
});
+205 -4
View File
@@ -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 {
+152
View File
@@ -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 };
}
+168
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.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;
}
}
+362 -15
View File
@@ -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;
}
+64
View File
@@ -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,
};
}
+222 -59
View File
@@ -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 <your-id> --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,
};
}),
};
+102 -3
View File
@@ -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,
};
}
+314 -10
View File
@@ -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(),
+24 -2
View File
@@ -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,
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more