Merge canonical for TASK-pilot-cli-suite-timeout

This commit is contained in:
thePR0M3TH3AN committed 2026-10-07 02:17:32 -04:00
commit 0c8db11bcc
41 files changed
+4544 -24

No files matched your search

+64
View File
@@ -2,6 +2,70 @@
Repository state outranks conversation memory. Query ownership before crossing a domain boundary.
## Owner-requested restart and continuation repair (2026-10-03 UTC)
The owner said "yeah fix the issue" after the arbiter verified that the fleet
had stopped making progress. The arbiter implemented a schema-compatible,
owner-only manual legacy recovery and a bounded deterministic continuation pilot.
These are source changes pending independent QA and installed-release acceptance,
not completed umbrella tasks. Read
docs/spec_changes/2026-10-03-legacy-runtime-recovery.md. The arbiter may apply exact
tested stopped-invocation recovery and resume the existing seven identities with
the source CLI. Preserve old unknown/failure receipts and existing unique commits.
The source controller may launch only the explicitly approved existing roster,
under its daily turn cap, event deduplication and per-identity physical turn guard.
It does not grant agents permission to start providers or bypass budgets, resource
leases, approval boundaries or task ownership. Existing legacy manager-wake
configuration stays disabled; this source pilot is a distinct, bounded mechanism.
Manager should process current inbox and route real next work rather than leaving
it at a proposal-only loop. QA may independently review the exact new source and
scenario packet now; no fabricated QA acceptance is implied by owner implementation.
## Owner priority update: reliability before visual polish (2026-10-03 UTC)
The owner requested that TORCH's operating defects be fixed first so the fleet
benefits from improved operation. This supersedes the UI-first ordering below,
not the existing resume authorization or safety boundaries. The authoritative
backlog priorities were changed through the revision-checked owner API.
Sequence the smallest safe current-schema stopped-unknown recovery and CLI
qualification first; then truthful lifecycle/status and their source-safety
prerequisites; then qualified bounded continuation/event-driven wakes under
pause, budget and authority policy; then actionable operational attention.
Compact cards/detail modals and coordinator Chat remain high-priority work after
these reliability gates. Do not require the entire future company architecture
to deliver a safe minimal recovery. Reprioritization does not resolve blockers,
grant test consent, enable automatic wakes or launch agents. Preserve task owners,
dependencies and evidence; explicitly block/resequence existing assignments when
needed rather than replacing them silently or creating concurrent assignments.
## Owner-approved fleet resume and priority polish cycle (2026-10-02)
The owner explicitly said: "We got a reset. lets get torch as pollished as we can.
Spin them all back up." This lifts the earlier manual wind-down for the existing
seven identities. The arbiter controls physical launches and timer restoration;
automatic AI wakes remain disabled until their runtime safety gates qualify.
Do not interpret old pause messages as a new pause after this authorization.
Session Manager may triage and assign the existing development roadmap within
approved ownership, one active implementation assignment per specialist, direct
peer coordination and independent QA. The earlier first-slice/audit-only limits
below are superseded for this development cycle. Preserve existing unfinished
assignments and resolve or explicitly block them rather than silently replacing
them to take new work. Prioritize compact cards/detail modals and coordinator-first
agent Chat, then broader polish; runtime recovery required for safe operation comes
first. Read docs/CONSOLE_CHAT_AND_COMPACT_CARDS_SPEC.md and the newer owner inbox.
Company/KB proposals remain roadmap requirements, not implemented capabilities.
Reconcile the three prior working/unknown records through supported audited
recovery, with actual stopped-executor evidence. No peer impersonation, direct
SQLite mutation, silent guard removal or false PASS promotion. Current model/
provider profiles stay unchanged. Existing checked integration and non-force
development push authority remains; no default-main/legacy rewrite, destructive
cleanup, extra identities, package publication or unreviewed deployment is granted.
Record honest progress and blockers; end completed turns without polling loops.
## Fresh owner authorization: guarded integration and continued development (2026-10-01)
The owner explicitly answered the arbiter's two held decisions: "yes do whatever
+243
View File
@@ -0,0 +1,243 @@
# Adaptive software company: integration plan
Status: owner-approved roadmap direction, 2026-10-01; implementation pending.
Applies to the Portable Agent Fleet. This document records the supplied adaptive
software company proposal and the agreed refinements. It does not certify the
current alpha as unattended or production-ready. The fleet remains manually
paused until the owner explicitly requests resume.
## Product boundary
TORCH supplies organization, ownership, intent, authority, durable work,
coordination, evidence, verification, integration and recovery. Coding harnesses
supply implementation intelligence, tools and temporary delegation. Start with
the smallest useful organization; do not require a fixed Engineering/Product/QA
taxonomy or a new permanent AI supervisor. Projects may use different roles.
Preserve worktree isolation, one active implementation assignment per specialist,
revision-checked mutations, direct peer communication, resource leases, exact
candidate verification, serialized integration, independent review where required,
audits and separate release/deployment authority. KB/handbook/working-memory
requirements remain governed by [Project knowledge](PROJECT_KNOWLEDGE_SPEC.md).
## Existing work: extend, do not replace
This is a staged extension of current systems, not a rewrite. Reuse the current
identity, organization, backlog, messaging, runtime, checks, resource, integration,
delivery and schedule services. Owner-approved grants are already proposed as
`TASK-874ed1e4-9414-4a8f-8575-a6c5aceefc56`; do not build a competing grant system.
Link first-work intake, event-driven wakes, usage governor, hierarchy pilots,
starting-presence and provider update-path tasks instead of duplicating them.
The earlier executor-outcome task is completed, but later pilot launches exposed
stdout-bound/terminal-outcome disagreement. A completed historical task is not
proof that the later installed runtime is ready for unattended operation.
Preserve those receipts and qualify the new regression explicitly.
## Phase A: trustworthy runtime state before unattended coordination
Lead domains: Provider Runtime, with Project Kernel and independent QA.
1. Reproduce the installed output-bound failure using bounded deterministic
fixtures. Keep parser/transport bounds; do not treat a wrapper's observation
as a substitute for an authenticated runtime outcome.
2. Define startup reservation, process/session ownership, terminal outcome and
recovery contracts. Distinguish success, failure, unknown and physically stopped.
3. Publish guarded starting presence before launching. Coordinate manual,
scheduled and controller paths through the same exclusivity mechanism.
4. Add a supported, owner-attributed recovery operation with executor-stopped
evidence, revision/lease checks and audit. Never impersonate a specialist,
edit SQLite manually, infer death from a name or drop an unknown reservation.
5. Qualify manual pause across reboot, quota reset, schedule due, message arrival,
upgrade and event replay. Only explicit owner resume can clear this pause.
Exit gate: oversized output, timeout, signal, malformed terminal data and a crash
between spawn and receipt cannot produce false success or a duplicate launch;
unknown outcomes remain recoverable and block unsafe actions. Actual installed
runtime qualification and independent QA are required, not just fixture tests.
## Phase B: scoped capability authority migration
Lead: Project Kernel; extend the existing owner-grants proposal.
Identity identifies the participant; role describes responsibility; an approved
grant confers authority. Reporting edges and implementation ownership do not
confer management capabilities. Never interpret delegated user authority as
permission to impersonate the owner or act outside its explicit scope.
Before coding, inventory identity-special business rules and their CLI, MCP,
Console, scheduled and internal entry points. Define a single authorization
service and a capability mapping for each action. Grant records must identify:
- grant ID, issuer/delegation provenance, recipient and required capability;
- project/domain/task/path scope and operation constraints;
- approval/reference evidence and policy/grant revision;
- validity/revocation and whether further delegation is allowed;
- owner-reserved exclusions and independent-review conflict rules.
Initially migrate the current Session Manager's permitted operations without
widening permissions. Support preview and explicit approval of broader grants.
No default wildcard capabilities, hierarchy inheritance or self-granting.
Evaluate the current grant immediately before effects; audit the actual actor,
grant, scope, relevant revisions and allow/deny reason.
Exit gate: old permitted actions still work under equivalent grants; ungranted,
out-of-scope, revoked and stale actions fail across every entry point. A new lead
can be assigned a bounded capability without source changes. Independent review
cannot approve its own work merely because it also holds a leadership role.
## Phase C: durable event contract
Lead: Work & Integration, with Kernel/Runtime; extend existing event-driven wakes.
Version events for work-ready/blocked, assignment completion, verification,
integration readiness, approval requests/decisions, pending messages, resource
release, runtime observations, schedule due and evidence-backed organization
pressure. Existing audit records are evidence, not automatically executable orders.
Each event needs an ID, project, type/schema version, source entity and revision,
actor/cause, timestamp, correlation/causation IDs and deduplication key. Keep
payloads bounded and avoid credentials, private document bodies or full transcripts.
Define emission and recovery at each state-change boundary. Use transactional
outbox semantics where one store supports them; when Git/files and SQLite cross
a boundary, specify a durable journal/reconciliation protocol rather than claiming
one atomic transaction. Preserve one authority for task state. Define cursor,
retention, replay and schema-upgrade behavior before enabling consumers.
Exit gate: crash before/after state mutation or event delivery, duplicate delivery,
out-of-order delivery and replay cannot lose a significant transition or authorize
a stale action. Event delivery may be at-least-once; effects must be idempotent.
## Phase D: small desired-state reconciler
Lead: Work & Integration / Provider Runtime. No strategy or implementation logic.
For each supported action, observe authoritative current state, compare with
approved policy and desired work, evaluate eligibility, reserve one bounded action,
recheck revisions/authority/pause/budget, execute and record the outcome.
Eligibility is allow, deny or unknown; unknown is not permission to retry.
Begin with durable notices and read-only explanations. Then pilot wake of one
eligible identity for assigned work, followed by narrowly configured ready-work
self-claim and integration-authority notifications. Do not automatically approve,
land, release, deploy, reassign ownership or create identities just because an
event arrived. Preserve existing checks and authorization gates for each operation.
Require persisted pause/backoff, launch reservations, concurrency limits,
bounded retry rules, actionable failures and periodic safety reconciliation.
Queue/backend/usage/model/auth unavailability must not create busy model loops.
Check budgets before launching, including uncertainty in subscription usage.
Use minimum useful wake context; a timer is a safety net, not another AI supervisor.
Exit gate: duplicate events cannot duplicate execution; paused or uncertain work
is never restarted; missed events are found by reconciliation; stale observations
cannot override a newly revoked grant. Start with an owner-approved bounded pilot,
manual intervention metric and immediate pause/rollback path.
## Phase E: native execution capabilities and repository evidence
Runtime lead: Provider Runtime. Repository evidence lead: Project Kernel.
Keep execution capabilities separate from lifecycle functions. Describe native
subagents/delegation, browser/computer use, artifacts, worktrees, background tasks,
checkpoints and review as advertised, locally available, qualified, unavailable
or unknown. Bind qualification to executable/version, host, account/config and
evidence; declarations alone do not prove availability. No model calls merely
to populate the inventory without approval.
Temporary harness workers are accountable to the persistent TORCH specialist,
not new roster members. They must not acquire broader TORCH credentials, authority,
ownership or uncontrolled resource access. Keep delegation provenance and aggregate
available cost evidence. Coordinate native worktrees with TORCH-owned workspaces.
Native review is useful but not automatically independent QA. Avoid orchestration
squared and promote workers only through evidence-backed organizational proposals.
Extend deterministic reconnaissance with bounded workspace/build/module graphs,
CODEOWNERS, co-change observations, entry points, schemas, deployment/process
boundaries and recurring cross-domain tasks. Give the Architect provenance,
coverage and uncertainty, not invented semantic conclusions. Do not execute
untrusted build tools or fetch history merely to analyze a repository silently.
Exit gate: absent capabilities have honest fallbacks; helper delegation cannot
expand authority; organization proposals cite real evidence and remain advisory.
## Phase F: owner-oriented onboarding and evidence UX
Leads: Owner Console, Release/Self-host and Kernel. Extend first-work intake.
Offer one guided path through prerequisites/runtime qualification, analysis,
small-team proposal and rationale, ownership/check gaps, explicit review,
installation/worktree preview, first-work intake and optional startup. Preserve
advanced CLI and diagnostics. Do not redefine the existing read-only `init` into
a mutating command without a documented compatibility decision and tests.
"Company ready" must distinguish configured, launchable and actually operational.
Missing models/auth, unknown outcomes, unresolved ownership and unqualified checks
must remain visible. Separate consent for downloads, spending, persistent services,
agent startup and publication; onboarding cannot override a manual pause.
Show conclusions, reason, evidence and exact artifacts progressively, with stable
links to commits, check conditions, receipts, decisions and discussions. Derive
implemented/verified/independently reviewed/integrated/released/live states from
their actual authorities. Surface unknown coverage and rejected/relaxed criteria;
green exit alone does not support a claimed business outcome or performance number.
Exit gate: a fresh user can reach useful reviewed work without knowing SQLite,
runtime IDs or schedule unit internals; desktop/mobile/keyboard browser inspection
passes. Every important asserted status can be traced to exact evidence.
## Phase G: adaptive organization and storage investigation
Extend existing evolution/hierarchy pilots to evidence-backed contraction,
promotion and consolidation. Roles must justify recurring outcomes rather than
headcount. Record baseline/window, hypothesis, costs, success/stop criteria and
owner-reviewed pilot. Transfer authority, ownership, docs and unfinished work
safely; never retire an active/dirty/unknown worker. Keep one implementation owner.
Measure bookkeeping friction before changing storage: dirty manager time,
administrative commits, collisions, checkpoint effort and integration delay
attributable to state. Compare current tracked records, dedicated state branch
and Git-intent/SQLite-operations alternatives against offline recovery, export,
backup and migration requirements. Do not dual-write authoritative task state or
move it until a separate reviewed decision and recovery tests justify the change.
## Phase H: multi-project and release qualification
Use separate controlled fixtures/pilots for a small web app, monorepo, compiled
systems project, graphics/game project and optional docs/data-heavy project.
Record init-to-useful-team, request-to-assignment, verified integration latency,
owner interventions, coordination volume, launch failures, false greens and
measured/estimated usage separately. Evaluate organization changes for actual benefit.
Do not run expensive/live-provider experiments without owner-approved budgets.
Release gates include migrations/backup compatibility, crash-safe lock recovery,
model/auth preflight, repository relocation, provider update compatibility,
distribution provenance/signing decision, atomic upgrade/rollback, event replay
and platform-specific schedule qualification. Remote execution is optional and
needs a separate trust/deployment boundary, not an implicit requirement.
## Integration and rollback discipline
- Assign bounded slices through the existing backlog; agree shared contracts first.
- Add scenario-based fault and authority tests with Test Integrity Notes. Do not
weaken accepted scenarios or substitute simulated success for installed evidence.
- Land exact checked commits through the existing integration mechanism, not
concurrent direct pushes or a second queue. Keep runtime/parser/security work
independently qualified from marketing and UI changes.
- Ship additive, disabled-by-default policy/config first. Migrations need explicit
version/compatibility rules, backup/restore tests and an upgrade preview.
- Do not roll back a database by blindly activating old code. Define supported
data downgrade or fail closed while preserving the new store and receipts.
- Enable notifications before wakes; qualify one-domain pilot before broad fleet
activation. Fresh source is not automatically an installed runtime upgrade.
- Give the owner a report of configured/enabled/qualified state and remaining gates.
## Backlog index
The authoritative proposals use `TASK-company-` IDs: runtime-recovery,
authority-migration, event-contract, reconciler, native-capabilities,
repository-evidence, onboarding, evidence-ux, organization-experiments,
storage-study and multi-project-qualification. Existing tasks remain authoritative
for their previously scoped work. New proposals are unassigned until Manager
triage after explicit owner resume; this document is not another work queue.
@@ -0,0 +1,91 @@
# Priority Console upgrade: compact cards and agent Chat
Status: owner-approved development requirement; completion is determined by the
authoritative backlog and exact installed/browser evidence, not this document.
On 2026-10-03 the owner promoted TASK-owner-agent-chat,
TASK-console-agent-chat-ui and TASK-console-chat-cards-qualification to urgent.
The fleet is explicitly resumed. Existing runtime safety gates and any subsequent
owner pause still apply.
## Immediate owner-facing milestone
Prioritize a minimal usable Chat: approved coordinator selected by default,
activated-agent selector, durable per-agent history and message composer with
explicit destination and truthful queued/handled status. Session Manager should
assign eligible Console/Runtime/QA contributions without stealing active work.
Keep recovery prerequisites progressing alongside this owner-facing milestone.
If history/composer can safely ship before automatic wake qualification, propose
a named, scoped slice through normal triage; do not silently remove dependencies.
Paused or unqualified messaging may persist but must not trigger provider starts.
## Compact cards and detail modal
Make card-based views a scanning layer rather than full-document readers. Apply
a shared collapsed-height/max-height policy across data types, with consistent
heights within each grid/view and responsive size variants. Do not squeeze tables,
charts or conversations into an arbitrary universal card height.
Show title, state, accountable agent, a two/three-line summary and important
blockers or decisions. Clamp overflowing ordinary content with an explicit
View details action. Critical warnings must remain identifiable and accessible;
provide a concise warning summary and full detail rather than clipping it silently.
Retain sensible quick actions such as approve/reject on collapsed cards, using
existing evidence/revision/preview confirmation and authority checks.
Clicking the card's detail affordance opens an accessible modal with complete
content, evidence, linked conversations/tasks and contextual actions. Nested
action buttons must not accidentally open the modal or trigger another mutation.
Modal content may scroll. Preserve underlying view/filter/scroll and unsent input;
support keyboard access, focus containment/restoration, Escape, labelled headings,
mobile full-screen details and explicit stale/deleted-record states. Refresh must
not replace unsaved edits or apply an action against an obsolete revision.
Start with Work and Needs Your Attention, then cover remaining card-based views
with the same pattern. Do not replace specialist views or create duplicate data.
## Conversational Chat view
Add Chat immediately below Overview in the sidebar. Default to the highest-level
approved project coordinator, currently Session Manager, not a permanently
hard-coded identity. Show an empty/explanatory state if none is configured; never
create or activate an agent to satisfy the screen.
Provide a familiar chronological message stream, composer and selector for every
activated fleet identity. Persistently show Talking to…, identity, role, runtime
availability and a distinct visual accent. Colour is supplementary: labels,
avatars/initials and accessible contrast must also distinguish participants.
Combine durable owner exchanges and publishable inter-agent exchanges involving
the selected agent, preserving sender, recipient, thread/context and causal links.
Clearly distinguish direct owner conversation from collapsible routine fleet
traffic; replying must explicitly target the selected agent or named thread and
never silently broadcast. Do not claim every harness transcript is available.
Reuse existing owner-agent-chat/messages and formal Conversations audit view,
not a parallel conversation database. Include links to tasks, approvals, images
and supporting evidence. No private chain-of-thought or indiscriminate terminal,
secret or private-file exposure.
Stream publishable replies/progress only when the adapter supports it; otherwise
show honest durable-message updates. Expose queued/delivered/being-handled/failed
outcomes accurately: acknowledgement alone does not mean work is done. Preserve
drafts per recipient, history on navigation/reload, reconnect deduplication and
an explicit send destination. New arrivals must not force-scroll a user reading
history. Keep the composer usable on mobile and by keyboard.
Messages can be saved while paused without waking an agent. Future wake-on-message
requires separately qualified reservations, budget, authority and owner-approved
policy. Casual chat does not bypass an explicit approval or grant new authority.
Use clearly labelled demo fixtures with no live APIs, not private fleet history.
## Acceptance and delivery
Use realistic long content, many senders, agent switches, interrupted delivery,
stale records and pending owner decisions. Inspect desktop/mobile/keyboard flows
in an actual browser and retain screenshots. Prove bounded collapsed layout,
complete details, action correctness, focus restoration, preserved filters/drafts,
safe refresh, accurate chat attribution and no launch on paused send.
Existing backlog proposals remain authoritative. New high-priority slices cover
compact cards/details, Chat presentation and independent browser qualification.
Runtime wake safety remains a prerequisite for execution, not a reason to delay
safe read-only/chat-intake UI development.
+107
View File
@@ -0,0 +1,107 @@
# Operational flow improvements and Ops responsibility pilot
Owner approved 2026-10-03. Extends SELF_HEALING_QUALIFICATION_PLAN.md and existing
services, backlog and authority. No additional standing agent is created.
## Operational incidents and wait resolution
An incident connects the existing task, message, approval, check, resource and
runtime evidence. It does not become a second task ledger. Record stable cause
identity, accountable resolver, affected work, first/last observation, next action,
configurable response deadline, escalation authority and resolution evidence.
Keep received, acknowledged, acted-on and resolved distinct. A reply, timeout,
state churn or healthy doctor result cannot resolve the underlying condition.
Acceptance scenarios:
- A waits on B, B acknowledges but does not decide: A remains waiting; a due
deadline produces one actionable escalation to the correct manager.
- A current decision resolves the wait: a refreshed observation removes the
active escalation, preserving original history and exact decision evidence.
- Duplicate, reordered or missed notifications: one incident per exact cause,
periodic reconciliation catches the missing notification; unrelated candidates,
scopes and approvers remain separate.
- Manager is offline or authority revoked: deterministic monitoring persists
the wait and routes only to a currently authorized resolver; it cannot assume
approval, impersonate a manager or launch a duplicate executor.
- Quota/auth failure or an uncertain live executor: persisted hold/backoff and
guarded recovery remain; repeated incident notices do not create model loops.
Reuse existing event/reconciler, approval-dedup and attention tasks. Contract/API
changes require owned review, revision checks and independent QA; no prompt-only
claim of successful incident handling.
## Source adoption and test/QA handoffs
Provide a supported, attributable path from a legitimate contributor/release
candidate to registered ownership and native exact-candidate qualification.
Bind source and destination identities, immutable SHA, ownership/path scope,
required contributor consent and actual engine/adapter provenance. Preview
current branch/worktree/assignment/guard/lease state before any mutation.
- A clean exact owner-approved release candidate can be transferred or composed
through the supported route and produce fresh native check receipts, independent
review and serialized integration without a manual branch reset.
- Dirty destination, active task/check/lease, stale source or changed approval
refuses/defer with an exact remedy and responsible owner; preserve every byte
and unique commit. Installation activation never counts as canonical landing.
- Interrupted/repeated adoption is idempotent; preserve original lineage and
failure evidence. Never fabricate receipts or silently change scope.
- Test handoff has a named test implementer, exact permitted paths/scenarios,
candidate/revision binding and reviewer. Approved additive tests can proceed
without repeatedly seeking the same consent; new scope needs a new decision.
- QA rejection returns an actionable issue to a named owner. QA acceptance is
exact and independent, never a blanket authority or weakened test criterion.
Extend the existing candidate-source qualification and authority services, not a
parallel landing queue. Agree owned interfaces and contribution boundaries once,
then validate them mechanically. Preserve one active assignment per specialist.
## Additive acceptance for existing Chat and evidence tasks
Applies to TASK-owner-agent-chat, TASK-console-agent-chat-ui,
TASK-company-evidence-ux and TASK-console-attention-actions. Retain all original
criteria. The installer currently has no owner criterion-amendment API; attach
this owner-approved addendum by durable references, with manager-owned tracked
reconciliation rather than unsupported live task-file rewriting.
- Message status distinguishes queued, delivered/observed, acknowledged,
action started, decision/result and failed; unsupported stages remain unknown.
- Configurable response expectations show overdue requests and next resolver,
using deterministic reconciliation without duplicate wakes or owner nagging.
- Explicit permission-denied/provider-unavailable/offline states explain delay;
measured response latency is not an invented completion-time guarantee.
- Separate process activity from implementing, checking, coordinating and waiting.
Progress includes timestamp/source and links to exact changes/checks/decisions;
unavailable evidence is unknown, not idle, success or a fabricated percentage.
- A continuously active agent with an unresolved wait is visibly waiting or
coordinating; acknowledgements and unchanged status reports never imply useful
delivery. Display owner interventions and verified integration separately.
- Desktop/mobile/keyboard and stale/reconnected observation scenarios preserve
current modal accessibility, recipient identity, drafts and approval boundaries.
## Ops responsibility pilot
Provider Runtime is the initial Ops engineering contact, with Work and Integration
and Release collaborating within their existing implementation ownership.
Session Manager retains priority/assignment authority. This request creates no
new role capability, worktree, model choice or overlapping implementation owner.
Manager must arrange eligible task slices without overloading the existing active
assignment. Native temporary helpers stay under their responsible identity and
are used only when supported by the approved runtime policy.
Deterministic monitoring handles routine detection and qualified recovery. Ops
investigates novel incidents, proposes repairs/runbooks and measures recurring
failure causes. Ops cannot self-approve checks, overwrite dirty work, change
product priorities, spend through a paid fallback, or broaden recovery authority.
Review after ten resolved incidents or seven active development days, whichever
comes first. Record investigation vs waiting time, repeat incidents, escalations,
owner interventions, regression/recovery failures and displaced implementation
work. Recommend a dedicated standing Ops specialist only if sustained incident
load crowds out implementation and a distinct ownership/outcome can be shown.
Any new identity follows the existing approved organization-change pathway.
Success means fewer repeated unresolved handoffs and owner interventions without
weaker checks, higher failure rates or unbounded retries. Report the baseline and
observed result; do not claim the organizational change worked from agent count.
+12
View File
@@ -2743,6 +2743,18 @@ and refuses to replace a launcher path not owned by that installation.
## 27. User interfaces
The owner-approved [Adaptive software company integration plan](ADAPTIVE_COMPANY_INTEGRATION_PLAN.md)
extends this architecture with scoped capabilities, durable events, bounded
reconciliation, native-runtime awareness, evidence UX and project-fit onboarding.
It preserves current invariants and requires staged migration and qualification;
it is not a claim that the adaptive-company upgrade is implemented.
Project reference documentation, domain handbooks and provisional working memory
MUST follow [Project knowledge and domain institutional memory](PROJECT_KNOWLEDGE_SPEC.md).
This owner-approved extension requires a Git-backed shared KB, durable domain
ownership, compact agent entry points and a dedicated Console Knowledge view.
It is a development requirement, not a claim that the feature is implemented.
### 27.1 CLI
The target command surface includes:
+111
View File
@@ -0,0 +1,111 @@
# Project knowledge and domain institutional memory
Status: owner-approved development requirement, 2026-10-01. Not implemented.
The fleet remains manually paused; recording this requirement does not authorize
dispatch, new sessions, model calls, publication or automatic restart.
## Purpose
Give users and agents a shared, evolving project reference, while preserving
focused domain knowledge across session restarts, provider changes and agent
replacement. Documentation should explain how the project actually works, not
be a transcript archive, another backlog or a source of implicit authority.
## Three connected layers
1. **Project KB:** authoritative, Git-backed Markdown documenting architecture,
setup, interfaces, workflows, pipelines, troubleshooting and decisions.
2. **Domain handbook:** a compact entry point owned by the domain, linking to
relevant KB pages and recording domain conventions, procedures and pitfalls.
Prefer links over duplicated shared contracts. Knowledge belongs to the
durable domain identity, not a provider conversation or disposable session.
3. **Working memory:** current investigations, hypotheses, handoffs and unfinished
work. It is explicitly provisional; promotion to reference documentation
requires supporting evidence and the ordinary review/integration process.
The existing backlog remains the sole task queue. Handbooks may link to tasks
and domain coordination summaries but must not become competing task ledgers.
## Storage and adoption
The repository is the source of truth; Console indexes and search results are
rebuildable projections, never independent authoritative copies. Documentation
must remain usable offline and without TORCH. Do not require a hosted service,
particular AI provider or external documentation platform.
During init/analyze, discover existing documentation and propose a project-fit
navigation and ownership map. Reuse existing directories and conventions rather
than silently moving documents. For projects without conventions, a candidate
layout is `docs/knowledge/`, `docs/domains/<domain>/` and a clearly separated
working-memory area. Exact paths and metadata format need a reviewed implementation
contract; the layout is not a mandate for every project.
One accountable domain owns each reference page; collaborators and reviewers may
be named separately. Cross-domain interface documentation retains one owner and
review links to affected domains. Domain retirement, merge, split or reassignment
must preserve history and transfer responsibility, not delete institutional memory.
## Agent use and maintenance
Startup/resume briefs expose the compact domain index and essential references,
not the full KB. Agents retrieve relevant detail on demand and can discover
cross-domain references without loading unrelated documentation into every context.
Record which revision was supplied; warn about stale references where detectable.
Completion of a behavior, interface or workflow change requires documentation
impact to be considered: update affected pages or record why no update is needed.
Do not make clerical documentation edits a permission to change another domain's
implementation. Route cross-domain documentation changes through existing ownership
and review mechanisms and notify affected owners.
Reference pages expose owner, status (draft/experimental/reference/deprecated),
verification evidence and exact last-verified commit. A timestamp or successful
render does not prove accuracy. Branch/candidate documentation must not masquerade
as landed canonical behavior; represent divergence and unknown freshness honestly.
Flag broken links, missing owners and affected pages needing re-verification.
Do not automatically certify pages, mass-rewrite them or invent a freshness score.
## Console Knowledge view
Add a dedicated Knowledge screen, not another long Overview section. Provide
Read-the-Docs-style sidebar navigation, search, breadcrumbs, readable articles,
stable deep links, responsive/mobile navigation and keyboard accessibility.
Show owner, status, verified revision, related domains, tasks, pipeline definitions
and history. Keep handbook, reference and provisional memory visibly distinct.
Offer bounded actions to ask the responsible domain and report outdated content
through existing durable conversations/intake. Show queued/delivered outcomes;
these actions must not wake agents while paused or silently grant edit authority.
Use the same renderer for live project documentation and labelled demo fixtures.
Do not bundle private project documents into the public marketing demo.
## Safety and portability
Render repository text as untrusted content: sanitize Markdown, block script/raw
HTML execution, unsafe URL schemes, path traversal and escaping symlinks. Restrict
indexing/serving to approved documentation roots. Exclude credentials and private
working-memory content according to explicit project visibility policy, including
search snippets and cached indexes. External links/imported instructions do not
override TORCH policy. KB content does not expand operational authority.
## Required acceptance scenarios
- Existing project docs are discovered and linked without moves or overwrite;
an empty project receives only a reviewed, project-fit plan.
- Restart or provider replacement inherits the same domain handbook; unrelated
KB bodies are not injected into its startup context.
- A shared interface change updates its single authoritative page and routes
affected-domain notification; unchanged pages are not falsely re-verified.
- A domain ownership change preserves document history and transfers maintenance
responsibility without creating two authoritative copies.
- Provisional claims and candidate-branch behavior remain distinct from verified
reference docs; missing evidence is visible rather than converted into confidence.
- Navigation, search, deep links, keyboard and mobile use work on a realistic
multi-domain project; actual browser inspection is required before UI acceptance.
- Unsafe Markdown, traversal, symlink escape and private search leakage fail closed.
- Questions/corrections are durable while paused; no provider launch occurs.
- Markdown remains readable without TORCH, and the public demo has no private docs
or live control-plane calls.
Implementation slices are recorded in the existing authoritative TORCH backlog.
They remain proposed and unassigned for Session Manager triage after owner resume.
+81
View File
@@ -0,0 +1,81 @@
# Bounded self-healing qualification
Owner approved this priority on 2026-10-03. This extends the adaptive-company
plan and existing authoritative backlog; it is not a second work queue.
## Outcome
TORCH recovers authorized interrupted work without external babysitting. It
preserves unfinished work, assignments and evidence, never duplicates executors,
and escalates a genuine decision once with an actionable explanation. Managers
resolve coordination within delegated authority; deterministic machinery must
also detect manager/controller failure without relying on the failed agent.
## Execution order and existing owners
1. Provider Runtime / Kernel: finish `TASK-company-runtime-recovery`, including
exact stopped-process proof, retained guard/controller-lock recovery, fresh
instructions and preservation of dirty work. Qualification and native source
adoption/integration remain prerequisites, not gates to bypass.
2. Release / Runtime / Kernel: finish `TASK-conversation-persistent-operations`;
installer-owned services survive reboot, reconcile idempotently and uninstall
only manifest-owned resources. Keep dormant-install and explicit activation.
3. Runtime / QA: prioritize the failure/backoff portion of
`TASK-conversation-usage-governor`. Distinguish transient failure, quota,
authentication, unavailable model, unknown outcome and owner pause. Circuit
breakers must suppress repeated launches for the same unresolved cause.
4. Kernel / Work: complete the authority/event prerequisites and
`TASK-company-reconciler`, with existing `TASK-conversation-event-driven-wakes`
and `TASK-conversation-queue-watchdog`. Observe, reserve, recheck authority
and revision, execute one bounded action, record and reconcile. No duplicate
grant, event, task, messaging or recovery stores.
5. Manager / Console / QA: report the current wait owner, reason, age, last
recovery attempt, next eligible action and exact evidence. Chase dependencies
and approvals without inventing permission or sending repeated notices.
Urgent recovery does not preempt a running gate or steal an assignment. Managers
should resolve existing test/file-boundary handoffs and assign QA-owned tests
explicitly rather than circulate the same approval request indefinitely.
## Failure-injection acceptance matrix
Run against isolated fixtures and then an installed, owner-approved qualification
project. Do not intentionally kill the live development fleet to prove recovery.
| Fault | Required observable outcome |
| --- | --- |
| Specialist exits mid-task | Retain task/session/evidence and dirty bytes; prove old process stopped before one authorized resume |
| Process alive but quiet | Inspect tools/check queues and progress evidence; quiet alone never permits kill or guard release |
| Manager exits | Independent deterministic controller discovers the loss and performs one policy-authorized resume |
| Controller crashes after reservation | Reconcile exact invocation and durable reservation; no lost or duplicate execution |
| Crash leaves turn/controller lock | Recover only with exact ownership/process proof; unknown or foreign state fails closed |
| Reboot or service restart | Restore approved desired state once; preserve explicit manual pause and detached status |
| Missed/duplicate/out-of-order notification | Periodic reconciliation finds the wait; duplicates and stale revisions cannot repeat effects |
| Dependency or approval wait | Route to correct authority, record acknowledgement and escalate overdue wait without self-approval |
| Check/integration conflict | Preserve failed evidence and candidate; return actionable work to its owner without relaxing checks |
| Transient provider fault | Bounded retries with persisted backoff; no immediate unbounded restart loop |
| Quota/auth/model unavailable | Persist scoped hold/circuit breaker; report once, no paid fallback or unrelated-provider switch |
| Pause or authority revoked during recovery | Recheck immediately before effect and stop further launches |
Every accepted case needs exact candidate, controlled conditions, observable
assertions, original failure evidence and independent QA. Record recovery latency,
duplicate launches, lost work, retry count and owner interventions. A running
process, green command, task-state churn or acknowledgement is not itself useful
delivery. Finish with verified integration and next-work continuation.
## Current spending policy
The owner explicitly enabled unlimited automatic daily turns in alpha.4.2;
missing subscription telemetry must remain unknown, not become a false credit
estimate. Do not silently restore a finite cap as part of governor work. This
specific authorization supersedes older pilot cap instructions, not approval,
ownership, concurrency, no-progress or provider-failure boundaries. Unlimited
eligible work is not unlimited retries. No paid API fallback was authorized.
## Completion boundary
Do not close recovery/reconciler umbrellas from focused source tests alone.
Require installed-runtime failure injection, independent QA, reboot/pause
qualification and an observed unattended work cycle. Report remaining limitations
plainly; intentional owner decisions may remain blocked, forgotten handoffs may
not. No new permanent supervisor is required merely to implement this loop.
+498
View File
@@ -1,5 +1,172 @@
# Test Integrity Notes
## 2026-10-03 — Current unread inbox startup mitigation
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-fleet-startup-inbox
given: "Manager and specialist each have 102 unread handoffs, exceeding the legacy default history page"
when: "Fresh and resumed provider launch messages specify the current unread-inbox request and it is invoked through the registered identity-bound tool"
then: "Both launches name the correct identity and retrieve newer messages absent from default history, without acknowledging them or weakening authority; overflow coverage is explicitly uncertain"
observable_outcomes:
- "Actual provider launch arguments, registered MCP response, newest message identity and unchanged unread count"
determinism_controls:
- "Hermetic project/worktrees, increasing fixed clock and real local tool; no provider launches"
anti_cheat_rationale:
prevents:
- "Prompt-only proof that never exercises the actual inbox tool"
- "Claiming truncated history is complete or reading acknowledges a handoff"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-10-03 — Audited one-day additional allowance
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-runtime-continuation-day-override
given: "An exhausted owner-approved daily allowance and retained charged attempts"
when: "The owner approves a bounded current-UTC-day total and later pauses or the day rolls over"
then: "Extra work starts without resetting attempts; non-owner/invalid overrides refuse; pause persists; the following day uses the original daily cap"
observable_outcomes:
- "Actual launched count, charged attempts, remaining allowance, owner audit and next-day limit"
determinism_controls:
- "Hermetic project, fixed clock and controlled launch callback; no live provider or host scheduling"
anti_cheat_rationale:
prevents:
- "Erasing usage or permanently removing daily caps on an owner override"
- "Allowing a specialist to authorize extra budget or bypass pause"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-10-03 — Owner-configured concurrency
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-runtime-continuation-concurrency
given: "Seven eligible identities and an approved 48-turn daily cap"
when: "The owner raises concurrency to seven, starts a bounded controller, then pauses"
then: "Seven pending starts occur before completion; a tighter limit restricts candidates; invalid settings refuse; pause preserves cap, concurrency and charged attempts"
observable_outcomes:
- "Pending launch count, policy capacity, ledger attempts and refusal codes"
determinism_controls:
- "Hermetic Git repository, fixed clock, controlled promises and no provider calls"
anti_cheat_rationale:
prevents:
- "Treating sequential starts as concurrency or resetting usage on configuration"
- "Tick limits exceeding owner-approved policy"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-10-03 — Visible automatic-work stops
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-runtime-continuation-health
given: "An enabled pilot exhausts its daily attempt allowance"
when: "Observation reads the ledger, next UTC day, pause or malformed state"
then: "The exact stop and remaining allowance are visible without resetting or modifying state, and no timer/token qualification is fabricated"
- id: SCN-console-continuation-stop
given: "A daily cap stop and a specialist no-progress hold"
when: "Attention items are projected"
then: "The budget decision belongs to the owner, coordination belongs to the manager, and no fake approval action or automatic budget increase is offered"
observable_outcomes:
- "Live snapshot health, byte-identical ledger and owner/manager attention routing"
determinism_controls:
- "Hermetic repository, fixed UTC clock, injected executor and plain snapshot fixtures"
anti_cheat_rationale:
prevents:
- "Silent budget exhaustion disguised as healthy idle"
- "Resetting a ledger to make work appear available"
- "Treating declared policy as a verified running timer or measured usage"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-10-03 — Rolling continuation and bounded no-progress escalation
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-runtime-continuation-refill
given: "A long manager turn and a short specialist turn occupy two slots; a third identity waits"
when: "Only the short turn completes"
then: "The third starts before manager completion and the daily attempt cap still holds"
- id: SCN-runtime-continuation-no-progress
given: "An unchanged unfinished eligible assignment"
when: "Two successful executor turns make no recorded task progress"
then: "Exactly two specialist turns occur, one durable coordination request is sent, unchanged-work repeats stop, new revisions may resume and unmet dependencies remain ineligible"
- id: SCN-runtime-continuation-capacity
given: "An owner-started executor already occupies the configured automatic capacity"
when: "The continuation controller runs"
then: "No additional automatic turn starts and no attempt is charged"
- id: SCN-runtime-continuation-fairness
given: "One specialist keeps advancing revisions while another eligible identity has not started"
when: "A slot becomes available"
then: "The least-recently-dispatched peer starts before the returning specialist"
observable_outcomes:
- "Actual launch ordering, daily charges, coordination messages and explicit held-work reason"
determinism_controls:
- "Hermetic installed Git projects, manually resolved provider promises and task fixtures; no sleep, network or live model"
anti_cheat_rationale:
prevents:
- "Replacing the batch barrier by raising concurrency"
- "Claiming refill only after all turns finish"
- "Spending unlimited turns on unchanged work or ignoring dependency gates"
- "Ignoring physical executors started outside the controller"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-10-03 — Process-verified runtime presence
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-runtime-live-presence
given: "A Linux executor holds a real identity guard while durable state is idle"
when: "The observation API reads starting, working, approval waiting and terminal phases"
then: "Live phases display accurately, semantic waits persist, terminal guards vanish and durable reports are not fabricated"
- id: SCN-runtime-live-presence-refusal
given: "A guarded identity with injected OS observations"
when: "PID is replaced, dead, foreign-user, unavailable or project binding is wrong"
then: "It never reports active execution"
- id: SCN-runtime-live-presence-legacy
given: "A pre-observation guard without a process-start token"
when: "The exact TORCH up identity and canonical cwd match or disagree"
then: "Only a verified matching live executor displays working"
observable_outcomes:
- "Observation API state, reportedState, executor source and unchanged durable identity"
determinism_controls:
- "Hermetic installed repository and injected PID observations for refusal cases"
- "Supplemental Linux /proc integration observes the test process itself, with no provider or timing sleeps"
anti_cheat_rationale:
prevents:
- "Painting all idle badges working without process evidence"
- "Treating a crashed guard or reused PID as an active agent"
- "Hiding approval waits or fabricating heartbeat/task progress"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-09-30 — Bounded routine coordination
```yaml
@@ -2716,6 +2883,45 @@ test_integrity_note:
if_true_explain_spec_basis: ""
```
# Candidate source B2 frozen-input and fence boundaries (2026-10-01)
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-candidate-bootstrap-one-shot
given: "An issued, nonce-bound native admission"
when: "It is opened, replayed, expired, foreign, or malformed"
then: "Only one bounded private bootstrap opens; every invalid case refuses before child start and exposes no authority handle"
- id: SCN-candidate-input-consumption
given: "A frozen project-local source snapshot"
when: "The original source changes before the fixed Node child reads its cwd"
then: "The child observes the captured bytes; snapshot drift refuses"
- id: SCN-candidate-execution-boundary
given: "A fixed command and explicit bounded environment"
when: "Ambient PATH/HOME/cache fallback or output overflow is attempted"
then: "Execution refuses or remains non-success"
- id: SCN-candidate-fence-closure
given: "A private PID1 namespace observation"
when: "Descendant closure is absent or uncertain"
then: "The outcome is UNKNOWN/INCOMPLETE, never terminal PASS"
observable_outcomes:
- "No DB, ControlPlane, writer, receipt adapter, or authority FD reaches the child"
- "Product input links and special nodes refuse; frozen product S stays under 64MiB/file and 512MiB total"
- "Artifact output remains M/artifactManifest; receipt-adapter A is absent and unclaimed"
determinism_controls:
- "Temporary source trees, fixed Node executable, explicit empty ambient environment, bounded streams, and injected observations"
- "No live policy registration, installation, network, cache, or provider process"
anti_cheat_rationale:
prevents:
- "Reading mutated live source instead of captured bytes"
- "Replaying a private admission or relabeling fixture output as a receipt"
- "Treating PGID emptiness, zero exit, or Chromium metadata as descendant/binary proof"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Console attention advisory cohorts (2026-10-01)
```yaml
@@ -2765,6 +2971,47 @@ test_integrity_note:
if_true_explain_spec_basis: ""
```
# Candidate source B2 authenticated-attempt spec correction (2026-10-01)
```yaml
test_integrity_note:
change_type: spec_correction
scenarios:
- id: SCN-candidate-bootstrap-one-shot
given: "A CheckService-issued opaque registered-plan handle"
when: "A foreign, cloned, expired, replayed, or already-closed handle is used"
then: "No FD3 admission opens; the only accepted acknowledgement is the exact FD4 binding frame and it closes the original linear handle"
- id: SCN-candidate-input-consumption
given: "A frozen, bounded product snapshot and a fixed Node E binding"
when: "The original product bytes change before execution"
then: "An actual spawned child reads the frozen bytes; post-capture drift refuses before spawn"
- id: SCN-candidate-execution-boundary
given: "An opaque attempt with fixed command, arguments, explicit environment, and output cap"
when: "The child exits zero after exceeding the cap"
then: "The parent records overflow as non-success without accepting an injected executor or caller-selected limit"
- id: SCN-candidate-fence-closure
given: "A closed opaque attempt"
when: "The backend has not recorded namespace identity, FD closure, and descendant reaping"
then: "Terminal proof is UNKNOWN; fabricated observations and cloned handles cannot create proof"
observable_outcomes:
- "Only the exact FD4 acknowledgement closes a one-shot opaque admission"
- "Child stdout proves frozen-byte consumption, not same-process test reads"
- "Zero exit plus output overflow is never success"
- "Missing backend fence evidence is never TerminalProof"
determinism_controls:
- "Temporary source trees, fixed absolute Node runtime, explicit empty child environment, bounded streams"
- "No injected executor, caller-provided command, root, environment, limit, or terminal observation"
anti_cheat_rationale:
prevents:
- "Arbitrary tuple issuance or a fabricated bootstrap literal"
- "Boolean caller observations minting terminal proof"
- "Cloned or reused handles reopening an admission"
- "Same-process snapshot reads impersonating child consumption"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Compact Console Overview density (2026-10-01)
```yaml
@@ -2811,6 +3058,257 @@ test_integrity_note:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Owner-requested legacy recovery and bounded continuation (2026-10-03)
Implementation and behavioral basis:
[legacy runtime recovery](spec_changes/2026-10-03-legacy-runtime-recovery.md).
Existing assertions are unchanged; the new scenarios are in
`test/fleet/legacy-runtime-recovery.test.mjs`.
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-legacy-runtime-recovery
given: "A stopped exact invocation and retained unknown identity"
when: "The owner recovers presence"
then: "One owner audit, offline presence and preserved task/session/unknown"
- id: SCN-legacy-runtime-recovery-refusal
given: "Live or mismatched invocation/process evidence"
when: "Proof is requested"
then: "Recovery refuses without mutation"
- id: SCN-legacy-runtime-recovery-cas
given: "Stale, expired, forged or newly live evidence"
when: "Recovery applies"
then: "Transaction refuses without success audit"
- id: SCN-legacy-runtime-recovery-authority
given: "A specialist or incomplete expected snapshot"
when: "Owner recovery is attempted"
then: "Authority/snapshot validation refuses"
- id: SCN-runtime-turn-guard
given: "A physically live executor reporting idle"
when: "A second turn tries to start"
then: "The executor mutex refuses overlap until terminal release"
- id: SCN-runtime-continuation
given: "Owner-enabled roster and a new durable signal"
when: "Turns repeat, hit a cap or are paused"
then: "Only new eligible signals launch within the owner daily cap"
- id: SCN-runtime-continuation-signals
given: "Two eligible identities with multiple unread messages"
when: "A bounded batch starts and only acknowledgements change afterward"
then: "Both starts occur concurrently, a competing controller refuses, and acknowledgements alone cause no wake"
- id: SCN-runtime-continuation-pause
given: "Two eligible turns"
when: "The owner pauses during the first"
then: "The second does not start and pause is retained"
- id: SCN-legacy-runtime-recovery-project
given: "Cloned project/session metadata in distinct checkouts"
when: "Proof crosses checkout boundaries"
then: "Recovery refuses and leaves the other identity untouched"
observable_outcomes:
- "Persisted identity, task/session, owner audit, launch count and refusal codes"
determinism_controls:
- "Hermetic repositories, fixed clock and OS-boundary fixtures; no live provider"
anti_cheat_rationale:
prevents:
- "Boolean stopped proof or stop-as-PASS"
- "Stale/cross-project mutations and peer impersonation"
- "Duplicate physical turns or unchanged-signal busy loops"
- "Losing pause or refunding interrupted attempts"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-codex-mcp-runtime-dir
given: "A Codex adapter receives an injected Linux parent environment with a valid XDG runtime directory, or deterministic missing, malformed, or unsupported-platform context"
when: "It creates or resumes an identity-bound Codex session with a TORCH MCP server"
then: "Both launch records name only XDG_RUNTIME_DIR in MCP env_vars for valid context; invalid context emits no env_vars, and no runtime value, DBUS, home, path, token, or general environment is serialized"
observable_outcomes:
- "Create and resume plans expose the exact single-name MCP env_vars allowlist only for valid parent context"
- "Invalid or unsupported context omits the allowlist without synthesizing a runtime path"
- "The serialized invocation never exposes parent environment values"
determinism_controls:
- "Narrow injected adapter env and platform inputs; no process-wide environment mutation, host service, provider, or native check"
anti_cheat_rationale:
prevents:
- "Hard-coded runtime UID or directory values"
- "Forwarding DBUS, secrets, home, PATH, or the complete parent environment"
- "Treating a mocked launch record as native runtime qualification"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Exact-source adoption (2026-10-03)
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-integration-adoption-private-ref-cas
given: "A clean disposable Git repository, a base commit, and an owner-bound private-ref adoption"
when: "The adoption finalizes and a stale compare-and-swap attempts to alter its candidate ref"
then: "Real Git private archive and candidate refs are created atomically, while the stale write refuses"
- id: SCN-integration-adoption-journal-and-binding
given: "A prepared adoption journal record"
when: "A second preparation or lifecycle request supplies a different candidate binding"
then: "The journal revision compare-and-swap and immutable binding checks refuse without changing the prepared record"
- id: SCN-integration-adoption-finalize-recovery
given: "A real private-ref transaction completed before its journal write"
when: "Reconciliation is run and then replayed"
then: "One durable finalized recovery is recorded and the replay is consistent without another mutation"
- id: SCN-integration-adoption-rollback-recovery
given: "A finalized candidate whose private ref was removed before its rollback journal write, or whose candidate ref diverged"
when: "Rollback is requested"
then: "The completed rollback is recovered once and divergent candidate bytes refuse"
- id: SCN-integration-adoption-private-ref-intake
given: "An existing unfinalized private candidate ref and a separate finalized owner-bound private candidate ref"
when: "Direct private-ref intake and authenticated native qualification are requested"
then: "Direct intake refuses without a queue record, while the authenticated path records the exact private ref and commit in ordinary testing state"
- id: SCN-integration-adoption-approval-boundaries
given: "An ordinary source-only approval, or an adoption whose actor, action, approval revision, or provenance differs from the approved immutable binding"
when: "Preparation or a lifecycle transition is attempted"
then: "The transition refuses with its structured boundary code and no integration queue record"
- id: SCN-integration-adoption-request-replay
given: "A finalized adoption that has created its one ordinary private-ref integration request"
when: "Two independently opened authenticated API callers request it concurrently"
then: "The existing transactional adoption journal serializes them, one caller refuses after the completed transition, and exactly one queue row remains"
- id: SCN-integration-adoption-legacy-retries
given: "An existing project database containing two historical ordinary-request retry rows with the same source branch and commit"
when: "IntegrationService starts"
then: "Both records remain readable; startup adds no global uniqueness constraint and does not reject historical evidence"
observable_outcomes:
- "Git ref values, structured refusal codes, durable journal state, and native integration-request fields"
determinism_controls:
- "Disposable local Git repositories, temporary SQLite-backed control-plane state, fixed fixture bytes, and no network or provider"
anti_cheat_rationale:
prevents:
- "Fixture-only CAS implementations that do not exercise Git update-ref"
- "Mutable lifecycle bindings or overwriteable adoption journal records"
- "Claiming crash recovery without preserving actual ref effects"
- "Treating a private candidate as an ordinary specialist branch"
- "Using an unrelated, source-only, stale, or mismatched approval as adoption authority"
- "Duplicating a private-ref queue record through concurrent authenticated API requests or replay"
- "Rejecting historical ordinary-request retry evidence with an unqualified schema constraint"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-integration-adoption-lifecycle-revision-cas
given: "A prepared or finalized owner-bound adoption record with a known journal revision"
when: "A caller supplies a stale or omitted expectedRecordRevision to finalize, reconcile, rollback, or request native qualification"
then: "The operation refuses before private-ref, journal, queue, or audit effects; the original record and refs remain unchanged"
observable_outcomes:
- "ADOPTION_RECORD_REVISION_STALE is returned for stale or omitted lifecycle callers"
- "Private candidate/archive refs, durable journal record, audit count, and integration queue remain unchanged for every rejected lifecycle call"
determinism_controls:
- "Disposable local Git repository and temporary SQLite-backed control-plane fixture; no network, provider, or live adoption"
anti_cheat_rationale:
prevents:
- "Issuing external ref or queue effects from a stale lifecycle read"
- "Treating journal revision as an internal-only implementation detail"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
```yaml
test_integrity_note:
change_type: spec_correction
scenarios:
- id: SCN-integration-adoption-request-replay
given: "Two authenticated callers carry the same finalized adoption revision into the serialized native-request transition"
when: "One caller completes the transition and the other resumes with its now-stale expectedRecordRevision"
then: "The second caller strictly receives ADOPTION_RECORD_REVISION_STALE while exactly one requested journal record and one queue row remain"
observable_outcomes:
- "The replay loser reports the stale journal-revision boundary, not a generic post-state refusal"
- "Exactly one private-ref integration request and requested audit transition exist"
spec_correction_basis:
- "The former ADOPTION_REQUEST_STATE_INVALID replay expectation was wrong: after the serialized winner commits, the loser carries an obsolete journal revision, so revision CAS is the first observable refusal boundary under SCN-integration-adoption-request-replay."
determinism_controls:
- "Two independently opened local API processes over disposable Git and SQLite fixture state"
anti_cheat_rationale:
prevents:
- "Accepting a stale lifecycle read after another caller has committed"
- "Relaxing the exactly-one-request invariant after adding revision binding"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-10-03 — Schema-v2 read-only owner-reply boundary
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-read-only-v2-control-plane
given: "A canonical schema-v2 fixture, including a write-refusing state directory"
when: "A reader opens a normal SQLite read transaction and a mutation is attempted"
then: "It returns one coherent snapshot or a typed refusal; no database or sidecar bytes change"
- id: SCN-read-only-pinned-correlated-snapshot
given: "Two v2 identities in one observable generation"
when: "A writer atomically commits their next generation while the reader remains open"
then: "The reader observes one generation or refuses, never a mixed pair"
- id: SCN-owner-addressed-reply-durability
given: "A registered agent has an owner-originated request addressed to it"
when: "It sends a dedicated reply and state is reopened"
then: "The reply linkage, bounded owner cursor, and actor audit persist"
- id: SCN-owner-addressed-reply-refusal
given: "Spoofed senders, a different valid registered agent, generic owner recipients, foreign/forged references, or a non-owner reader"
when: "They invoke the owner-reply boundary"
then: "They refuse without message or audit writes; the owner conversation remains unchanged; direct and broadcast delivery remain intact"
observable_outcomes:
- "Schema-v2 compatibility refusal, stable snapshot rows, byte/sidecar preservation, durable reply linkage, cursor page, and audit event"
determinism_controls:
- "Hermetic Git/SQLite fixtures, fixed clocks and IDs, explicit transactions, no network, provider, sleep, or retry"
anti_cheat_rationale:
prevents:
- "Migrating or initializing a read-only or unknown schema"
- "Returning stale or mixed identity generations"
- "Treating owner as a fleet identity or accepting generic recipient bypasses"
- "Accepting foreign, forged, or valid-identity wrong-addressee owner-reply linkage"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
## 2026-10-03 — Public MCP owner-reply wrapper
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-mcp-owner-reply-public-boundary
given: "A registered agent has an owner-originated request addressed to it and a second registered identity exists"
when: "Each identity invokes the public stdio MCP owner-reply tool"
then: "Only the bound addressee can persist a reply with replyTo and audit attribution; selector fields and another valid identity refuse before a write"
observable_outcomes:
- "The MCP schema exposes only request_id and body with no additional properties or owner-history tool"
- "The persisted reply is linked to the owner request and audited as the bound actor"
- "Selector-field and wrong-addressee refusals leave owner conversation and audit counts unchanged"
determinism_controls:
- "Hermetic Git/SQLite fixture, fixed topology, local stdio MCP process, no network, provider, sleep, or retry"
anti_cheat_rationale:
prevents:
- "Selecting a sender, recipient, owner, or history through MCP input"
- "Replying from a different valid Fleet identity"
- "Exposing an agent-facing owner-history reader"
- "Claiming an in-process helper as public MCP qualification"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Self-host runtime metadata observations (2026-10-01)
+128
View File
@@ -1,5 +1,133 @@
# TORCH remaining work
## Immediate owner-facing milestone: agent Chat (2026-10-03)
Owner promoted `TASK-owner-agent-chat`, `TASK-console-agent-chat-ui` and
`TASK-console-chat-cards-qualification` to urgent. Deliver default coordinator
chat, activated-agent switching, durable history and a safe composer first,
following [the Chat specification](CONSOLE_CHAT_AND_COMPACT_CARDS_SPEC.md).
Manager must triage eligible slices; priority does not remove runtime-wake
dependencies or preempt active work. Recovery unblockers continue alongside it.
## Owner-approved self-healing priority (2026-10-03)
Owner also approved [operational flow and Ops pilot improvements](OPS_FLOW_IMPROVEMENT_SPEC.md):
two focused incident-resolution/adoption-and-QA-handoff requests, additive Chat
and evidence acceptance, and an existing-identity Ops responsibility pilot. No
eighth standing agent or overlapping implementation ownership is implied.
Authoritative urgent proposals (assignment remains with Session Manager):
- [ ] `TASK-fe86062d-847f-46ab-9ad9-d721113e5dcd`: operational incident ownership
and overdue wait resolution, extending current messages/events/approvals.
- [ ] `TASK-f40126ee-2485-45c8-a67b-4279e4e8704a`: supported exact-source
adoption and named QA/test handoffs through native qualification/integration.
The linked spec supplies additive acceptance for existing Chat/evidence tasks
and the Ops responsibility pilot; task-file criteria have not been rewritten by
the owner because the installed API has no supported amendment operation.
Follow [bounded self-healing qualification](SELF_HEALING_QUALIFICATION_PLAN.md).
Prioritize the existing runtime-recovery, persistent-operations, provider-failure
governor, event/reconciler and queue-watchdog work without duplicating their
backlog records or bypassing dependencies. Require installed failure-injection
evidence and independent QA, not merely active executors. The owner subsequently
enabled unlimited daily continuation in alpha.4.2; older cap/resume statements
below are historical and do not override that specific authorization. Retry
circuit breakers, concurrency and approval/ownership gates still apply.
## Current owner priority: reliability first (2026-10-03 UTC)
End-to-end repair qualification is still open. Runtime reported exact converged
candidate `d2d9845` with four fresh native check PASS receipts; QA independent
acceptance is not yet recorded. Owner source fixes cover guarded presence,
rolling/fair dispatch, no-progress escalation and visible daily-cap stops. The
first automatic allowance of 12 turns was exhausted; any increase needs an
explicit owner choice, not a manual-start bypass. Next dispatch should prioritize
QA's exact verdict/support decisions, Release's declared-inventory consumer
decision, safe Kernel/Work composition and actual Console implementation.
Require evidence of specialist work, independent review/checks, serialized
integration and next-work continuation before calling the autonomous loop ready.
Live all-fleet resume exposed a new urgent proposal:
`TASK-d95e72d0-816d-4fc2-acaf-b73642fd4881` — refill continuation capacity
when individual turns exit instead of waiting for the longest batch member.
Manager remained running after Kernel/Runtime exited, leaving free slots unused
and delaying the other four specialists. Preserve identity guards, pause, global
capacity and attempt accounting when replacing the batch barrier.
The same run reconfirmed the existing `TASK-pilot-starting-presence`: actual
executors can display idle during startup. Do not duplicate that task or falsify
presence to make the board look busy. Owner explicitly requested one simultaneous
resume of all seven existing identities; that manual batch is separate from the
automatic pilot's 12-turn daily ledger, not proof of a global usage governor.
This ordering supersedes the UI-first ordering below. The existing authoritative
backlog, not this index, controls task state and assignment.
1. Safe current-schema runtime recovery (`TASK-company-runtime-recovery`) and
combined CLI qualification (`TASK-pilot-cli-suite-timeout`).
2. Truthful startup/session status (`TASK-pilot-starting-presence`) and its
existing Kernel reservation/Work candidate-safety prerequisites.
3. Persistent operations and bounded event-driven continuation
(`TASK-conversation-persistent-operations`,
`TASK-conversation-event-driven-wakes`), followed by actual scheduler
qualification (`TASK-conversation-scheduler-qualification`).
4. Specific actionable operational warnings (`TASK-console-attention-actions`).
5. Compact cards/detail modals and coordinator-first Chat remain high priority.
The first four groups and incomplete technical prerequisites are urgent;
scheduler qualification is high. These priority-only changes preserve owners,
states, dependencies and evidence. Blocked remains blocked; automatic wakes stay
disabled until qualified and explicitly activated. Older pause/install/status
statements below are historical snapshots, not current runtime evidence.
Owner-requested recovery/continuation implementation is described in
[the source pilot specification](spec_changes/2026-10-03-legacy-runtime-recovery.md).
Do not close the runtime-recovery, persistent-operations or event-wake umbrellas
from focused tests alone. Release/Fleet Operations still need installer-owned
permanent timer provisioning/removal, installed artifact qualification and restart
recovery of retained crashed turn/controller guards. The transient host pilot does
not survive reboot, and its daily turn cap is not a token/dollar usage governor.
## Priority UI cycle: compact cards and agent Chat
Owner-approved [Console UI spec](CONSOLE_CHAT_AND_COMPACT_CARDS_SPEC.md).
Prioritize these after explicit owner resume, ahead of broad company expansion:
`TASK-console-compact-details`, `TASK-console-agent-chat-ui` and
`TASK-console-chat-cards-qualification`. Extend the existing high-priority
`TASK-owner-agent-chat`; do not duplicate messaging or bypass paused wake policy.
Proposals remain unassigned. Fleet startup is not authorized by this addition.
## Adaptive software company upgrade (owner-approved, 2026-10-01)
The [integration plan](ADAPTIVE_COMPANY_INTEGRATION_PLAN.md) records scope,
existing-task reuse, phased contracts, exit gates and migration/rollback rules.
The existing authoritative backlog contains eleven linked `TASK-company-`
proposals, not a second queue. Runtime recovery gates unattended coordination;
authority comes from scoped revocable grants, not reporting relationships.
Existing grants, event-driven wakes, first-work intake, usage governance and
hierarchy pilots are reused rather than replaced. All new items are proposed
and unassigned. No automatic restart or implementation is authorized by this
roadmap addition; the fleet remains manually paused pending owner resume.
## Owner-approved project knowledge requirement (2026-10-01)
See [Project knowledge and domain institutional memory](PROJECT_KNOWLEDGE_SPEC.md).
The existing authoritative backlog holds the implementation slices below; this
section is an index, not a second queue. All are proposed/unassigned, and the
fleet remains manually paused until the owner explicitly requests resume.
- `TASK-knowledge-contract`: project-fit Git-backed KB, domain handbooks and
provisional memory; metadata, visibility, evidence and ownership contract.
- `TASK-knowledge-agent-context`: documentation discovery, compact domain brief
references and provider-independent retrieval/inheritance without context bloat.
- `TASK-knowledge-maintenance`: documentation-impact workflow, verification
evidence, cross-domain notifications and ownership transfer without duplicate truth.
- `TASK-knowledge-console`: dedicated accessible Knowledge view, safe rendering,
search, history and durable ask-domain/report-outdated actions.
- `TASK-knowledge-qualification`: end-to-end inheritance/freshness/privacy/pause
scenarios and realistic browser acceptance, including isolated demo fixtures.
## Authoritative current handoff (2026-09-30, after alpha.2 reinstall)
TORCH is installed and healthy at `0.1.0-alpha.2`. Setup/restore and all seven
@@ -0,0 +1,16 @@
# Owner-configured continuation concurrency
The owner can configure `torch continuation enable --concurrency 7 --yes`.
The default remains three; valid settings are integers from one through seven.
An explicit tick `--limit` may tighten but never exceed the approved policy.
Each refill reads current policy; lowering capacity does not cancel active turns.
Omitted configuration fields preserve the approved daily cap and concurrency.
Configuration never resets the attempt ledger or bypasses physical identity guards.
An already-running older controller cannot hot-load this change. Replace its
timer, not its active service, and allow its native turns and controller lock to
finish before the new controller takes over. No concurrent controllers may
dispatch through the same project lock.
Scenario SCN-runtime-continuation-concurrency proves seven simultaneous pending
starts, tighter tick limits, invalid-policy refusal and preserved usage on pause.
@@ -0,0 +1,14 @@
# One-day continuation allowance override
An explicit owner decision can grant a higher bounded total for the current UTC
day using `torch continuation enable --today-max-turns 96 --yes`.
The override total includes turns already charged; it never erases usage.
The ordinary daily cap (1–48) remains unchanged. The override ceiling is 96.
At UTC midnight the override ceases to apply without a scheduled reset write.
Owner-only audited configuration, seven-slot concurrency, identity guards,
pause, unknown-outcome protection and all verification boundaries remain intact.
Omitting the override argument preserves its existing same-day decision.
This is a turn allowance, not a measured token, subscription-credit or cost limit.
SCN-runtime-continuation-day-override checks actual extra dispatch, preserved
charges, authorization/range refusals, pause and automatic next-day expiry.
@@ -0,0 +1,93 @@
# Manual owner recovery for retained unknown executor outcomes
The owner requested fixing the stalled fleet. Legacy runtime records lacked a
recovery API: three physically stopped executors remained `working/unknown` and
correctly refused duplicate launches. This change adds an owner-only, previewed
`recover-runtime` operation without schema changes or automatic recovery.
The owner explicitly selects a project-scoped systemd service invocation. Its
working directory and exact area argument must match this installation; its
terminal timestamp must match the retained identity update within five seconds
(systemd displays timestamps with one-second precision). Both service PIDs must
be zero, its cgroup absent, and no visible same-user native process may reference
the session or registered workspace. Physical state is checked again at apply.
Unreadable process visibility, an active service, foreign invocation or changed
identity refuses recovery. This is a **manual legacy operator binding**, not
cryptographic session provenance or protection against arbitrary same-user code.
Evidence is process-local, branded, single-use and expires after sixty seconds.
The service transaction compares the exact state/runtime/session/update snapshot,
sets presence to offline and writes an owner-attributed audit. Session, current
task and the unknown outcome are preserved. No task, test or launch is completed;
no process is killed, lease removed, schema migrated or provider started.
Preview and apply use the same implementation. Apply additionally requires the
session and update timestamp from the preview. Hosts/providers without verifiable
Linux/systemd evidence refuse this legacy recovery; no Boolean fallback exists.
## Bounded continuation pilot
`torch continuation enable --max-turns-per-day 12 --yes` approves only the
currently activated identity roster. `status` previews eligible identities;
`tick --limit 3 --yes` runs at most three normal guarded `up` operations. New
unacknowledged messages or assigned/in-progress task revision changes supply the
signal; an unchanged signal is not retried. Active/unknown identities, held turn
guards, detach, disabled policy and the daily UTC attempt cap prevent launch.
Attempts are charged before invocation, including failed/interrupted attempts.
The cap limits turns, **not dollars or token consumption**. Unknown quota remains
unknown. This controller makes no product/approval/assignment decisions.
`torch continuation pause --yes` prevents subsequent launches. Policy and attempt
ledger are separate atomic files so an in-flight turn cannot overwrite a pause.
Pause does not kill an already-running turn. A controller lock serializes ticks;
a crashed lock is retained rather than expired automatically. Each executor has
a separate physical turn guard held through classification and final presence,
so an agent reporting idle cannot permit overlapping native turns.
The first host pilot uses an explicitly installed transient user-systemd timer.
It survives this chat ending, but not a host reboot. Permanent installer-owned
provisioning/removal and installed artifact qualification remain existing backlog
work; this is not a claim that the alpha.3 installed package has been upgraded.
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-legacy-runtime-recovery
given: "An exact retained working/unknown identity and stopped project invocation"
when: "The owner previews and applies recovery"
then: "Presence becomes offline once; session, task and unknown outcome persist with one owner audit"
- id: SCN-legacy-runtime-recovery-refusal
given: "Live, foreign, mismatched or incomplete stopped-executor evidence"
when: "Recovery evidence is requested"
then: "Recovery refuses without identity mutation"
- id: SCN-legacy-runtime-recovery-cas
given: "Issued proof followed by a changed identity, live process, expiration or forgery"
when: "Recovery is applied"
then: "Transactional recovery refuses and creates no success audit"
- id: SCN-legacy-runtime-recovery-authority
given: "A specialist actor or incomplete expected snapshot"
when: "Owner recovery is requested"
then: "Authority or snapshot validation refuses"
- id: SCN-runtime-turn-guard
given: "A physical executor holds an identity guard and reports idle early"
when: "Another executor attempts the same identity"
then: "It refuses until the first executor finishes and releases its own guard"
- id: SCN-runtime-continuation
given: "An explicitly enabled roster, a new durable signal and a fixed daily turn cap"
when: "Continuation ticks, repeats a signal, exhausts its cap or is paused"
then: "Only eligible new signals launch; deduplication, cap, owner authority and pause remain enforced"
observable_outcomes:
- "Durable presence, session and task state; owner-attributed audit; explicit refusal codes"
determinism_controls:
- "Hermetic Git/install fixtures, fixed clock and injected OS observation boundary"
anti_cheat_rationale:
prevents:
- "Caller Boolean as stopped proof"
- "Process stop promoted to successful task or test"
- "Stale snapshot overwrites or silent peer impersonation"
- "Expired, foreign or replayed proof"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
@@ -0,0 +1,13 @@
# Live runtime presence instead of stale idle badges
The owner requires active executor turns to be distinguishable from old self-reported idle/offline state. Display is observational, not a new task-state machine or a substitute for authorization.
Each new physical turn guard records a starting phase and Linux process-start token, then enters working immediately before invoking the authorized executor. The read-only observation API validates the exact project/identity guard against a live same-user process with the matching start token. Existing legacy guards additionally require the exact TORCH up identity and canonical working directory. Dead, foreign-user, replaced or unobservable processes are not called active; retained unverified guards display unknown instead of false idle/offline.
An active verified guard overrides idle/offline display with starting/working. Original `reportedState`, `reportedSummary`, task and heartbeat are retained. Waiting/blocked reports remain visible even during a live turn. Guard removal naturally restores the last reported state after completion. No report, task, authority, launch policy, budget or receipt is changed by observation.
On unsupported OS process-observation surfaces, guarded execution remains unknown, not optimistically working. Other platform qualification remains open. Native harnesses with no resident TORCH executor may continue to rely on durable reports; this change does not claim to discover every external runtime process.
The live console pilot must load the source candidate to expose this observation. Updating its owned server does not upgrade the installed distribution or restart running agent turns. Broader startup/launch reservation work remains tracked by `TASK-pilot-starting-presence`.
Test integrity scenarios are recorded in docs/TEST_INTEGRITY.md. No existing expectations or test timeouts are relaxed.
@@ -0,0 +1,23 @@
# Rolling continuation without a whole-batch barrier
Owner-requested fix for `TASK-d95e72d0-816d-4fc2-acaf-b73642fd4881`.
The serialized controller reserves and charges each launch before dispatch. It waits for the first pending turn to complete, records its outcome, re-reads current policy/backlog/messages and immediately fills any available slot. A long manager turn no longer prevents other eligible identities from running. The controller lock still excludes competing automatic controllers; pending launches and all observed physical turn guards count toward automatic capacity. Owner-requested manual batches are separate authorization and are not silently included in the daily automatic attempt ledger.
The same approved roster, per-identity mutex, pause, detached state and daily cap remain authoritative. A pause prevents additional launches and never kills an already-started turn. Unknown or failed executor outcomes are not silently retried. Retained crashed guards/controller locks remain fail-closed and require supported recovery.
Successful turns may continue unchanged assigned/in-progress work if recorded dependencies are complete. After two consecutive successful turns leave the same task IDs/revisions unchanged, further unchanged-work retries are held and one durable request asks the Manager to resolve the blocker. New task revisions or fresh messages can justify another bounded turn. This is a conservative no-progress signal, not proof that no files changed or that a task is done; the Manager must inspect evidence. Waiting, blocked, unassigned, completed or unmet-dependency work is not automatically converted into implementation work. Existing one-assignment rules and self-claim policy are not changed.
Resume instructions now request eligible implementation/verification/coordination rather than just a status report. They do not grant additional ownership or approval authority. All output completion remains exact-evidence gated.
Status exposes occupied guard identities and no-progress holds. Installed release and reboot persistence remain unqualified. Existing native pilot service executes this source candidate on its next activation; the daily limit remains 12 automatic attempts, not a token-cost governor.
Eligible identities are ordered least-recently-dispatched within the current UTC day, with the Manager first only on ties. A repeatedly revised specialist task must not consume every freed slot while an eligible peer has never started.
## Visible operating stops
The read-only project observation includes the automatic-turn budget, remaining attempts, exact pause/cap stop, retained capacity guards and current no-progress holds. It never resets the ledger, changes authority or treats declared policy as verified timer installation. No-progress holds are displayed only while the same active task IDs/revisions remain; resolved or revised tasks do not retain stale alerts.
An exhausted automatic allowance appears as an owner policy decision in Overview and as an explicit Fleet status line. Manager-owned no-progress waits remain with the Manager, rather than being falsely routed to the owner. There is no fabricated approval button or silent allowance increase. Manual owner turns are not included in this ledger; token/cost governance remains unknown.
Deterministic acceptance scenarios and anti-cheat notes are in docs/TEST_INTEGRITY.md. Independent live qualification remains necessary; do not close broader event-driven or persistent-operations umbrellas based on this source fix alone.
@@ -0,0 +1,15 @@
# Explicit unread inbox on startup and resume
Legacy `torch_read_messages` defaults to the oldest 100 historical messages.
Observed native inboxes exceeded 100 unread messages during fleet supervision.
Launch/resume now explicitly requests the bound identity's unread inbox with a
limit of 1000 and explains chronological order, potential overflow and deliberate
acknowledgements. This uses the existing tool without changing its API contract.
This mitigation does not implement pagination, newest-first history, coherent
cross-query observation, or complete overflow detection. Those remain in the
existing unread-first inbox task. A result of 1000 is explicitly uncertain,
not a complete-inbox claim. No permissions follow from reading a message.
SCN-fleet-startup-inbox exercises the actual fresh/resume launch arguments and
registered identity-bound inbox tool against messages beyond the default page.
@@ -0,0 +1,172 @@
# Fleet arbiter checkpoint — 2026-10-03, 05:58 UTC
## Outcome
The owner-approved blocker-clearing directive produced concrete component
commits and exact QA contributor consent. No new verified fleet landing has
been established in this checkpoint. Automatic usage reached the approved
48-turn cap; no additional provider turns were started or budget increased.
## Decisions and evidence
- Work committed its read-only convergence component at
`7e0734ae90314b6182046b1d813efd40b453743f`.
- Release committed read-only CLI routing at
`dd77b7015e5d34851725237d6f424babccf7f7d4`.
- QA message `c3f7ac15-5d40-4f85-a27b-6d5837b141a9` approved the exact
compact-card source/test contribution. Actual browser capability remains
unavailable in those native sessions; this is not visual acceptance.
- Arbiter directive `8865db38-a077-4935-a154-7b566d65aa44` authorized
source-only isolated composition without managed-branch convergence or
integration. Corresponding directives were persisted for all implementers.
## Concrete compatibility failure
A detached checkout at canonical
`7e3d333c5467c15d72be950f1fac9c3cd5439aee` was created at
`/tmp/torch-readonly-composition-EirOji/candidate`. Applying exactly Kernel
`18ccb0890d93c8a01e2c2d96db775b1ec14fe973` with `cherry-pick -x` produced
content conflicts in `src/control-plane/service.mjs`. Composition stopped;
Work and Release components were not applied, and no composed test PASS is
claimed. The conflicted detached checkout is retained as diagnostic evidence.
Canonical and managed agent checkouts were not altered by this operation.
The conflicts expose future schema-v3/reservation prerequisites absent from
canonical, which initializes schema version 2. The reader requires
`runtime_launch_reservations`, and its branch-local refusal scenario treats
version 2 as incompatible. Pulling the entire future branch would therefore
broaden this narrow current-branch unblock into an architecture upgrade.
The existing Kernel reader tests passed 2/2 on the Kernel branch under hermetic
fixtures (`/tmp/torch-reader-component-review-20261003.log`). This is source
review evidence only: it does not establish canonical compatibility, composed
CLI behavior, operational receipts, independent QA, or installed qualification.
## Next bounded action
Kernel should supply a current-canonical-compatible reader slice without
future reservation APIs, schema migration or live writer initialization. QA
must approve strict version-relative refusal scenarios, not weaken assertions.
Immutable SQLite observation also needs a demonstrated consistent snapshot or
fail-closed behavior under concurrent writers, not only a sidecar precheck.
Correction request `34a90b11-a64d-4ed7-a7ae-cf2ba8ef46f2` and corresponding
manager/peer requests preserve the exact evidence and composition boundary.
## Controller handoff
The old three-slot controller drained without termination. At 05:57:52 UTC,
the replacement seven-slot worker ran, obtained the released controller lock,
and returned `daily-turn-cap` with `launched: []`. This verifies a cap-preserving
handoff, not seven concurrent native workers or reboot persistence. The
replacement timer remains configured; the ledger was not reset.
## Remaining gates
Current-base compatible composition, strict composed CLI scenarios, exact
source gates, independent QA, guarded serialized integration and next-work
continuation remain open. Native execution cannot continue today without an
explicit new budget decision; the existing policy resets its daily allowance
at UTC midnight. No deployment, installed-runtime upgrade, live DDL, authority
expansion or completion assertion follows from this checkpoint.
## Subsequent verification and delivery blockers
The sections above describe the earlier checkpoint, not the latest outcome.
Canonical source subsequently reached `3bb9c55`; the installed accepted runtime
was not upgraded by the following source reviews.
### Current-v2 reader and CLI
The corrected Kernel slice `c79f8d4`, Work convergence slice `7e0734a`, and
Release CLI slice `dd77b70` applied cleanly to canonical `3bb9c55` in the
detached review tree `/tmp/torch-current-reader-review-vFjsSi/candidate`.
The resulting candidate was `62629b806b00b4ceb8701c754c65b22738614eb9`.
Root's full suite passed 282 tests with four explicit skips. A focused command
named nonexistent `cli.test.mjs`; its 11 passing tests therefore did not prove
CLI coverage. The actual full-suite glob did include `cli-scenarios.test.mjs`.
Root then removed the reader's `BEGIN` and transaction-open flag in an isolated
mutant. The original four reader scenarios still passed: a demonstrated
coverage gap, not proof of a production-reader defect. QA granted an additive
correlated-generation scenario, delivered as
`85914d3ef8a310878cec8345d976e78b3128d47e` atop `62629b8`.
Root independently verified five reader scenarios passing on that exact
candidate. The same no-transaction mutant failed the new scenario because a
single reader combined generation A and generation B, not because close failed.
Evidence was delivered to QA and Manager in owner messages `23102515` and
`fd088d9f`. The review-only mutant remains in
`/tmp/torch-reader-oracle-independent-W4arvj/candidate`; do not promote it.
Work separately delivered `cdc3aa9b800f7f2c1c925e1d8ecd21d9948fd3d2`, a sibling
of `85914d3` atop `62629b8`. Its additive CLI scenario invokes actual `brief`,
`converge plan`, and `converge guards` against a sidecar-free, write-refusing
fixture and checks unchanged database bytes. Root verified that exact targeted
scenario passing in `/tmp/torch-cli-reader-independent-YpA8xa/candidate` after
providing dependencies. An initial invocation used the wrong working directory
and failed importing `zod` before assertions; that setup failure is not a
behavioral test result. No composed `85914d3` plus `cdc3aa9` candidate, native
check receipt, installed qualification, or integration is asserted here.
### Compact-card source corrections
Candidate `29afa15` initially left long title/domain metadata unbounded: an
isolated headless reproduction grew a card from 407px to 2,803px. Its current
controls link also left the modal open over the destination. Correction
`063034274a9e42b589aed6ef6071780b06f9a3b7` bounds those fields and closes/routes
the modal. Root independently ran all six focused browser scenarios successfully.
QA reported full-suite/check/dashboard/lint success for that source candidate.
This is hermetic demo evidence, not connected live desktop/mobile qualification,
uniform-height acceptance across every card type, or installed delivery.
The correction contains `Closes: TASK-console-compact-details`; the full task
remains incomplete. Manager reported that automatic closure refuses a blocked
task that is not ready to integrate. Preserve that boundary rather than making
the task integration-ready solely to land a source slice. Existing managed
Console branch `2261e9b` and its history remain separate from the detached source.
### Failed gates and operational requests
Runtime's converged `0bb5ca4a084ad19a99cef1d8ea3162bf29272f8f` and Kernel's
`85914d3` both reported full-suite cancellation at the existing 60-second
`cli-scenarios.test.mjs` boundary. Neither is a green full-suite result.
Runtime preserved its excerpt and audit in its worktree under
`test_logs/cli-suite-timeout-0bb5ca4a.observed-output.log` and
`reports/test-audit/cli-suite-timeout-report-2026-10-03.md`.
There is no TORCH check receipt for that direct `npm run test` invocation.
The captured file-level output does not identify an internal CLI subscenario.
No timeout increase, assertion relaxation, or retry-until-green was authorized.
Root submitted these concrete findings through durable owner requests:
- `700f9ac7`: live snapshot response took 9.182 seconds; the following static
page response took 0.003552 seconds. Synchronous doctor/Git observation is a
candidate cause, not a proven root cause. Profile without hiding stale state.
- `db2380f0`: configuration defines a one-slot browser resource, but full-test
and dashboard-check resource lists are empty. Direct test commands have no
queue receipt. This coordination gap is verified; its role in the timeout
is not. Avoid new competing full/browser suites while diagnosing it.
- `0d25bf3e`: ordinary integration intake requires a registered specialist
branch tip equal to the candidate SHA. Detached `626`/`859`/`cdc` do not meet
that boundary, and Session Manager is not a feature-source identity. A
reviewed lossless adoption path is needed; do not impersonate a specialist,
reset its branch, import its whole future branch, or fabricate receipts.
- `ea12134f`: Owner Console had both live parent and native Codex child for
approximately 42 minutes while its durable progress report remained old.
Separate executor liveness from recent activity and actual waiting reasons.
Age alone does not authorize termination, guard cleanup, or restart.
### Launch allowance and next actions
An audited one-day allowance permitted 96 automatic launches on UTC
2026-10-03, while the ordinary future daily cap remains 48. The ledger reached
96 and the live observation returned `daily-turn-cap`. In-flight turns were
allowed to finish; no ledger reset or further allowance was granted. The policy
uses UTC midnight, not local midnight. It is an invocation allowance, not proof
of token/cost governance or reboot-persistent scheduling.
Next work remains: independent current-candidate QA; deterministic diagnosis
of the failed CLI gate and correct resource admission; lossless candidate
adoption/intake; exact-SHA qualifying receipts; serialized integration;
installed sandbox/startup verification; connected live UI review; and the
remaining roadmap. Durable manager backlog and owner messages remain the
authoritative queue. This report does not close those tasks or broaden authority.
@@ -0,0 +1,53 @@
# Owner repair and bounded continuation pilot
## Candidate and verification
Canonical development branch: `rewrite/portable-agent-fleet`.
- `11dabd2`: exact stopped-unknown owner recovery, physical turn guard, opt-in continuation.
- `4c42715`: reviewed Runtime MCP XDG runtime-directory forwarding, cherry-picked without its unfinished broader branch.
- `f823714`: bounded parallel turns and signal watermarks that do not wake on acknowledgements alone.
- Complete suite: 266 PASS, 4 skipped, zero failures/cancellations (`/tmp/torch-continuation-final-suite.log`). Syntax check and full lint PASS. Subsequent MCP focused suite: 6 PASS after removing unnecessary string escapes, with identical assertion semantics.
These are local/source verification results, not independent QA acceptance or an installed-release upgrade. No acceptance assertions or test timeouts were relaxed.
## Actual recovery
Owner-selected legacy units were verified terminal, with no live matching native process. The new public source CLI recovered these identities to offline, retaining prior unknown outcomes and runtime/session/task data:
| Identity | Owner audit |
| --- | --- |
| owner-console | cfae5b3d-695b-4657-95f0-cdba230741db |
| qa | 158bc692-e775-49a2-987f-8d9d3e8ffb9a |
| release-self-host | 7f618cd6-86cd-471f-a5f0-d7f03f8c6fb4 |
This is an explicit owner-selected legacy invocation binding, not cryptographic session provenance. Old failed units remain evidence. No processes were killed, task/test results forged, reservations fabricated, or schema migrations applied.
## Actual pilot
Project: `3dc11932-6e3c-42aa-9a8a-ca7b8f818b54`.
Transient user timer/service: `torch-3dc11932-6e3c-42aa-9a8a-ca7b8f818b54-continuation-pilot`.
First activation: 2026-10-03 04:00:18 UTC. Source CLI starts at most three existing approved identities concurrently, charges attempts before launch, and checks again two minutes after the previous batch exits. At verification, three attempts were charged and physical guards existed for session-manager, project-kernel and provider-runtime; their source `up` child processes were running. Daily cap: 12 turns, UTC day. This is a turn cap, NOT measured token/cost governance. No additional identities were authorized.
Monitoring URL returned HTTP 200: <http://127.0.0.1:4174/console>.
Pause future turns from the canonical repository:
```sh
node bin/torch.mjs continuation pause --yes --json
```
Pause does not kill already-started turns. Inspect policy/eligibility with `node bin/torch.mjs continuation status --json`. The timer remains a cheap deterministic check when paused or capped.
## Remaining qualification
- Independent QA of this exact composed candidate and real native completion/continuation.
- Startup presence truthfulness: physical launches may still display idle until their first status report.
- Installer-owned persistent timer registration, removal, upgrade and reboot qualification. This transient timer survives chat exit, not host reboot.
- Crash reconciliation for retained controller/turn locks; no time-based expiry silently permits overlapping executors.
- Real usage governor, runtime/model/auth qualification and operational failure reporting.
- Existing umbrella tasks remain open; the previously observed CLI timeout still needs a root-cause diagnosis despite this passing run.
Installed alpha.3 runtime and console service were not replaced. The older schedule framework's automatic model wakes remain disabled; the source continuation pilot is separately authorized.
@@ -0,0 +1,45 @@
# Experimental self-host activation — alpha.4.1
## Activated
The owner approved two bounded independent QA turns. Final QA verdict
`f0db9c8e-c2c7-4d60-a4c6-a23567643907` accepted exact source
`b0cab8eca1bc62ee908875dfac441a1b8a655fa0` for experimental active-pointer
activation only. Installed version is `0.1.0-alpha.4.1`; alpha.3 remains available
for rollback. No provider/model settings or daily usage limit were changed.
Real package acceptance: 293 PASS, zero failures/cancellations, four explicit
skips (297 total); syntax and lint passed. Acceptance digest:
`a522c0ac7058cf20187f13a7e9be694f90241e4e5fedff0eea6c8beeb84dd362`.
Receipt lives in the installed version's `.torch-validation.json`.
Isolated installed smoke verified first-use installation, read-only brief and
convergence readers, startup dry-run, alpha.3 rollback, re-upgrade and identical
project configuration, without launching any provider.
Evidence: `/tmp/torch-alpha41-installed-smoke-result.json`.
The existing console and continuation service ExecStart overrides now follow
the validated active pointer. Only the owned console was restarted; the timer's
cadence and budget are unchanged. Live loopback snapshot retrieval succeeded at
`http://127.0.0.1:4174/console`. Installed manager and QA briefs succeeded from
the host. The temporary qualification console on port 4175 was stopped.
## Boundaries and remaining work
- Canonical source remains `93602042eb561d7882549ffe298def4c29e4d1cf` on
`rewrite/portable-agent-fleet`. Functional changes are committed on
`release/selfhost-alpha4-20261003`, not canonically integrated or published.
- QA did not approve canonical landing, deployment, publication or fleet resume.
Registered-source adoption and native exact-commit integration gates remain.
- Browser resource admission changes in the release configuration are not yet
applied to the canonical project's tracked configuration.
- The historical CLI timeout's cause is not established. Bounded test-process
concurrency is mitigation, not retrospective qualification of failed checks.
- Collapsed warning cards now retain full accessible modal evidence; uniform
coverage of every card type and connected live browser review remain open.
- QA's earlier native sandbox brief access failure still needs a native-runtime
follow-up; successful host readers do not prove sandbox access is corrected.
- The ordinary fleet allowance is unchanged; the current one-day allowance was
exhausted. No additional specialist turn was authorized or launched.
This record is not a broader release-readiness claim.
@@ -0,0 +1,50 @@
# Owner-authorized unlimited fleet resume
The owner explicitly requested removing the daily limit and continuing against
subscription access. Installed alpha.4.2 source is committed at
`df8e557685a5dbf4a27f5f03098ecb4495615527` on
`release/selfhost-alpha4-20261003`. Functional source is not canonically integrated.
Real installed-candidate acceptance: 294 PASS, zero failures/cancellations,
four explicit skips. Accepted digest:
`8ffed308d89a696ac3b62ce6568fe23a0920c7221e0b651f2a0cec7de1231477`.
The unlimited scenario first failed against capped code, then passed; an added
physical-guard check initially used the wrong API signature and was corrected
without dropping assertions. Failed intermediate acceptance was not activated.
Installed isolated smoke passed fresh install, unlimited CLI, read-only brief,
pause preserving policy, restoring a finite cap before rollback, and unchanged
project configuration. Zero providers were launched by smoke tests.
Evidence: `/tmp/torch-alpha42-installed-smoke-result.json`.
## Live state
Activated alpha.4.2 through the accepted-version service. Set owner-audited
`maxTurnsPerDay: null`, cleared the obsolete same-day override and retained all
charged attempts. Concurrency remains seven. Sent durable resume/priority
instructions to all seven standing identities, including an independent QA
request for this incremental change; that request is not a completed review.
Started the existing continuation service and refreshed only the owned console.
At 2026-10-03T20:11:01Z the installed live snapshot verified all seven identities
with active process-backed working guards: Session Manager, Project Kernel,
Provider Runtime, Work and Integration, Owner Console, Release and Self-host,
Independent QA. Attempt count was 103, with no daily stop reason.
Snapshot: `/tmp/torch-alpha42-live-snapshot.json`.
Dashboard remains `http://127.0.0.1:4174/console`. Its continuation label now says
the daily limit is disabled instead of displaying a null/fake allowance.
## Boundaries
Unlimited mode persists until the owner changes it; it is not a credit meter.
No paid API fallback, model/profile change or bypass of approvals, ownership,
verification, physical executor guards or stalled-work holds was enabled.
Provider quota/auth failures remain failures, not permission to route elsewhere.
The manager was instructed to avoid retry storms and prioritize unblockers.
Before rolling back to alpha.4.1 or older, restore a finite cap with the current
runtime; those versions correctly reject the new null policy. This prevents an
old runtime from silently interpreting unlimited as an ordinary allowance.
Canonical integration, native sandbox brief follow-up and broader release
qualification remain pending. Active execution alone is not proof of landed work.
+12
View File
@@ -16,6 +16,18 @@ function attentionGroups(snapshot) {
const agentName = (id) => agents.get(id)?.title ?? id ?? 'Owner not recorded';
const add = (group, item) => groups[group].push(item);
const approvals = (snapshot.approvalRequests?.items ?? []).filter((approval) => approval.status === 'pending');
const continuation = snapshot.continuation;
if (continuation?.available && continuation.stopReason === 'daily-turn-cap') add('owner', {
tone: 'decision', title: 'Automatic fleet work stopped at its daily limit', owner: 'Project owner',
detail: 'No additional automatic agent turns can start today unless you explicitly change the limit. Existing turns may finish; idle does not mean all work is done.',
evidence: `${continuation.attempts}/${continuation.maxTurnsPerDay} automatic attempts · UTC day ${continuation.day} · not measured token/cost usage`,
href: '#fleet', action: 'Review continuation policy', decisionApprovalId: null,
});
for (const hold of continuation?.held ?? []) add('fleet', {
tone: 'review', title: `${agentName(hold.areaId)} needs a concrete coordination decision`, owner: agentName('session-manager'),
detail: 'Two completed turns left the assigned task state unchanged. Unchanged-work retries are held; inspect evidence and resolve the actual blocker rather than restarting blindly.',
evidence: hold.areaId, href: '#communications', action: 'Open coordination messages', requestOwner: 'session-manager',
});
const advisory = [];
const recheckCodes = new Set(['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING']);
+1
View File
@@ -183,6 +183,7 @@
<section id="fleet" class="console-section fleet-section" data-console-view="fleet" 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>
<p id="continuation-status" class="surface-note" role="status">Reading automatic continuation policy…</p>
<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">
+7 -1
View File
@@ -1039,6 +1039,12 @@ function renderAttention(groups) {
}
function render(snapshot) {
const continuation = snapshot.continuation;
setText('#continuation-status', continuation?.available
? `Automatic turns: ${continuation.attempts}/${continuation.maxTurnsPerDay} used today (UTC); ${continuation.remainingTurns} left. ${continuation.dailyOverride?.day === continuation.day ? `Owner-approved extra allowance expires at UTC midnight; normal daily cap ${continuation.configuredMaxTurnsPerDay}. ` : ''}Concurrency policy: up to ${continuation.maxConcurrency ?? 3}. ${continuation.stopReason === 'daily-turn-cap'
? 'No further automatic starts: the owner-controlled daily cap is reached. Running turns may finish.'
: continuation.stopReason === 'paused' ? 'Owner policy is paused; no new automatic starts.' : 'Policy permits new eligible turns; this does not verify timer installation.'} ${continuation.held?.length ?? 0} unchanged-work hold(s) need Manager coordination. This is not measured token or cost usage.`
: 'Automatic continuation policy is unavailable; do not assume the fleet can start more work.');
const project = snapshot.project ?? snapshot.repository;
const agents = snapshot.agents ?? [];
const tasks = snapshot.backlog ?? [];
@@ -1087,7 +1093,7 @@ function render(snapshot) {
return `<article class="agent-row">
<span class="presence ${escapeHtml(agent.state)}" aria-label="${live ? 'Active' : 'Offline'}"></span>
<div class="agent-copy"><strong>${escapeHtml(agent.title ?? agent.areaId)}</strong><span>${escapeHtml(agent.summary ?? profile)}</span><small>${escapeHtml(agent.areaId)} · ${escapeHtml(profile)}</small></div>
<div class="agent-state"><strong>${escapeHtml(agent.state ?? 'unknown')}</strong><small>${escapeHtml(ageLabel(agent.heartbeatAt))}</small></div>
<div class="agent-state"><strong>${escapeHtml(agent.state ?? 'unknown')}</strong><small>${escapeHtml(agent.executor?.state === 'active' ? 'executor verified live' : ageLabel(agent.heartbeatAt))}</small></div>
</article>`;
}).join('') : empty('Install an approved Fleet to see persistent identities.'));
+18 -3
View File
@@ -31,15 +31,28 @@ function commandRecord(command, args, cwd) {
return { command, args, cwd, mutatesRuntime: true };
}
function mcpArgs(mcp) {
function mcpRuntimeEnvironmentNames(env, platform) {
const runtimeDirectory = env?.XDG_RUNTIME_DIR;
if (platform !== 'linux' || typeof runtimeDirectory !== 'string'
|| !/^\/run\/user\/[1-9]\d*$/.test(runtimeDirectory)) {
return [];
}
return ['XDG_RUNTIME_DIR'];
}
function mcpArgs(mcp, { env, platform } = {}) {
if (!mcp) return [];
if (typeof mcp.name !== 'string' || !/^[a-z0-9_-]+$/.test(mcp.name)
|| typeof mcp.command !== 'string' || !Array.isArray(mcp.args)) {
throw new TorchError('Codex MCP launch configuration is invalid', { code: 'INVALID_RUNTIME_INPUT' });
}
const environmentNames = mcpRuntimeEnvironmentNames(env, platform);
return [
'-c', `mcp_servers.${mcp.name}.command=${JSON.stringify(mcp.command)}`,
'-c', `mcp_servers.${mcp.name}.args=${JSON.stringify(mcp.args)}`,
...(environmentNames.length
? ['-c', `mcp_servers.${mcp.name}.env_vars=${JSON.stringify(environmentNames)}`]
: []),
];
}
@@ -169,12 +182,14 @@ export function diagnoseCodexStartupFailure({ stdout, stderr } = {}) {
export class CodexRuntimeAdapter {
constructor({
env = process.env,
platform = process.platform,
runner = null,
executable = 'codex',
nodeExecutable = process.execPath,
} = {}) {
this.name = 'codex';
this.env = env;
this.platform = platform;
this.runner = runner;
this.executable = executable;
this.nodeExecutable = nodeExecutable;
@@ -241,7 +256,7 @@ export class CodexRuntimeAdapter {
const cwd = text(worktree, 'worktree');
const args = [
'--cd', cwd, ...launchPolicyArgs(launchPolicy ?? this.configuration.launchPolicy),
...mcpArgs(mcp),
...mcpArgs(mcp, { env: this.env, platform: this.platform }),
...((reasoning ?? this.configuration.reasoning) ? ['-c', `model_reasoning_effort=${JSON.stringify(text(reasoning ?? this.configuration.reasoning, 'reasoning'))}`] : []),
'exec', '--json',
...optionalModelArgs(model ?? this.configuration.model),
@@ -266,7 +281,7 @@ export class CodexRuntimeAdapter {
: '';
const args = [
'--cd', cwd, ...launchPolicyArgs(launchPolicy ?? this.configuration.launchPolicy),
...mcpArgs(mcp),
...mcpArgs(mcp, { env: this.env, platform: this.platform }),
...((reasoning ?? this.configuration.reasoning) ? ['-c', `model_reasoning_effort=${JSON.stringify(text(reasoning ?? this.configuration.reasoning, 'reasoning'))}`] : []),
'exec', 'resume', '--json',
...optionalModelArgs(model ?? this.configuration.model), sessionId, `${text(message, 'message')}${currentInstructions}`,
+55 -1
View File
@@ -14,7 +14,7 @@ import { startConsole } from './console/server.mjs';
import { classifyRecoverability, createLocalCanonical, planLocalCanonical } from './canonical/local.mjs';
import { openControlPlane, openControlPlaneReadOnly } from './control-plane/service.mjs';
import { fetchCanonicalObjects, planCanonicalFetch } from './canonical/fetch.mjs';
import { spawnSync } from 'node:child_process';
import { spawn as spawnProcess, spawnSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import {
mkdirSync, mkdtempSync, renameSync, rmSync, statSync, writeFileSync,
@@ -70,6 +70,8 @@ import {
} from './design/architect.mjs';
import { FleetEvolutionService } from './evolution/service.mjs';
import { HierarchyEvolutionService } from './evolution/hierarchy.mjs';
import { issueStoppedExecutorEvidence } from './runtime/stopped-executor-evidence.mjs';
import { configureContinuation, planContinuation, runContinuation } from './runtime/continuation.mjs';
import { ConvergenceService } from './convergence/service.mjs';
import {
attachForge, detachForge, forgeStatus, planForgeAttach, planForgeDetach,
@@ -95,6 +97,8 @@ Usage:
torch install --restore [--dry-run] [--yes] [--json]
torch setup [--parent <path>] [--dry-run] [--yes] [--json]
torch up [--fresh] [--only <id,id>] [--dry-run] [--yes] [--json]
torch recover-runtime --area <id> --unit <service> --invocation <id> [--expected-session <id> --expected-updated-at <time> --yes] [--json]
torch continuation <status|enable|pause|tick> [--max-turns-per-day <1-48>] [--today-max-turns <1-96>] [--concurrency <1-7>] [--limit <1-7>] [--yes] [--json]
torch down [--dry-run] [--yes] [--json]
torch capture [--json]
torch detach [--dry-run] [--yes] [--json]
@@ -585,6 +589,56 @@ export async function runCli(argv = process.argv.slice(2), {
process.stdout.write(HELP);
return 0;
}
if (command === 'continuation') {
const control = openControlPlane({ repositoryRoot: optionValue(argv, '--repo') ?? cwd, env });
try {
const operation = argv[1] ?? 'status';
const backlog = new BacklogService({ repositoryRoot: control.repositoryRoot, controlPlane: control });
if (operation === 'status' || !argv.includes('--yes') || argv.includes('--dry-run')) {
print(planContinuation(control, backlog), { json }); return 0;
}
if (operation === 'enable' || operation === 'pause') {
print(configureContinuation(control, { actorId: 'owner', enabled: operation === 'enable',
maxTurnsPerDay: optionValue(argv, '--max-turns-per-day') === undefined ? undefined : Number(optionValue(argv, '--max-turns-per-day')),
todayMaxTurns: optionValue(argv, '--today-max-turns') === undefined ? undefined : Number(optionValue(argv, '--today-max-turns')),
maxConcurrency: optionValue(argv, '--concurrency') === undefined ? undefined : Number(optionValue(argv, '--concurrency')) }), { json }); return 0;
}
if (operation !== 'tick') throw new TorchError('Unknown continuation operation.', { code: 'UNKNOWN_COMMAND' });
print(await runContinuation(control, backlog, { actorId: 'owner', limit: optionValue(argv, '--limit') === undefined ? undefined : Number(optionValue(argv, '--limit')),
launch: areaId => new Promise(resolveTurn => {
const child = spawnProcess(process.execPath, [fileURLToPath(new URL('../bin/torch.mjs', import.meta.url)),
'up', '--only', areaId, '--yes', '--json'], { cwd: control.repositoryRoot, env,
stdio: ['ignore', 'pipe', 'pipe'] });
child.stdout.resume(); child.stderr.resume();
let failed = false;
child.on('error', () => { failed = true; });
child.on('close', (code, signal) => resolveTurn(failed || signal ? null : code));
}) }), { json });
return 0;
} finally { control.close(); }
}
if (command === 'recover-runtime') {
const control = openControlPlane({ repositoryRoot: optionValue(argv, '--repo') ?? cwd, env });
try {
const areaId = optionValue(argv, '--area');
const issued = issueStoppedExecutorEvidence({ controlPlane: control, actorId: 'owner', areaId,
unit: optionValue(argv, '--unit'), invocationId: optionValue(argv, '--invocation') });
if (!argv.includes('--yes') || argv.includes('--dry-run')) {
print({ ...issued.plan, requiresApproval: true }, { json });
return 0;
}
const expected = issued.plan.expectedIdentity;
if (optionValue(argv, '--expected-session') !== expected.runtimeSessionId
|| optionValue(argv, '--expected-updated-at') !== expected.updatedAt) {
throw new TorchError('Apply requires the exact session and updated timestamp from the recovery preview.', {
code: 'RUNTIME_RECOVERY_SNAPSHOT_CONFLICT',
});
}
print(control.recoverOwnerStoppedUnknownIdentity({ actorId: 'owner', areaId,
expectedIdentity: expected, evidence: issued.evidence }), { json });
return 0;
} finally { control.close(); }
}
if (command === 'runtimes') {
const operation = argv[1] ?? 'list';
if (operation === 'update') {
+371 -11
View File
@@ -5,8 +5,9 @@ import { DatabaseSync } from 'node:sqlite';
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 { loadProjectConfig, validateProjectConfig } from '../kernel/config.mjs';
import { organizationGraphFromConfig } from '../kernel/organization.mjs';
import { consumeStoppedExecutorEvidence } from '../runtime/stopped-executor-evidence.mjs';
export const PRESENCE_STATES = Object.freeze([
'starting', 'working', 'waiting', 'idle', 'stale', 'stopping', 'offline',
@@ -14,9 +15,23 @@ export const PRESENCE_STATES = Object.freeze([
const MESSAGE_KINDS = new Set([
'message', 'coordination-request', 'handoff-request', 'completion', 'blocker', 'manager-check-in', 'manager-stale-work-review',
'approval-request', 'approval-decision',
'approval-request', 'approval-decision', 'owner-reply',
]);
const CONTROL_PLANE_V2_SCHEMA_VERSION = 2;
// This reader intentionally recognizes only the canonical v2 state shape. A
// newer state must be rejected rather than interpreted with a partial guess.
const CONTROL_PLANE_V2_COLUMNS = Object.freeze({
identities: ['area_id', 'runtime', 'runtime_session_id', 'state', 'summary', 'current_task', 'heartbeat_at', 'updated_at'],
messages: ['id', 'project_id', 'sender_id', 'recipient_id', 'kind', 'created_at', 'body', 'task_ref', 'path_ref', 'commit_ref', 'handoff_ref', 'delivered_at'],
message_acks: ['message_id', 'area_id', 'acknowledged_at'],
reports: ['id', 'project_id', 'area_id', 'kind', 'summary', 'task_ref', 'evidence', 'created_at'],
handoffs: ['id', 'project_id', 'sender_id', 'recipient_id', 'path_ref', 'task_ref', 'reason', 'status', 'created_at'],
audit_events: ['id', 'project_id', 'actor_id', 'operation', 'entity_type', 'entity_id', 'details', 'created_at'],
approval_requests: ['id', 'project_id', 'requester_id', 'approver_id', 'task_ref', 'title', 'summary', 'evidence', 'status', 'revision', 'created_at', 'decided_at', 'decided_by', 'decision_note'],
});
function readJson(path, code) {
try {
return JSON.parse(readFileSync(path, 'utf8'));
@@ -25,6 +40,40 @@ function readJson(path, code) {
}
}
function readJsonBytes(bytes, path, code) {
try {
return JSON.parse(bytes.toString('utf8'));
} catch (error) {
throw new TorchError(`Cannot read TORCH state at ${path}`, { code, details: error.message });
}
}
function readOnlyUnavailable(message, details = null) {
return new TorchError(message, { code: 'CONTROL_PLANE_READ_ONLY_UNAVAILABLE', details });
}
function exactColumns(database, table) {
return database.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name);
}
function validateV2ReadOnlySchema(database) {
const version = database.prepare('PRAGMA user_version').get()?.user_version;
if (version !== CONTROL_PLANE_V2_SCHEMA_VERSION) {
throw readOnlyUnavailable('The control-plane state is not the supported schema-v2 snapshot.', {
expected: CONTROL_PLANE_V2_SCHEMA_VERSION, actual: version ?? null,
});
}
for (const [table, expected] of Object.entries(CONTROL_PLANE_V2_COLUMNS)) {
const present = database.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
const actual = present ? exactColumns(database, table) : null;
if (!actual || actual.length !== expected.length || actual.some((name, index) => name !== expected[index])) {
throw readOnlyUnavailable('The control-plane state does not match the required schema-v2 tables.', {
table, expected, actual,
});
}
}
}
function requiredText(value, name) {
if (typeof value !== 'string' || !value.trim()) {
throw new TorchError(`${name} must be a non-empty string`, {
@@ -39,6 +88,35 @@ function optionalText(value, name) {
return requiredText(value, name);
}
function messageObservationSelection(value) {
if (value !== 'unread' && value !== 'history') {
throw new TorchError('Message observation selection must be unread or history', {
code: 'INVALID_MESSAGE_SELECTION', details: { selection: value, allowed: ['unread', 'history'] },
});
}
return value;
}
function messageObservationLimit(value) {
if (!Number.isInteger(value) || value < 1 || value > 1000) {
throw new TorchError('Message observation limit must be an integer from 1 through 1000', {
code: 'INVALID_MESSAGE_LIMIT', details: { limit: value, minimum: 1, maximum: 1000 },
});
}
return value;
}
function ownerConversationCursor(value) {
if (value === undefined || value === null) return null;
if (!value || typeof value !== 'object' || Array.isArray(value)
|| Object.keys(value).length !== 2 || !Object.hasOwn(value, 'createdAt') || !Object.hasOwn(value, 'id')) {
throw new TorchError('Owner conversation cursor must contain only createdAt and id', {
code: 'INVALID_OWNER_CONVERSATION_CURSOR',
});
}
return { createdAt: requiredText(value.createdAt, 'cursor.createdAt'), id: requiredText(value.id, 'cursor.id') };
}
function normalizeReferences(references = {}) {
return {
task: optionalText(references.task, 'references.task'),
@@ -112,6 +190,13 @@ function effectiveRoster(roster, config) {
}
function rowToMessage(row) {
const references = {
task: row.task_ref ?? null,
path: row.path_ref ?? null,
commit: row.commit_ref ?? null,
handoff: row.handoff_ref ?? null,
};
if (row.kind === 'owner-reply') references.replyTo = row.handoff_ref ?? null;
return {
id: row.id,
projectId: row.project_id,
@@ -122,12 +207,7 @@ function rowToMessage(row) {
body: row.body,
acknowledgedAt: row.acknowledged_at ?? null,
deliveredAt: row.delivered_at ?? null,
references: {
task: row.task_ref ?? null,
path: row.path_ref ?? null,
commit: row.commit_ref ?? null,
handoff: row.handoff_ref ?? null,
},
references,
};
}
@@ -224,10 +304,15 @@ function initializeSchema(database) {
}
export class ControlPlane {
constructor({ repositoryRoot, env = process.env, clock = () => new Date(), idFactory = randomUUID } = {}) {
constructor({ repositoryRoot, env = process.env, clock = () => new Date(), idFactory = randomUUID, readOnly = false } = {}) {
this.repositoryRoot = resolve(repositoryRoot ?? process.cwd());
this.clock = clock;
this.idFactory = idFactory;
this.readOnly = readOnly === true;
if (this.readOnly) {
this.initializeReadOnly(env);
return;
}
this.manifest = readInstallManifest(this.repositoryRoot);
this.config = loadProjectConfig(this.repositoryRoot);
this.roster = readJson(join(this.repositoryRoot, '.torch', 'roster.yaml'), 'INVALID_TORCH_ROSTER');
@@ -267,7 +352,104 @@ export class ControlPlane {
for (const areaId of this.agents.keys()) insert.run(areaId, 'offline');
}
initializeReadOnly(env) {
const trackedRoot = join(this.repositoryRoot, '.torch');
const manifestPath = join(trackedRoot, 'install-manifest.json');
const configPath = join(trackedRoot, 'torch.yaml');
const rosterPath = join(trackedRoot, 'roster.yaml');
try {
this.readOnlyInputs = [
{ path: manifestPath, bytes: readFileSync(manifestPath) },
{ path: configPath, bytes: readFileSync(configPath) },
{ path: rosterPath, bytes: readFileSync(rosterPath) },
];
this.manifest = readJsonBytes(this.readOnlyInputs[0].bytes, manifestPath, 'INVALID_INSTALL_MANIFEST');
this.config = validateProjectConfig(readJsonBytes(this.readOnlyInputs[1].bytes, configPath, 'CONFIG_INVALID'));
this.roster = readJsonBytes(this.readOnlyInputs[2].bytes, rosterPath, 'INVALID_TORCH_ROSTER');
} catch (error) {
if (error instanceof TorchError) throw error;
throw readOnlyUnavailable('The tracked control-plane inputs could not be read atomically.', { cause: error.message });
}
if (this.config.project?.id !== this.manifest.projectId) {
throw new TorchError('Tracked configuration and installation manifest disagree on project identity', {
code: 'PROJECT_ID_MISMATCH',
});
}
const localState = (this.manifest.external ?? []).find((entry) => entry.type === 'local-state');
if (!localState?.path || !existsSync(localState.path)) {
throw new TorchError('TORCH local project state is unavailable', { code: 'LOCAL_STATE_MISSING' });
}
const expectedStateRoot = projectStatePath(this.manifest.projectId, env);
if (resolve(localState.path) !== resolve(expectedStateRoot)) {
throw new TorchError('Installation manifest local state path does not match this environment', {
code: 'LOCAL_STATE_PATH_MISMATCH',
details: { manifest: localState.path, expected: expectedStateRoot },
});
}
const metadataPath = join(expectedStateRoot, 'project.json');
try {
this.readOnlyInputs.push({ path: metadataPath, bytes: readFileSync(metadataPath) });
} catch (error) {
throw readOnlyUnavailable('The local control-plane metadata could not be read.', { cause: error.message });
}
const metadata = readJsonBytes(this.readOnlyInputs[3].bytes, metadataPath, 'LOCAL_STATE_METADATA_INVALID');
if (metadata.projectId !== this.manifest.projectId || resolve(metadata.root) !== this.repositoryRoot) {
throw new TorchError('Local state metadata does not belong to this installation root', {
code: 'LOCAL_STATE_METADATA_MISMATCH',
});
}
this.stateRoot = expectedStateRoot;
this.projectId = this.manifest.projectId;
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',
});
}
try {
this.database = new DatabaseSync(join(this.stateRoot, 'state.db'), { readOnly: true });
// A normal read transaction preserves SQLite's locking and WAL semantics.
this.database.exec('BEGIN');
this.readTransactionOpen = true;
validateV2ReadOnlySchema(this.database);
for (const areaId of this.agents.keys()) {
if (!this.database.prepare('SELECT area_id FROM identities WHERE area_id = ?').get(areaId)) {
throw readOnlyUnavailable('The schema-v2 snapshot does not contain every registered identity.', { areaId });
}
}
this.assertReadOnlyInputsStable();
} catch (error) {
try { if (this.readTransactionOpen) this.database.exec('ROLLBACK'); } catch { /* close only */ }
try { this.database?.close(); } catch { /* close only */ }
if (error instanceof TorchError) throw error;
throw readOnlyUnavailable('A coherent read-only control-plane snapshot is unavailable.', { cause: error.message });
}
}
assertReadOnlyInputsStable() {
if (!this.readOnly) return;
for (const input of this.readOnlyInputs) {
let current;
try { current = readFileSync(input.path); } catch (error) {
throw readOnlyUnavailable('A control-plane input disappeared while it was being read.', { path: input.path, cause: error.message });
}
if (!current.equals(input.bytes)) {
throw readOnlyUnavailable('A control-plane input changed while the read-only snapshot was open.', { path: input.path });
}
}
}
assertWritable() {
if (this.readOnly) {
throw new TorchError('This control-plane handle is read-only.', { code: 'CONTROL_PLANE_READ_ONLY' });
}
}
refreshRoster() {
if (this.readOnly) {
this.assertReadOnlyInputsStable();
return { schema: this.roster.schema, areaIds: [...this.agents.keys()] };
}
const roster = readJson(join(this.repositoryRoot, '.torch', 'roster.yaml'), 'INVALID_TORCH_ROSTER');
const config = loadProjectConfig(this.repositoryRoot);
const agents = new Map(effectiveRoster(roster, config).map((area) => [area.id, area]));
@@ -284,11 +466,18 @@ export class ControlPlane {
}
close() {
if (this.readTransactionOpen) {
try { this.database.exec('COMMIT'); } finally { this.readTransactionOpen = false; }
}
this.database.close();
}
assertIdentity(areaId) {
this.refreshRoster();
return this.assertLoadedIdentity(areaId);
}
assertLoadedIdentity(areaId) {
const normalized = requiredText(areaId, 'areaId');
if (!this.agents.has(normalized)) {
throw new TorchError(`Unknown Fleet identity: ${normalized}`, {
@@ -307,6 +496,9 @@ export class ControlPlane {
identity(areaId) {
const id = this.assertIdentity(areaId);
const row = this.database.prepare('SELECT * FROM identities WHERE area_id = ?').get(id);
if (this.readOnly && !row) {
throw readOnlyUnavailable('The read-only snapshot is missing a registered identity.', { areaId: id });
}
return rowToAgent(row, this.agents.get(id));
}
@@ -366,6 +558,7 @@ export class ControlPlane {
}
sendMessage({ sender, recipient, body, kind = 'message', references = {} } = {}) {
this.assertWritable();
const senderId = this.assertIdentity(sender);
const recipientId = this.assertRecipient(recipient);
const normalizedKind = requiredText(kind, 'kind');
@@ -395,6 +588,7 @@ export class ControlPlane {
}
sendOwnerRequest({ actorId, recipient, body, references = {} } = {}) {
this.assertWritable();
const ownerId = this.assertOwnerActor(actorId);
const recipientId = this.assertIdentity(recipient);
const normalizedBody = requiredText(body, 'body');
@@ -425,9 +619,43 @@ export class ControlPlane {
return record;
}
sendOwnerReply({ sender, requestId, body } = {}) {
this.assertWritable();
const senderId = this.assertIdentity(sender);
const parentId = requiredText(requestId, 'requestId');
const parent = this.database.prepare(`
SELECT * FROM messages
WHERE id = ? AND project_id = ? AND sender_id = 'owner' AND recipient_id = ? AND kind = 'message'
`).get(parentId, this.projectId, senderId);
if (!parent) {
throw new TorchError('Owner reply must reference an owner request addressed to the sender', {
code: 'OWNER_REPLY_REFERENCE_INVALID', details: { requestId: parentId, sender: senderId },
});
}
const normalizedBody = requiredText(body, 'body');
const record = {
id: this.idFactory(), projectId: this.projectId, sender: senderId, recipient: 'owner',
kind: 'owner-reply', createdAt: this.clock().toISOString(), body: normalizedBody,
acknowledgedAt: null, deliveredAt: null,
references: { task: null, path: null, commit: null, handoff: parentId, replyTo: parentId },
};
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, null, null, null, parentId,
);
this.audit({ actorId: senderId, operation: 'message.reply-owner', entityType: 'message', entityId: record.id,
details: { requestId: parentId } });
return record;
}
readMessages({ recipient, unacknowledgedOnly = false, limit = 100 } = {}) {
const recipientId = this.assertIdentity(recipient);
const normalizedLimit = Number.isInteger(limit) && limit > 0 && limit <= 1000 ? limit : 100;
const normalizedLimit = messageObservationLimit(limit);
const query = `
SELECT m.*, a.acknowledged_at
FROM messages m
@@ -440,7 +668,96 @@ export class ControlPlane {
return this.database.prepare(query).all(recipientId, recipientId, normalizedLimit).map(rowToMessage);
}
observeMessages({ recipient, selection, limit } = {}) {
// Unlike legacy readMessages, this observation must not refresh the roster:
// refreshRoster maintains identity rows and is not a read-only observation.
const recipientId = this.assertLoadedIdentity(recipient);
const normalizedSelection = messageObservationSelection(selection);
const normalizedLimit = messageObservationLimit(limit);
const rows = this.database.prepare(`
WITH visible_messages AS MATERIALIZED (
SELECT m.*, a.acknowledged_at
FROM messages m
LEFT JOIN message_acks a ON a.message_id = m.id AND a.area_id = ?
WHERE m.recipient_id = ? OR m.recipient_id = 'all'
),
selected_messages AS MATERIALIZED (
SELECT * FROM visible_messages
WHERE ? = 'history' OR acknowledged_at IS NULL
),
page AS MATERIALIZED (
SELECT * FROM selected_messages
ORDER BY created_at ASC, id ASC
LIMIT ?
),
metadata AS (
SELECT
(SELECT COUNT(*) FROM selected_messages) AS selected_count,
(SELECT COUNT(*) FROM visible_messages WHERE acknowledged_at IS NULL) AS pending_unread_count
)
SELECT page.*, metadata.selected_count, metadata.pending_unread_count
FROM metadata
LEFT JOIN page ON TRUE
ORDER BY page.created_at ASC, page.id ASC
`).all(recipientId, recipientId, normalizedSelection, normalizedLimit);
const messages = rows.filter((row) => row.id !== null).map(rowToMessage);
const selectedCount = Number(rows[0]?.selected_count ?? 0);
const complete = selectedCount <= normalizedLimit;
return {
recipient: recipientId,
messages,
observation: {
selection: normalizedSelection,
requestedLimit: normalizedLimit,
returnedCount: messages.length,
complete,
truncated: !complete,
pendingUnreadCount: Number(rows[0]?.pending_unread_count ?? 0),
},
};
}
readOwnerConversation({ actorId, requestId, cursor, limit = 100 } = {}) {
this.assertOwnerActor(actorId);
const parentId = requiredText(requestId, 'requestId');
const pageCursor = ownerConversationCursor(cursor);
const normalizedLimit = messageObservationLimit(limit);
const request = this.database.prepare(`
SELECT * FROM messages
WHERE id = ? AND project_id = ? AND sender_id = 'owner' AND kind = 'message'
`).get(parentId, this.projectId);
if (!request) {
throw new TorchError('Owner conversation request is unavailable in this project', {
code: 'OWNER_CONVERSATION_NOT_FOUND', details: { requestId: parentId },
});
}
const rows = this.database.prepare(`
SELECT * FROM messages
WHERE project_id = ? AND recipient_id = 'owner' AND kind = 'owner-reply' AND handoff_ref = ?
AND (? IS NULL OR created_at > ? OR (created_at = ? AND id > ?))
ORDER BY created_at ASC, id ASC
LIMIT ?
`).all(
this.projectId, parentId,
pageCursor?.createdAt ?? null, pageCursor?.createdAt ?? null,
pageCursor?.createdAt ?? null, pageCursor?.id ?? null,
normalizedLimit + 1,
);
const hasMore = rows.length > normalizedLimit;
const replies = rows.slice(0, normalizedLimit).map(rowToMessage);
const last = replies.at(-1);
return {
request: rowToMessage(request), replies,
page: {
requestedLimit: normalizedLimit, returnedCount: replies.length,
complete: !hasMore, truncated: hasMore,
nextCursor: hasMore && last ? { createdAt: last.createdAt, id: last.id } : null,
},
};
}
ackMessage({ recipient, messageId } = {}) {
this.assertWritable();
const recipientId = this.assertIdentity(recipient);
const id = requiredText(messageId, 'messageId');
const message = this.database.prepare('SELECT * FROM messages WHERE id = ?').get(id);
@@ -459,7 +776,39 @@ export class ControlPlane {
return { messageId: id, recipient: recipientId, acknowledgedAt };
}
recoverOwnerStoppedUnknownIdentity({ actorId, areaId, expectedIdentity, evidence } = {}) {
this.assertWritable();
const owner = this.assertOwnerActor(actorId);
const id = this.assertIdentity(areaId);
if (!expectedIdentity || ['state', 'runtime', 'runtimeSessionId', 'updatedAt'].some(key => !expectedIdentity[key])) {
throw new TorchError('Recovery requires the complete previewed identity snapshot.', { code: 'RUNTIME_RECOVERY_SNAPSHOT_REQUIRED' });
}
this.database.exec('BEGIN IMMEDIATE');
try {
const current = this.identity(id);
if (current.state !== 'working' || !current.summary?.includes('Runtime executor completion is unknown')
|| ['state', 'runtime', 'runtimeSessionId', 'updatedAt'].some(key => current[key] !== expectedIdentity[key])) {
throw new TorchError('Identity changed since recovery was reviewed.', { code: 'RUNTIME_RECOVERY_SNAPSHOT_CONFLICT' });
}
const facts = consumeStoppedExecutorEvidence(evidence, { projectId: this.projectId, repositoryRoot: this.repositoryRoot, identity: current });
const now = this.clock().toISOString();
const result = this.database.prepare(`UPDATE identities SET state = 'offline', summary = ?, heartbeat_at = ?, updated_at = ?
WHERE area_id = ? AND state = ? AND runtime = ? AND runtime_session_id = ? AND updated_at = ?`)
.run('Owner verified stopped legacy executor; prior outcome remains unknown.', now, now,
id, current.state, current.runtime, current.runtimeSessionId, current.updatedAt);
if (result.changes !== 1) throw new TorchError('Recovery lost its identity snapshot.', { code: 'RUNTIME_RECOVERY_SNAPSHOT_CONFLICT' });
const audit = this.auditOwnerAction({ actorId: owner, operation: 'runtime.legacy-unknown.recover-stopped',
entityType: 'identity', entityId: id, details: { previousIdentity: expectedIdentity, ...facts } });
this.database.exec('COMMIT');
return { identity: this.identity(id), auditId: audit.id, priorOutcome: 'unknown', sessionsStarted: false, mutationPerformed: true };
} catch (error) {
this.database.exec('ROLLBACK');
throw error;
}
}
reportStatus({ areaId, state, summary, runtime, runtimeSessionId, task } = {}) {
this.assertWritable();
const id = this.assertIdentity(areaId);
const normalizedState = requiredText(state, 'state');
if (!PRESENCE_STATES.includes(normalizedState)) {
@@ -486,6 +835,7 @@ export class ControlPlane {
}
writeReport({ areaId, kind, summary, task, evidence } = {}) {
this.assertWritable();
const id = this.assertIdentity(areaId);
const createdAt = this.clock().toISOString();
const report = {
@@ -535,6 +885,7 @@ export class ControlPlane {
}
requestHandoff({ sender, recipient = 'session-manager', path, task, reason } = {}) {
this.assertWritable();
const senderId = this.assertIdentity(sender);
const recipientId = this.assertIdentity(recipient);
const pathRef = path ? normalizedProjectPath(path) : null;
@@ -576,6 +927,7 @@ export class ControlPlane {
}
requestApproval({ requester, approver, task, title, summary, evidence } = {}) {
this.assertWritable();
const requesterId = this.assertIdentity(requester);
const approverId = requiredText(approver, 'approver');
if (approverId === 'owner') this.assertOwnerActor(approverId);
@@ -651,6 +1003,7 @@ export class ControlPlane {
}
decideApproval({ approvalId, decidedBy, decision, note, expectedRevision } = {}) {
this.assertWritable();
const id = requiredText(approvalId, 'approvalId');
const actor = requiredText(decidedBy, 'decidedBy');
const owner = actor === 'owner';
@@ -708,6 +1061,7 @@ export class ControlPlane {
}
insertOwnerApprovalDecisionMessage({ sender, recipient, approvalId, task, decision, note }) {
this.assertWritable();
const id = this.idFactory();
const createdAt = this.clock().toISOString();
const body = `Approval ${decision}: ${note ?? 'No additional note.'}`;
@@ -732,6 +1086,7 @@ export class ControlPlane {
}
audit({ actorId, operation, entityType, entityId = null, details = null } = {}) {
this.assertWritable();
const actor = this.assertIdentity(actorId);
const event = {
id: this.idFactory(), projectId: this.projectId, actorId: actor,
@@ -750,8 +1105,9 @@ export class ControlPlane {
}
auditOwnerAction({ actorId, operation, entityType, entityId = null, details = null } = {}) {
this.assertWritable();
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)) {
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', 'runtime.legacy-unknown.recover-stopped', 'runtime.continuation.configure'].includes(operation)) {
throw new TorchError('This owner audit surface does not authorize the requested operation', {
code: 'OWNER_OPERATION_UNSUPPORTED', details: { operation },
});
@@ -803,3 +1159,7 @@ export class ControlPlane {
export function openControlPlane(options) {
return new ControlPlane(options);
}
export function openControlPlaneReadOnly(options) {
return new ControlPlane({ ...options, readOnly: true });
}
+384
View File
@@ -0,0 +1,384 @@
import { createHash, randomUUID } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { TorchError } from '../kernel/errors.mjs';
import { observeRuntimeTurn } from '../runtime/turn-guard.mjs';
const ADOPTION_SCHEMA = 'torch.dev/integration-adoption/v1alpha1';
const AUTHORITY_SCHEMA = 'torch.dev/integration-adoption-authority/v1alpha1';
const ADAPTER_NAME = 'git-private-ref-cas-v1';
const ACTIONS = new Set(['plan', 'prepare', 'finalize', 'reconcile', 'rollback', 'request']);
const MUTATING_ACTIONS = new Set(['prepare', 'finalize', 'rollback', 'request']);
const SHA = /^[0-9a-f]{40,64}$/;
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/;
const ZERO_SHA = '0'.repeat(40);
function git(root, args, { allowFailure = false } = {}) {
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
if (result.status !== 0 && !allowFailure) {
throw new TorchError(`Git command failed: git ${args.join(' ')}`, {
code: 'GIT_COMMAND_FAILED', details: result.stderr?.trim() || null,
});
}
return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' };
}
function tableExists(database, name) {
return Boolean(database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name));
}
function text(value, name) {
if (typeof value !== 'string' || !value.trim()) {
throw new TorchError(`${name} is required`, { code: 'ADOPTION_INPUT_INVALID', details: { name } });
}
return value.trim();
}
function sha(value, name) {
const normalized = text(value, name);
if (!SHA.test(normalized)) throw new TorchError(`${name} must be a full Git object id`, {
code: 'ADOPTION_INPUT_INVALID', details: { name },
});
return normalized;
}
function digest(value) {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
function durableRecord(event) {
if (!event?.details) return null;
try {
const record = JSON.parse(event.details);
return record?.schema === ADOPTION_SCHEMA ? record : null;
} catch { return null; }
}
function listWorktreePaths(root) {
const records = [];
for (const line of git(root, ['worktree', 'list', '--porcelain']).stdout.split('\n')) {
if (line.startsWith('worktree ')) records.push(line.slice('worktree '.length));
}
return records;
}
function authorityFrom(approval) {
if (!approval || approval.status !== 'approved' || approval.approver_id !== 'owner' || approval.decided_by !== 'owner') return null;
try {
const authority = JSON.parse(approval.decision_note ?? '');
return authority?.schema === AUTHORITY_SCHEMA ? authority : null;
} catch { return null; }
}
function exactAuthority(authority, binding) {
if (!authority || authority.action !== binding.action) return false;
const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest',
'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef'];
return fields.every((field) => authority[field] === binding[field])
&& authority.adapter?.name === binding.adapter?.name
&& authority.adapter?.id === binding.adapter?.id;
}
function exactInput(record, input) {
const fields = ['adoptionId', 'candidateSha', 'baseTargetSha', 'provenanceManifestDigest',
'contributorArea', 'destinationArea', 'targetBranch', 'candidateRef', 'expectedOldRef'];
const mismatches = fields.filter((field) => input[field] !== undefined
&& input[field] !== (field === 'adoptionId' ? record.id : record[field]));
if (input.provenance !== undefined && JSON.stringify(input.provenance) !== JSON.stringify(record.provenance)) mismatches.push('provenance');
if (mismatches.length) throw new TorchError('Lifecycle input cannot override immutable prepared adoption fields', {
code: 'ADOPTION_IMMUTABLE_BINDING_STALE', details: { adoptionId: record.id, fields: mismatches },
});
if (!Number.isInteger(input.expectedRecordRevision) || input.expectedRecordRevision !== record.revision) {
throw new TorchError('Lifecycle input must bind the current adoption record revision', {
code: 'ADOPTION_RECORD_REVISION_STALE',
details: { adoptionId: record.id, expectedRevision: input.expectedRecordRevision ?? null, actual: record.revision },
});
}
}
/**
* Real Git private-ref transaction adapter. It is source code only: callers
* decide when it is invoked, while the adapter itself gives temp-repository
* scenarios the same atomic update-ref semantics as a real repository.
*/
export class GitPrivateRefAdapter {
constructor({ repositoryRoot } = {}) {
this.repositoryRoot = resolve(text(repositoryRoot, 'repositoryRoot'));
this.name = ADAPTER_NAME;
this.id = digest({ adapter: this.name, repositoryRoot: this.repositoryRoot });
}
readRefs(refs) {
const values = {};
for (const ref of refs) {
const result = git(this.repositoryRoot, ['rev-parse', '--verify', '--quiet', ref], { allowFailure: true });
values[ref] = result.status === 0 ? result.stdout : null;
}
return values;
}
compareAndSwapRefs({ expected = [], writes = [] } = {}) {
const commands = ['start'];
const writtenRefs = new Set(writes.map((entry) => entry.ref));
for (const { ref, sha: expectedSha } of expected) {
if (writtenRefs.has(ref)) continue;
commands.push(`verify ${text(ref, 'ref')} ${expectedSha ?? ZERO_SHA}`);
}
for (const { ref, sha: nextSha } of writes) {
const expectedEntry = expected.find((entry) => entry.ref === ref);
if (!expectedEntry) throw new TorchError('Every private-ref write requires an exact expected value', {
code: 'ADOPTION_REF_CAS_EXPECTED_REQUIRED', details: { ref },
});
const current = expectedEntry.sha ?? null;
if (nextSha === null) commands.push(`delete ${text(ref, 'ref')} ${current ?? ZERO_SHA}`);
else commands.push(`update ${text(ref, 'ref')} ${sha(nextSha, 'next ref sha')} ${current ?? ZERO_SHA}`);
}
commands.push('prepare', 'commit', '');
const result = spawnSync('git', ['-C', this.repositoryRoot, 'update-ref', '--stdin'], {
input: commands.join('\n'), encoding: 'utf8',
});
if (result.status !== 0) throw new TorchError('Private ref compare-and-swap refused', {
code: 'ADOPTION_REF_CAS_FAILED', details: result.stderr?.trim() || null,
});
return { adapter: { name: this.name, id: this.id }, expected, writes };
}
}
export class AdoptionService {
constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID, policy, refAdapter } = {}) {
this.repositoryRoot = repositoryRoot;
this.controlPlane = controlPlane;
this.clock = clock;
this.idFactory = idFactory;
this.policy = policy ?? {};
this.refAdapter = refAdapter ?? new GitPrivateRefAdapter({ repositoryRoot });
}
plan({ action = 'plan', actorId, approvalId, approvalRevision, adoptionId,
candidateSha, baseTargetSha, provenance, provenanceManifestDigest, contributorArea,
destinationArea, targetBranch, candidateRef, expectedOldRef, adapter } = {}) {
const blockers = [];
const id = adoptionId ? text(adoptionId, 'adoptionId') : null;
if (id && !SAFE_ID.test(id)) blockers.push({ code: 'ADOPTION_ID_INVALID' });
if (!ACTIONS.has(action)) blockers.push({ code: 'ADOPTION_ACTION_INVALID' });
if (actorId !== 'owner') blockers.push({ code: 'OWNER_ACTOR_REQUIRED' });
if (!Number.isInteger(approvalRevision) || approvalRevision < 1) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_REQUIRED' });
const adapterBinding = adapter?.name && adapter?.id ? { name: adapter.name, id: adapter.id } : null;
if (MUTATING_ACTIONS.has(action) && !adapterBinding) blockers.push({ code: 'ADOPTION_ADAPTER_BINDING_REQUIRED' });
if (adapterBinding && (adapterBinding.name !== this.refAdapter.name || adapterBinding.id !== this.refAdapter.id)) {
blockers.push({ code: 'ADOPTION_ADAPTER_PROVENANCE_INVALID' });
}
let candidate; let base; let provenanceDigest;
try {
candidate = sha(candidateSha, 'candidateSha');
base = sha(baseTargetSha, 'baseTargetSha');
if (!Array.isArray(provenance) || !provenance.length || provenance.some((entry) => typeof entry !== 'string' || !entry)) {
throw new TorchError('provenance must be a non-empty ordered string list', { code: 'ADOPTION_INPUT_INVALID' });
}
provenanceDigest = digest(provenance);
if (sha(provenanceManifestDigest, 'provenanceManifestDigest') !== provenanceDigest) blockers.push({ code: 'ADOPTION_PROVENANCE_DIGEST_MISMATCH', expected: provenanceDigest });
} catch (error) {
blockers.push({ code: error.code ?? 'ADOPTION_INPUT_INVALID', details: error.details ?? null });
}
const target = targetBranch ?? this.policy.target;
if (typeof target !== 'string' || !target.trim()) blockers.push({ code: 'ADOPTION_TARGET_REQUIRED' });
const candidateReference = candidateRef ?? (id ? `refs/torch/integration-candidates/${id}` : null);
const expectedReference = expectedOldRef ?? (target ? `refs/heads/${target}` : null);
if (!candidateReference || !/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(candidateReference)) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_INVALID' });
if (!expectedReference || !/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(expectedReference)) blockers.push({ code: 'ADOPTION_EXPECTED_REF_INVALID' });
let contributor; let destination;
try { contributor = this.controlPlane.assertIdentity(text(contributorArea, 'contributorArea')); }
catch (error) { blockers.push({ code: 'ADOPTION_CONTRIBUTOR_INVALID', details: error.code ?? null }); }
try { destination = this.controlPlane.assertIdentity(text(destinationArea, 'destinationArea')); }
catch (error) { blockers.push({ code: 'ADOPTION_DESTINATION_INVALID', details: error.code ?? null }); }
if (contributor && destination && contributor === destination) blockers.push({ code: 'ADOPTION_IDENTITIES_MUST_DIFFER' });
const binding = { action, adoptionId: id, candidateSha: candidate, baseTargetSha: base, provenanceManifestDigest: provenanceDigest,
contributorArea: contributor, destinationArea: destination, targetBranch: target, candidateRef: candidateReference,
expectedOldRef: expectedReference, adapter: adapterBinding };
const approval = approvalId ? this.controlPlane.database.prepare(`SELECT * FROM approval_requests WHERE id = ? AND project_id = ?`)
.get(approvalId, this.controlPlane.projectId) : null;
if (!approval) blockers.push({ code: 'ADOPTION_APPROVAL_MISSING' });
else if (approval.revision !== approvalRevision) blockers.push({ code: 'ADOPTION_APPROVAL_REVISION_STALE', actual: approval.revision });
else if (!exactAuthority(authorityFrom(approval), binding)) blockers.push({ code: 'ADOPTION_APPROVAL_BINDING_INVALID' });
if (candidate && git(this.repositoryRoot, ['cat-file', '-e', `${candidate}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_MISSING' });
if (base && git(this.repositoryRoot, ['cat-file', '-e', `${base}^{commit}`], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_BASE_MISSING' });
if (candidate && base && git(this.repositoryRoot, ['merge-base', '--is-ancestor', base, candidate], { allowFailure: true }).status !== 0) blockers.push({ code: 'ADOPTION_CANDIDATE_NOT_DESCENDED_FROM_BASE' });
if (expectedReference && base) {
const current = this.refAdapter.readRefs([expectedReference])[expectedReference];
if (!current) blockers.push({ code: 'ADOPTION_EXPECTED_REF_MISSING' });
else if (current !== base) blockers.push({ code: 'ADOPTION_EXPECTED_REF_STALE', expected: base, actual: current });
}
const candidateExists = candidateReference && this.refAdapter.readRefs([candidateReference])[candidateReference] != null;
if (['plan', 'prepare', 'finalize'].includes(action) && candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_ALREADY_EXISTS' });
if (action === 'request' && !candidateExists) blockers.push({ code: 'ADOPTION_CANDIDATE_REF_MISSING' });
for (const worktree of listWorktreePaths(this.repositoryRoot)) {
const entries = git(worktree, ['status', '--porcelain=v1', '--untracked-files=all']).stdout.split('\n').filter(Boolean);
if (entries.length) blockers.push({ code: 'ADOPTION_WORKTREE_DIRTY', worktree, entries });
}
if (tableExists(this.controlPlane.database, 'worktree_guards')) {
for (const guard of this.controlPlane.database.prepare(`SELECT area_id, guard_type, reason FROM worktree_guards WHERE project_id = ? AND released_at IS NULL`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_WORKTREE_GUARD_ACTIVE', guard });
}
if (tableExists(this.controlPlane.database, 'resource_leases')) {
for (const lease of this.controlPlane.database.prepare(`SELECT id, resource_id, area_id FROM resource_leases WHERE released_at IS NULL`).all()) blockers.push({ code: 'ADOPTION_RESOURCE_LEASE_ACTIVE', lease });
}
if (tableExists(this.controlPlane.database, 'prepared_checks')) {
for (const check of this.controlPlane.database.prepare(`SELECT id, area_id, state FROM prepared_checks WHERE project_id = ? AND state IN ('prepared', 'running')`).all(this.controlPlane.projectId)) blockers.push({ code: 'ADOPTION_PREPARED_CHECK_ACTIVE', check });
}
for (const area of [contributor, destination].filter(Boolean)) {
const executor = observeRuntimeTurn({ stateRoot: this.controlPlane.stateRoot, repositoryRoot: this.repositoryRoot,
projectId: this.controlPlane.projectId, areaId: area, now: this.clock });
if (executor.state !== 'inactive') blockers.push({ code: 'ADOPTION_EXECUTOR_NOT_INACTIVE', areaId: area, executor });
}
return { action: `plan-adoption-${action}`, adoptionId: id, approval: approval ? { id: approval.id, revision: approval.revision, status: approval.status, approver: approval.approver_id } : null,
authority: binding, candidateSha: candidate ?? null, baseTargetSha: base ?? null, provenanceManifestDigest: provenanceDigest ?? null,
contributorArea: contributor ?? null, destinationArea: destination ?? null, targetBranch: target ?? null, candidateRef: candidateReference,
expectedOldRef: expectedReference, blockers, canProceed: blockers.length === 0, mutationPerformed: false };
}
#append(adoptionId, expectedRevision, record, operation) {
this.controlPlane.database.exec('BEGIN IMMEDIATE');
try {
this.#appendInTransaction(adoptionId, expectedRevision, record, operation);
this.controlPlane.database.exec('COMMIT');
} catch (error) { this.controlPlane.database.exec('ROLLBACK'); throw error; }
}
#appendInTransaction(adoptionId, expectedRevision, record, operation) {
const current = this.#find(adoptionId);
if (expectedRevision === null ? current : !current || current.revision !== expectedRevision) {
throw new TorchError('Adoption journal changed before this transition could be recorded', {
code: 'ADOPTION_RECORD_REVISION_STALE', details: { adoptionId, expectedRevision, actual: current?.revision ?? null },
});
}
this.controlPlane.database.prepare(`INSERT INTO audit_events
(id, project_id, actor_id, operation, entity_type, entity_id, details, created_at)
VALUES (?, ?, 'owner', ?, 'integration-adoption', ?, ?, ?)`)
.run(this.idFactory(), this.controlPlane.projectId, operation, adoptionId, JSON.stringify(record), this.clock().toISOString());
}
#fromRecord(record, input, action) {
exactInput(record, input);
return this.plan({ action, actorId: input.actorId, approvalId: input.approvalId, approvalRevision: input.approvalRevision,
adoptionId: record.id, candidateSha: record.candidateSha, baseTargetSha: record.baseTargetSha, provenance: record.provenance,
provenanceManifestDigest: record.provenanceManifestDigest, contributorArea: record.contributorArea, destinationArea: record.destinationArea,
targetBranch: record.targetBranch, candidateRef: record.candidateRef, expectedOldRef: record.expectedOldRef, adapter: input.adapter });
}
prepare(input = {}) {
const adoptionId = input.adoptionId ?? this.idFactory();
const plan = this.plan({ ...input, action: 'prepare', adoptionId });
if (!plan.canProceed) throw new TorchError('Adoption inputs are not eligible for preparation', { code: 'ADOPTION_PREPARE_BLOCKED', details: plan.blockers });
const now = this.clock().toISOString();
const record = { schema: ADOPTION_SCHEMA, id: adoptionId, revision: 1, state: 'prepared', approvalId: text(input.approvalId, 'approvalId'),
approvalRevision: input.approvalRevision, authority: plan.authority, provenance: [...input.provenance], candidateSha: plan.candidateSha,
baseTargetSha: plan.baseTargetSha, provenanceManifestDigest: plan.provenanceManifestDigest, candidateRef: plan.candidateRef,
expectedOldRef: plan.expectedOldRef, contributorArea: plan.contributorArea, destinationArea: plan.destinationArea, targetBranch: plan.targetBranch,
archiveRef: `refs/torch/integration-archives/${adoptionId}`, preparedAt: now, finalizedAt: null, rolledBackAt: null };
this.#append(adoptionId, null, record, 'integration.adoption.prepare');
return { ...record, mutationPerformed: true };
}
finalize(input = {}) {
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
if (record.state !== 'prepared') throw new TorchError('Only a prepared adoption can be finalized', { code: 'ADOPTION_FINALIZE_STATE_INVALID', details: { state: record.state } });
const plan = this.#fromRecord(record, input, 'finalize');
if (!plan.canProceed) throw new TorchError('Adoption finalize is blocked', { code: 'ADOPTION_FINALIZE_BLOCKED', details: plan.blockers });
const receipt = this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.expectedOldRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: null }, { ref: record.archiveRef, sha: null }],
writes: [{ ref: record.archiveRef, sha: record.baseTargetSha }, { ref: record.candidateRef, sha: record.candidateSha }] });
const finalized = { ...record, revision: record.revision + 1, state: 'finalized', finalizeApprovalId: text(input.approvalId, 'approvalId'),
finalizeApprovalRevision: input.approvalRevision, finalizedAt: this.clock().toISOString(), refReceiptDigest: digest(receipt) };
this.#append(record.id, record.revision, finalized, 'integration.adoption.finalize');
return { ...finalized, mutationPerformed: true };
}
reconcile(input = {}) {
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
const plan = this.#fromRecord(record, input, 'reconcile');
if (!plan.canProceed) return { record, plan, state: 'blocked', mutationPerformed: false };
const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]);
const finalizedRefs = refs[record.expectedOldRef] === record.baseTargetSha && refs[record.candidateRef] === record.candidateSha && refs[record.archiveRef] === record.baseTargetSha;
if (record.state === 'prepared' && finalizedRefs) {
const recovered = { ...record, revision: record.revision + 1, state: 'finalized', finalizedAt: this.clock().toISOString(),
recoveredAt: this.clock().toISOString(), recovery: 'private-ref-cas-completed-before-journal' };
this.#append(record.id, record.revision, recovered, 'integration.adoption.reconcile-finalized');
return { record: recovered, refs, state: 'recovered-finalized', mutationPerformed: true };
}
if (record.state === 'finalized' && finalizedRefs) return { record, refs, state: 'consistent', mutationPerformed: false };
if (record.state === 'prepared' && refs[record.candidateRef] === null && refs[record.archiveRef] === null) return { record, refs, state: 'prepared', mutationPerformed: false };
return { record, refs, state: 'diverged', mutationPerformed: false };
}
rollback(input = {}) {
const record = this.getPrepared(text(input.adoptionId, 'adoptionId'));
if (record.state !== 'finalized') throw new TorchError('Only a finalized adoption can be rolled back', { code: 'ADOPTION_ROLLBACK_STATE_INVALID', details: { state: record.state } });
const plan = this.#fromRecord(record, input, 'rollback');
if (!plan.canProceed) throw new TorchError('Adoption rollback is blocked', { code: 'ADOPTION_ROLLBACK_BLOCKED', details: plan.blockers });
const refs = this.refAdapter.readRefs([record.expectedOldRef, record.candidateRef, record.archiveRef]);
const recovered = refs[record.expectedOldRef] === record.baseTargetSha
&& refs[record.candidateRef] === null && refs[record.archiveRef] === record.baseTargetSha;
if (!recovered && refs[record.candidateRef] !== record.candidateSha) {
throw new TorchError('Candidate ref changed before rollback', { code: 'ADOPTION_ROLLBACK_REF_DIVERGED', details: refs });
}
const receipt = recovered ? { adapter: { name: this.refAdapter.name, id: this.refAdapter.id }, recovered: true, refs }
: this.refAdapter.compareAndSwapRefs({ expected: [{ ref: record.candidateRef, sha: record.candidateSha }], writes: [{ ref: record.candidateRef, sha: null }] });
const rolledBack = { ...record, revision: record.revision + 1, state: 'rolled-back', rollbackApprovalId: text(input.approvalId, 'approvalId'),
rollbackApprovalRevision: input.approvalRevision, rolledBackAt: this.clock().toISOString(), rollbackReceiptDigest: digest(receipt),
...(recovered ? { recoveredAt: this.clock().toISOString(), recovery: 'private-ref-rollback-completed-before-journal' } : {}) };
this.#append(record.id, record.revision, rolledBack, 'integration.adoption.rollback');
return { ...rolledBack, mutationPerformed: true };
}
requestAdoptedCandidate(input = {}, { requestIntegration, findIntegrationRequest } = {}) {
if (typeof requestIntegration !== 'function') throw new TorchError('Native integration request adapter is unavailable', { code: 'ADOPTION_NATIVE_REQUEST_UNAVAILABLE' });
const adoptionId = text(input.adoptionId, 'adoptionId');
this.controlPlane.database.exec('BEGIN IMMEDIATE');
try {
const record = this.getPrepared(adoptionId);
exactInput(record, input);
if (!['finalized', 'requesting'].includes(record.state)) {
throw new TorchError('Only a finalized adoption can request native qualification', {
code: 'ADOPTION_REQUEST_STATE_INVALID', details: { state: record.state },
});
}
const plan = this.#fromRecord(record, input, 'request');
if (!plan.canProceed) throw new TorchError('Adopted candidate request is blocked', { code: 'ADOPTION_REQUEST_BLOCKED', details: plan.blockers });
const requesting = record.state === 'requesting' ? record : { ...record, revision: record.revision + 1, state: 'requesting',
requestApprovalId: text(input.approvalId, 'approvalId'), requestApprovalRevision: input.approvalRevision, requestStartedAt: this.clock().toISOString() };
if (record.state !== 'requesting') this.#appendInTransaction(record.id, record.revision, requesting, 'integration.adoption.request-started');
const existing = findIntegrationRequest?.({ candidateRef: requesting.candidateRef, commit: requesting.candidateSha });
if (existing && (existing.sourceArea !== requesting.contributorArea || existing.targetBranch !== requesting.targetBranch)) {
throw new TorchError('Existing private-ref integration request does not match this adoption', {
code: 'ADOPTION_INTEGRATION_REQUEST_CONFLICT', details: { adoptionId: requesting.id, integrationRequestId: existing.id },
});
}
const request = existing ?? requestIntegration({ adoptionId: requesting.id, areaId: requesting.contributorArea,
commit: requesting.candidateSha, candidateRef: requesting.candidateRef });
const requested = { ...requesting, revision: requesting.revision + 1, state: 'requested', integrationRequestId: request.id, requestedAt: this.clock().toISOString() };
this.#appendInTransaction(requesting.id, requesting.revision, requested, 'integration.adoption.request');
this.controlPlane.database.exec('COMMIT');
return { ...requested, integrationRequest: request, mutationPerformed: true };
} catch (error) {
this.controlPlane.database.exec('ROLLBACK');
throw error;
}
}
#find(adoptionId) {
const row = this.controlPlane.database.prepare(`SELECT details FROM audit_events WHERE project_id = ?
AND entity_type = 'integration-adoption' AND entity_id = ? ORDER BY rowid DESC LIMIT 1`).get(this.controlPlane.projectId, adoptionId);
return durableRecord(row);
}
getPrepared(adoptionId) {
const id = text(adoptionId, 'adoptionId');
const record = this.#find(id);
if (!record) throw new TorchError('Unknown adoption journal record', { code: 'ADOPTION_NOT_FOUND', details: { adoptionId: id } });
return record;
}
}
+101 -1
View File
@@ -4,6 +4,7 @@ import { TorchError } from '../kernel/errors.mjs';
import { readInstallManifest } from '../kernel/install.mjs';
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
import { BacklogService } from '../backlog/service.mjs';
import { AdoptionService } from './adoption.mjs';
function git(root, args, { allowFailure = false } = {}) {
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
@@ -15,6 +16,21 @@ function git(root, args, { allowFailure = false } = {}) {
return { status: result.status, stdout: result.stdout?.trim() ?? '', stderr: result.stderr?.trim() ?? '' };
}
function text(value, name) {
if (typeof value !== 'string' || !value.trim()) {
throw new TorchError(`${name} is required`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } });
}
return value.trim();
}
function sha(value, name) {
const normalized = text(value, name);
if (!/^[0-9a-f]{40,64}$/.test(normalized)) {
throw new TorchError(`${name} must be a full Git object id`, { code: 'INTEGRATION_INPUT_INVALID', details: { name } });
}
return normalized;
}
function integrationRow(row) {
if (!row) return null;
return {
@@ -42,6 +58,8 @@ export class IntegrationService {
this.worktrees = new Map((manifest.external ?? [])
.filter((entry) => entry.type === 'worktree')
.map((entry) => [entry.area, entry]));
this.adoption = new AdoptionService({ repositoryRoot, controlPlane, clock, idFactory,
policy: { target: this.policy.target } });
this.controlPlane.database.exec(`
CREATE TABLE IF NOT EXISTS integration_requests (
id TEXT PRIMARY KEY,
@@ -84,12 +102,56 @@ export class IntegrationService {
code: 'INTEGRATION_SOURCE_MOVED', details: { branchTip, sourceCommit },
});
}
return this.#registerRequest({ area, sourceBranch: worktree.branch, sourceCommit });
}
requestCandidate({ areaId, commit, candidateRef } = {}) {
throw new TorchError('Private candidate intake requires an authenticated finalized adoption', {
code: 'INTEGRATION_ADOPTED_INTAKE_INTERNAL_ONLY', details: { areaId, commit, candidateRef },
});
}
#requestFinalizedCandidate({ adoptionId, areaId, commit, candidateRef } = {}) {
const area = this.controlPlane.assertIdentity(areaId);
if (area === 'session-manager') {
throw new TorchError('Session Manager does not own a feature worktree by default', {
code: 'INTEGRATION_SOURCE_INVALID',
});
}
const adoption = this.adoption.getPrepared(text(adoptionId, 'adoptionId'));
if (adoption.state !== 'requesting' || adoption.contributorArea !== area
|| adoption.candidateSha !== commit || adoption.candidateRef !== candidateRef) {
throw new TorchError('Private candidate does not match an authenticated finalized adoption', {
code: 'INTEGRATION_ADOPTED_BINDING_INVALID', details: { adoptionId, state: adoption.state },
});
}
const ref = text(candidateRef, 'candidateRef');
if (!/^refs\/torch\/integration-candidates\/[A-Za-z0-9._-]+$/.test(ref)) {
throw new TorchError('Adopted candidates must use a private integration candidate ref', {
code: 'INTEGRATION_ADOPTED_REF_INVALID', details: { candidateRef: ref },
});
}
const sourceCommit = sha(commit, 'commit');
const refTip = git(this.repositoryRoot, ['rev-parse', '--verify', ref], { allowFailure: true });
if (refTip.status !== 0 || refTip.stdout !== sourceCommit) {
throw new TorchError('Adopted candidate ref no longer names the exact requested commit', {
code: 'INTEGRATION_ADOPTED_REF_STALE', details: { candidateRef: ref, sourceCommit, actual: refTip.stdout || null },
});
}
const existing = this.controlPlane.database.prepare(`SELECT id FROM integration_requests
WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`)
.get(this.controlPlane.projectId, ref, sourceCommit);
if (existing) return this.get(existing.id);
return this.#registerRequest({ area, sourceBranch: ref, sourceCommit });
}
#registerRequest({ area, sourceBranch, sourceCommit }) {
const targetBranch = this.policy.target;
const baseTargetCommit = git(this.repositoryRoot, ['rev-parse', targetBranch]).stdout;
const now = this.clock().toISOString();
const record = {
id: this.idFactory(), projectId: this.controlPlane.projectId, sourceArea: area,
sourceBranch: worktree.branch, sourceCommit, targetBranch, baseTargetCommit,
sourceBranch, sourceCommit, targetBranch, baseTargetCommit,
state: 'testing', reason: null, requiredChecks: this.policy.required_checks ?? [],
authorizedBy: null, authorizedAt: null, createdAt: now, updatedAt: now, landedAt: null,
};
@@ -110,6 +172,44 @@ export class IntegrationService {
return this.evaluate(record.id);
}
planAdoption(input = {}) {
return this.adoption.plan(input);
}
prepareAdoption(input = {}) {
return this.adoption.prepare(input);
}
getPreparedAdoption(adoptionId) {
return this.adoption.getPrepared(adoptionId);
}
finalizeAdoption(input = {}) {
return this.adoption.finalize(input);
}
reconcileAdoption(input = {}) {
return this.adoption.reconcile(input);
}
rollbackAdoption(input = {}) {
return this.adoption.rollback(input);
}
requestAdoptedCandidate(input = {}) {
return this.adoption.requestAdoptedCandidate(input, {
requestIntegration: ({ adoptionId, areaId, commit, candidateRef }) => this.#requestFinalizedCandidate({ adoptionId, areaId, commit, candidateRef }),
findIntegrationRequest: ({ candidateRef, commit }) => this.findCandidateRequest({ candidateRef, commit }),
});
}
findCandidateRequest({ candidateRef, commit } = {}) {
const row = this.controlPlane.database.prepare(`SELECT id FROM integration_requests
WHERE project_id = ? AND source_branch = ? AND source_commit = ? ORDER BY rowid LIMIT 1`)
.get(this.controlPlane.projectId, candidateRef, commit);
return row ? this.get(row.id) : null;
}
get(requestId) {
const row = this.controlPlane.database.prepare('SELECT * FROM integration_requests WHERE id = ?').get(requestId);
if (!row) throw new TorchError(`Unknown integration request: ${requestId}`, { code: 'INTEGRATION_NOT_FOUND' });
+26 -3
View File
@@ -19,6 +19,7 @@ export const TORCH_CORE_MCP_TOOL_NAMES = Object.freeze([
'torch_who_owns',
'torch_neighbours',
'torch_send_message',
'torch_reply_to_owner',
'torch_read_messages',
'torch_ack_message',
'torch_report_status',
@@ -100,6 +101,19 @@ function claimedIdentity(controlPlane, actorId, claim, field) {
});
}
function boundActorIdentity(controlPlane, actorId) {
if (typeof actorId !== 'string' || actorId.trim().length === 0) {
throw new TorchError('torch_reply_to_owner requires an actual bound Fleet identity', {
code: 'FLEET_IDENTITY_REQUIRED', details: { field: 'bound_actor' },
});
}
return controlPlane.assertIdentity(actorId);
}
function inputSchema(tool) {
return typeof tool.schema?.safeParse === 'function' ? tool.schema : z.object(tool.schema);
}
export function createTorchToolset(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
evolutionService, hierarchyService, repositoryRoot,
@@ -169,6 +183,15 @@ export function createTorchToolset(controlPlane, {
sender: claimedIdentity(controlPlane, actorId, sender, 'sender'), recipient, body, references,
}),
}],
['torch_reply_to_owner', {
description: 'Reply to one owner-originated request as the bound Fleet identity. This tool cannot select a sender, recipient, owner, or conversation history.',
schema: z.object({
request_id: z.string().min(1), body: z.string().min(1),
}).strict(),
invoke: ({ request_id: requestId, body }) => controlPlane.sendOwnerReply({
sender: boundActorIdentity(controlPlane, actorId), requestId, body,
}),
}],
['torch_read_messages', {
description: 'Read durable direct and group messages visible to one Fleet identity.',
schema: {
@@ -736,14 +759,14 @@ export function describeTorchTools() {
}).entries()].map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: z.toJSONSchema(z.object(tool.schema)),
inputSchema: z.toJSONSchema(inputSchema(tool)),
}));
}
export function callTorchTool(controlPlane, name, input = {}, options = {}) {
const tool = createTorchToolset(controlPlane, options).get(name);
if (!tool) throw new Error(`Unknown TORCH MCP tool: ${name}`);
const parsed = z.object(tool.schema).parse(input);
const parsed = inputSchema(tool).parse(input);
return tool.invoke(parsed);
}
@@ -762,7 +785,7 @@ export function createTorchMcpServer(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
evolutionService, hierarchyService, repositoryRoot, artifactService,
})) {
server.registerTool(name, { description: tool.description, inputSchema: tool.schema }, async (input) => {
server.registerTool(name, { description: tool.description, inputSchema: inputSchema(tool) }, async (input) => {
try {
const result = await tool.invoke(input);
return {
+14 -1
View File
@@ -16,6 +16,8 @@ import { assessBacklogHealth, backlogObservedCommitDistance } from '../backlog/s
import { observeTaskActivity } from '../backlog/activity.mjs';
import { ScheduleLauncherService } from '../schedules/launcher.mjs';
import { hierarchyOrder } from '../runtime/hierarchy-order.mjs';
import { observeRuntimeTurn } from '../runtime/turn-guard.mjs';
import { observeContinuation } from '../runtime/continuation.mjs';
function json(path) {
return JSON.parse(readFileSync(path, 'utf8'));
@@ -403,6 +405,7 @@ export function observeProject({ repositoryRoot, env = process.env, now = () =>
metrics: ['cachedInput', 'uncachedInput', 'cacheCreation', 'cacheRead', 'compactions', 'resumedPromptBytes', 'costPerVerifiedItem'],
},
};
result.continuation = observeContinuation({ stateRoot, tasks: result.backlog, now });
result.backlogHealth = backlogHealth(repository.root, result.backlog, result.agents, now);
result.backlogActivity = observeBacklogActivity({ repositoryRoot: repository.root,
tasks: result.backlog, config, manifest, now: now() });
@@ -417,11 +420,21 @@ export function observeProject({ repositoryRoot, env = process.env, now = () =>
`).all().map((row) => [row.area_id, row]));
result.agents = result.agents.map((agent) => {
const row = live.get(agent.areaId);
return row ? {
const reported = row ? {
...agent, state: row.state, runtime: row.runtime ?? agent.runtime,
runtimeSessionId: row.runtime_session_id, summary: row.summary,
task: row.task, heartbeatAt: row.heartbeat_at,
} : agent;
const executor = observeRuntimeTurn({ stateRoot, repositoryRoot: repository.root,
projectId: manifest.projectId, areaId: agent.areaId, now });
const staleIdle = ['idle', 'offline'].includes(reported.state);
return { ...reported, reportedState: reported.state, reportedSummary: reported.summary, executor,
...(executor.state === 'active' && staleIdle ? {
state: executor.phase,
summary: `Live runtime executor is ${executor.phase} (last reported state: ${reported.state}).`,
} : executor.state === 'unknown' && staleIdle ? {
state: 'unknown', summary: 'Runtime turn guard exists, but its executor could not be verified.',
} : {}) };
});
}
if (hasTable(database, 'schedule_wake_reservations')) {
+178
View File
@@ -0,0 +1,178 @@
import { createHash, randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TorchError } from '../kernel/errors.mjs';
function statePath(control) { return join(control.stateRoot, 'continuation.json'); }
function occupiedAreas(control) {
const path = join(control.stateRoot, 'sessions', 'turn-guards');
return existsSync(path) ? readdirSync(path, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name) : [];
}
function taskKey(tasks) { return JSON.stringify(tasks.map(t => [t.id, t.revision]).sort((a, b) => a[0].localeCompare(b[0]))); }
function dailyCap(state, day) {
return state.dailyOverride?.day === day ? Math.max(state.maxTurnsPerDay, state.dailyOverride.maxTurns) : state.maxTurnsPerDay;
}
function read(control) {
const policyPath = join(control.stateRoot, 'continuation-policy.json');
const state = { enabled: false, maxTurnsPerDay: 12, maxConcurrency: 3, areas: [], day: null, attempts: 0, handled: {}, progress: {},
...(existsSync(statePath(control)) ? JSON.parse(readFileSync(statePath(control), 'utf8')) : {}),
...(existsSync(policyPath) ? JSON.parse(readFileSync(policyPath, 'utf8')) : {}) };
if (typeof state.enabled !== 'boolean' || !Number.isInteger(state.maxTurnsPerDay)
|| state.maxTurnsPerDay < 1 || state.maxTurnsPerDay > 48 || !Array.isArray(state.areas)
|| !Number.isInteger(state.maxConcurrency) || state.maxConcurrency < 1 || state.maxConcurrency > 7
|| !Number.isInteger(state.attempts) || state.attempts < 0 || !state.handled || typeof state.handled !== 'object'
|| !state.progress || typeof state.progress !== 'object'
|| (state.dailyOverride != null && (!/^\d{4}-\d{2}-\d{2}$/.test(state.dailyOverride.day)
|| !Number.isInteger(state.dailyOverride.maxTurns) || state.dailyOverride.maxTurns < 1 || state.dailyOverride.maxTurns > 96))) {
throw new TorchError('Continuation policy or attempt ledger is invalid; launch refused.', { code: 'CONTINUATION_POLICY_INVALID' });
}
return state;
}
function save(control, state, policy = false) {
const path = policy ? join(control.stateRoot, 'continuation-policy.json') : statePath(control);
const temporary = `${path}.${randomUUID()}.tmp`;
const value = policy ? { enabled: state.enabled, maxTurnsPerDay: state.maxTurnsPerDay, maxConcurrency: state.maxConcurrency, areas: state.areas, dailyOverride: state.dailyOverride ?? null }
: { day: state.day, attempts: state.attempts, handled: state.handled, progress: state.progress };
writeFileSync(temporary, JSON.stringify(value), { flag: 'wx', mode: 0o600 });
renameSync(temporary, path);
}
export function configureContinuation(control, { actorId, enabled, maxTurnsPerDay, maxConcurrency, todayMaxTurns } = {}) {
control.assertOwnerActor(actorId);
const previous = read(control);
maxTurnsPerDay ??= previous.maxTurnsPerDay;
maxConcurrency ??= previous.maxConcurrency;
if (typeof enabled !== 'boolean' || !Number.isInteger(maxTurnsPerDay) || maxTurnsPerDay < 1 || maxTurnsPerDay > 48
|| !Number.isInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 7
|| (todayMaxTurns !== undefined && (!Number.isInteger(todayMaxTurns) || todayMaxTurns < maxTurnsPerDay || todayMaxTurns > 96))) {
throw new TorchError('Continuation requires explicit enable/pause, 1–48 daily turns, 1–7 concurrent turns and an optional same-day total between the daily cap and 96.', { code: 'CONTINUATION_POLICY_INVALID' });
}
const state = { ...previous, enabled, maxTurnsPerDay, maxConcurrency, areas: control.listAgents().map(a => a.areaId) };
if (todayMaxTurns !== undefined) state.dailyOverride = { day: control.clock().toISOString().slice(0, 10), maxTurns: todayMaxTurns };
save(control, state, true);
control.auditOwnerAction({ actorId, operation: 'runtime.continuation.configure', entityType: 'project', entityId: control.projectId,
details: { enabled, maxTurnsPerDay, maxConcurrency, dailyOverride: state.dailyOverride ?? null, areas: state.areas } });
return { enabled, maxTurnsPerDay, maxConcurrency, dailyOverride: state.dailyOverride ?? null, areas: state.areas, sessionsStarted: false };
}
export function observeContinuation({ stateRoot, tasks = [], now = () => new Date() } = {}) {
try {
const state = read({ stateRoot });
const day = now().toISOString().slice(0, 10);
const attempts = state.day === day ? state.attempts : 0;
const cap = dailyCap(state, day);
const remainingTurns = Math.max(0, cap - attempts);
const held = Object.entries(state.progress).filter(([area, progress]) => {
const active = tasks.filter(t => t.owner === area && ['assigned', 'in_progress'].includes(t.state));
return active.length && progress?.status === 0 && progress.noProgress >= 2 && progress.taskKey === taskKey(active);
}).map(([areaId]) => ({ areaId, reason: 'no-progress-needs-coordination' }));
return { available: existsSync(join(stateRoot, 'continuation-policy.json')), enabled: state.enabled,
day, dayTimezone: 'UTC', attempts, maxTurnsPerDay: cap, configuredMaxTurnsPerDay: state.maxTurnsPerDay,
dailyOverride: state.dailyOverride ?? null, maxConcurrency: state.maxConcurrency, remainingTurns,
stopReason: !state.enabled ? 'paused' : remainingTurns === 0 ? 'daily-turn-cap' : null,
capacityHeldBy: occupiedAreas({ stateRoot }), held,
budgetKind: 'automatic-turn-cap', manualTurnsIncluded: false, tokenOrCostGovernance: 'unknown',
timerInstallationVerified: false, mutationPerformed: false };
} catch {
return { available: false, stopReason: 'policy-or-ledger-unreadable', mutationPerformed: false };
}
}
export function planContinuation(control, backlog, { at = new Date(), limit } = {}) {
const state = read(control);
const day = at.toISOString().slice(0, 10);
const attempts = state.day === day ? state.attempts : 0;
const cap = dailyCap(state, day);
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 7)) throw new TorchError('Continuation limit must be 1–7.', { code: 'CONTINUATION_POLICY_INVALID' });
const capacity = Math.min(limit ?? state.maxConcurrency, state.maxConcurrency);
const metadata = JSON.parse(readFileSync(join(control.stateRoot, 'project.json'), 'utf8'));
if (!state.enabled || metadata.detachedAt || attempts >= cap) {
return { enabled: state.enabled, maxConcurrency: capacity, reason: metadata.detachedAt ? 'detached' : !state.enabled ? 'paused' : 'daily-turn-cap', candidates: [], attempts };
}
const tasks = backlog.list();
const candidates = [];
const held = [];
for (const areaId of state.areas) {
const agent = control.getAgent(areaId);
if (!['idle', 'offline', 'waiting'].includes(agent.state)
|| existsSync(join(control.stateRoot, 'sessions', 'turn-guards', areaId))) continue;
const messages = control.readMessages({ recipient: areaId, unacknowledgedOnly: true, limit: 1000 });
const active = tasks.filter(t => t.owner === areaId && ['assigned', 'in_progress'].includes(t.state)
&& (t.dependencies ?? []).every(id => tasks.find(t => t.id === id)?.state === 'completed'));
const previous = state.handled[areaId] ?? {};
const freshMessages = messages.filter(m => !previous.messageIds?.includes(m.id));
const freshTasks = active.filter(t => previous.taskRevisions?.[t.id] !== t.revision);
const progress = state.progress[areaId];
const continueWork = active.length > 0 && progress?.status === 0
&& progress.taskKey === taskKey(active) && progress.noProgress < 2;
if (!freshMessages.length && !freshTasks.length && !continueWork) {
if (active.length && progress?.noProgress >= 2) held.push({ areaId, reason: 'no-progress-needs-coordination', taskIds: active.map(t => t.id) });
continue;
}
const signal = { messageIds: messages.map(m => m.id), taskRevisions: Object.fromEntries(active.map(t => [t.id, t.revision])) };
const fingerprint = createHash('sha256').update(JSON.stringify(signal)).digest('hex');
candidates.push({ areaId, fingerprint, signal, workKey: taskKey(active),
messageCount: freshMessages.length, taskIds: active.map(t => t.id), continueWork });
}
const ordinal = area => state.handled[area]?.dispatchedDay === day ? state.handled[area].dispatchedOrdinal ?? 0 : 0;
candidates.sort((a, b) => ordinal(a.areaId) - ordinal(b.areaId)
|| Number(b.areaId === 'session-manager') - Number(a.areaId === 'session-manager'));
return { enabled: true, maxConcurrency: capacity, day, attempts, remainingTurns: cap - attempts,
occupiedAreas: occupiedAreas(control), held,
candidates: candidates.slice(0, Math.min(capacity, cap - attempts)), mutationPerformed: false };
}
export async function runContinuation(control, backlog, { actorId, launch, at, clock = () => at ?? new Date(), limit } = {}) {
control.assertOwnerActor(actorId);
if (typeof launch !== 'function') throw new TorchError('Continuation needs an authorized launcher.', { code: 'RUNTIME_EXECUTION_NOT_AUTHORIZED' });
const lock = join(control.stateRoot, 'continuation.lock');
try { mkdirSync(lock); } catch (error) {
if (error.code === 'EEXIST') return { reason: 'controller-active-or-unreconciled', launched: [] };
throw error;
}
try {
const pending = new Map();
const launched = [];
let reason = null;
while (true) {
// Refill immediately after EACH completion. Pending promises and physical
// guards both count, including manual executors outside this controller.
let current = planContinuation(control, backlog, { at: clock(), limit });
let occupied = new Set([...occupiedAreas(control), ...pending.keys()]);
while (occupied.size < current.maxConcurrency && current.enabled && !current.reason) {
const candidate = current.candidates.find(c => !occupied.has(c.areaId));
if (!candidate) break;
const state = read(control);
if (state.day !== current.day) { state.day = current.day; state.attempts = 0; }
state.attempts++;
state.handled[candidate.areaId] = { ...candidate.signal,
dispatchedDay: current.day, dispatchedOrdinal: state.attempts };
save(control, state); // Reserve before launch; unknown attempts are never refunded.
let result;
try { result = launch(candidate.areaId); } catch { result = null; }
pending.set(candidate.areaId, Promise.resolve(result).catch(() => null)
.then(status => ({ areaId: candidate.areaId, status, candidate })));
current = planContinuation(control, backlog, { at: clock(), limit });
occupied = new Set([...occupiedAreas(control), ...pending.keys()]);
}
reason = current.reason ?? (occupied.size >= current.maxConcurrency ? 'capacity-full' : null);
if (!pending.size) break;
const completed = await Promise.race(pending.values());
pending.delete(completed.areaId);
launched.push({ areaId: completed.areaId, status: completed.status });
const state = read(control);
const active = backlog.list().filter(t => t.owner === completed.areaId && ['assigned', 'in_progress'].includes(t.state));
const key = taskKey(active);
const unchanged = active.length > 0 && key === completed.candidate.workKey;
const prior = state.progress[completed.areaId];
const noProgress = unchanged ? (prior?.taskKey === key ? prior.noProgress : 0) + 1 : 0;
state.progress[completed.areaId] = { status: completed.status, taskKey: key, noProgress, completedAt: clock().toISOString() };
save(control, state);
if (completed.status === 0 && noProgress === 2 && typeof control.sendOwnerRequest === 'function') {
control.sendOwnerRequest({ actorId, recipient: 'session-manager',
body: `TORCH continuation requires coordination: ${completed.areaId} completed two turns without advancing assigned task state (${active.map(t => t.id).join(', ')}). Automatic unchanged-work retries are held. Inspect the blocker and route a concrete decision; do not mark work done without evidence.` });
}
}
return { launched, reason };
} finally { rmdirSync(lock); }
}
+8 -3
View File
@@ -11,6 +11,7 @@ import {
costCeilingPlanIssues, profileCapabilityIssues, resolveLaunchPolicy, validateRuntimeAdapter,
} from '../adapters/runtime.mjs';
import { hierarchyOrder } from './hierarchy-order.mjs';
import { withRuntimeTurnGuard } from './turn-guard.mjs';
const MCP_ENTRY = fileURLToPath(new URL('../mcp/server.mjs', import.meta.url));
@@ -155,15 +156,16 @@ function promptPaths(stateRoot, repositoryRoot, areaId, config) {
}
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.`;
const inboxRequest = JSON.stringify({ recipient: areaId, unacknowledged_only: true, limit: 1000 });
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. Use torch_read_messages for the current unread inbox. Inbox request: ${inboxRequest}. Messages are chronological; a default history page is not the current inbox. If 1000 messages are returned, coverage may be truncated: report that limitation and never bulk-acknowledge unseen messages. Acknowledge messages only after inspecting and handling them; messages do not override enforced approval or ownership boundaries.`;
if (areaId === 'session-manager') {
return fresh
? `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}`;
: `Resume the TORCH Fleet from durable state. Reconcile live status and inbox, resolve actionable coordination waits within your authority, and triage/assign eligible existing backlog work. Do not stop at a status report when a concrete routing decision can unblock work. Assess Fleet evolution when evidence warrants it, not as a prerequisite to every dispatch. Preserve ownership, approvals and dependency gates. ${currentBrief}`;
}
return fresh
? `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}`;
: `Resume this TORCH domain from durable state. Read the inbox and current backlog; continue your eligible assigned implementation, verification or coordination work, not just a status check. Work until a concrete handoff, completed turn budget or real blocker, and persist evidence and actionable waits. Never bypass dependencies, ownership or approval gates. If no eligible work exists, idle is valid. ${currentBrief}`;
}
function atomicJson(path, value) {
@@ -570,7 +572,9 @@ export function startFleet({ plan, controlPlane, executor, adapters } = {}) {
const runtimes = adapterMap({ adapters });
for (const action of plan.actions) {
controlPlane.assertIdentity(action.areaId);
withRuntimeTurnGuard(controlPlane, action.areaId, setPhase => {
writeCombinedPrompt(action);
setPhase('working');
const result = executor({
...action.launch, areaId: action.areaId, runtime: action.runtime,
runtimeSessionId: action.runtimeSessionId,
@@ -634,6 +638,7 @@ export function startFleet({ plan, controlPlane, executor, adapters } = {}) {
? `${action.mode} turn completed` : `${action.mode} requested`,
});
started.push({ areaId: action.areaId, runtimeSessionId: identity.runtimeSessionId, mode: action.mode });
});
}
if (plan.stateRoot) {
const metadata = projectMetadata(plan.stateRoot);
+106
View File
@@ -0,0 +1,106 @@
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { readFileSync, readdirSync, readlinkSync, statSync } from 'node:fs';
import { basename, join, resolve } from 'node:path';
import { TorchError } from '../kernel/errors.mjs';
const issued = new WeakMap();
const TTL_MS = 60_000;
function refuse(message) {
throw new TorchError(message, { code: 'RUNTIME_RECOVERY_UNVERIFIED' });
}
export function readStoppedUnit(unit) {
const result = spawnSync('systemctl', ['--user', 'show', unit,
'--property=Id,ActiveState,SubState,MainPID,ControlPID,InvocationID,ExecStart,WorkingDirectory,ControlGroup,ExecMainExitTimestamp'],
{ encoding: 'utf8', timeout: 5000, maxBuffer: 64 * 1024 });
if (result.error || result.signal || result.status !== 0) refuse('Cannot verify the owner-selected service invocation.');
return Object.fromEntries(result.stdout.trim().split('\n').map(line => {
const index = line.indexOf('=');
return [line.slice(0, index), line.slice(index + 1)];
}));
}
export function readNativeProcesses() {
if (process.platform !== 'linux') refuse('Legacy process inspection is supported only on Linux.');
const processes = [];
for (const name of readdirSync('/proc').filter(value => /^\d+$/.test(value))) {
const directory = join('/proc', name);
try {
if (statSync(directory).uid !== process.getuid()) continue;
const argv = readFileSync(join(directory, 'cmdline'), 'utf8').split('\0').filter(Boolean);
if (!argv.length) continue;
const native = argv.some(value => /^(?:codex|claude|pi)(?:\.js)?$/.test(basename(value)));
if (native) processes.push({ pid: Number(name), argv, cwd: readlinkSync(join(directory, 'cwd')) });
} catch (error) {
if (error.code !== 'ENOENT' && error.code !== 'ESRCH') refuse('Native process visibility is incomplete; recovery refused.');
}
}
return processes;
}
function verify(record) {
const unit = record.readUnit(record.unit);
const prefix = `torch-${record.projectId}-`;
if (typeof record.unit !== 'string' || !record.unit.startsWith(prefix) || !/^[A-Za-z0-9_.-]+\.service$/.test(record.unit)
|| unit.Id !== record.unit || !/^[a-f0-9]{32}$/.test(unit.InvocationID ?? '')
|| unit.InvocationID !== record.invocationId
|| !['failed', 'inactive'].includes(unit.ActiveState)
|| !['failed', 'dead'].includes(unit.SubState)
|| unit.MainPID !== '0' || unit.ControlPID !== '0' || unit.ControlGroup !== ''
|| resolve(unit.WorkingDirectory || '/') !== record.repositoryRoot) {
refuse('The selected project service is not a verified stopped invocation.');
}
// Explicit legacy operator binding, not a claim of cryptographic session provenance.
// Exact area token and terminal timestamp must agree with the retained unknown row.
if (!(unit.ExecStart ?? '').split(/[\s;]+/).includes(record.identity.areaId)
|| !Number.isFinite(Date.parse(unit.ExecMainExitTimestamp))
|| Math.abs(Date.parse(unit.ExecMainExitTimestamp) - Date.parse(record.identity.updatedAt)) > 5000) {
refuse('Service area or terminal timestamp does not match the retained unknown identity.');
}
for (const processRecord of record.readProcesses()) {
if (processRecord.argv.includes(record.identity.runtimeSessionId)
|| processRecord.argv.includes(record.worktree)
|| resolve(processRecord.cwd) === record.worktree) {
refuse('A native process still references the selected session or workspace.');
}
}
return { unit: record.unit, invocationId: unit.InvocationID,
terminalAt: new Date(Date.parse(unit.ExecMainExitTimestamp)).toISOString(),
outcome: 'unknown', verification: 'owner-selected-legacy-invocation' };
}
export function issueStoppedExecutorEvidence({ controlPlane, actorId, areaId, unit, invocationId,
readUnit = readStoppedUnit, readProcesses = readNativeProcesses, now = () => Date.now() } = {}) {
controlPlane.assertOwnerActor(actorId);
const identity = controlPlane.getAgent(areaId);
if (identity.state !== 'working' || !identity.runtimeSessionId || !identity.updatedAt
|| !identity.summary?.includes('Runtime executor completion is unknown')) {
refuse('Only an exact retained working/unknown identity can use legacy recovery.');
}
const worktree = controlPlane.manifest.external?.find(entry => entry.type === 'worktree' && entry.area === areaId)?.path;
if (!worktree) refuse('No registered workspace exists for this identity.');
const record = { identity: Object.freeze({ areaId, state: identity.state, runtime: identity.runtime,
runtimeSessionId: identity.runtimeSessionId, updatedAt: identity.updatedAt }),
unit, invocationId, projectId: controlPlane.projectId, repositoryRoot: resolve(controlPlane.repositoryRoot),
worktree: resolve(worktree), readUnit, readProcesses, now, issuedAt: now() };
const facts = verify(record);
const evidence = Object.freeze({});
issued.set(evidence, record);
return { evidence, plan: { areaId, expectedIdentity: record.identity, ...facts,
mutationPerformed: false, effect: 'Recover presence to offline; preserve task, session and unknown outcome. No provider launch.' } };
}
export function consumeStoppedExecutorEvidence(evidence, { projectId, repositoryRoot, identity } = {}) {
const record = issued.get(evidence);
if (!record) refuse('Recovery requires fresh Runtime-issued evidence, not caller text or a Boolean.');
issued.delete(evidence);
if (record.projectId !== projectId || record.repositoryRoot !== resolve(repositoryRoot)
|| record.now() < record.issuedAt
|| record.now() - record.issuedAt > TTL_MS
|| ['areaId', 'state', 'runtime', 'runtimeSessionId', 'updatedAt'].some(key => record.identity[key] !== identity[key])) {
refuse('Recovery evidence is stale, expired or belongs to another identity.');
}
const facts = verify(record); // Recheck physical state immediately before the transactional mutation.
return { ...facts, digest: createHash('sha256').update(JSON.stringify({ identity: record.identity, ...facts })).digest('hex') };
}
+82
View File
@@ -0,0 +1,82 @@
import { randomUUID } from 'node:crypto';
import { mkdirSync, readFileSync, readlinkSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import { basename, join, resolve } from 'node:path';
import { TorchError } from '../kernel/errors.mjs';
function nativeProcess(pid) {
if (process.platform !== 'linux') throw new Error('Process observation unavailable');
const directory = `/proc/${pid}`;
const stat = readFileSync(join(directory, 'stat'), 'utf8');
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
return { uid: statSync(directory).uid, state: fields[0], startTicks: fields[19],
cwd: readlinkSync(join(directory, 'cwd')),
argv: readFileSync(join(directory, 'cmdline'), 'utf8').split('\0').filter(Boolean) };
}
// Observational only: never changes assignments, heartbeat, authority or launch eligibility.
export function observeRuntimeTurn({ stateRoot, repositoryRoot, projectId, areaId,
inspectProcess = nativeProcess, uid = process.getuid?.(), now = () => new Date() } = {}) {
if (!/^[a-z0-9][a-z0-9-]*$/.test(areaId)) return { state: 'unknown', reason: 'invalid-identity' };
let record;
try { record = JSON.parse(readFileSync(join(stateRoot, 'sessions', 'turn-guards', areaId, 'owner.json'), 'utf8')); }
catch (error) { return { state: error.code === 'ENOENT' ? 'inactive' : 'unknown', reason: 'guard-unavailable' }; }
if (record.projectId !== projectId || record.areaId !== areaId || !Number.isSafeInteger(record.pid)
|| record.pid < 1 || typeof record.nonce !== 'string' || !record.nonce) {
return { state: 'unknown', reason: 'guard-invalid' };
}
try {
const live = inspectProcess(record.pid);
if (live.uid !== uid || ['Z', 'X'].includes(live.state)) return { state: 'unknown', reason: 'executor-not-live' };
if (record.startTicks) {
if (record.startTicks !== live.startTicks) return { state: 'unknown', reason: 'executor-replaced' };
} else {
// Existing pre-observation guards: require an exact identity-bound TORCH up process.
const only = live.argv.indexOf('--only');
if (resolve(live.cwd) !== resolve(repositoryRoot) || !live.argv.includes('up')
|| only < 0 || live.argv[only + 1] !== areaId
|| !live.argv.some(arg => ['torch.mjs', 'torch'].includes(basename(arg)))) {
return { state: 'unknown', reason: 'legacy-executor-unverified' };
}
}
return { state: 'active', phase: record.phase === 'starting' ? 'starting' : 'working',
observedAt: now().toISOString(), source: 'live-process-and-turn-guard' };
} catch { return { state: 'unknown', reason: 'executor-unobservable' }; }
}
// One physical executor per identity, including the interval after an agent
// reports idle but before its harness actually exits. Crashed guards never expire.
export function withRuntimeTurnGuard(controlPlane, areaId, run) {
controlPlane.assertIdentity(areaId);
if (!/^[a-z0-9][a-z0-9-]*$/.test(areaId)) throw new TorchError('Invalid runtime guard identity.', { code: 'INVALID_RUNTIME_INPUT' });
const parent = join(controlPlane.stateRoot, 'sessions', 'turn-guards');
mkdirSync(parent, { recursive: true });
const directory = join(parent, areaId);
try { mkdirSync(directory); } catch (error) {
if (error.code === 'EEXIST') throw new TorchError('An executor already holds this identity turn guard.', {
code: 'RUNTIME_TURN_ACTIVE', details: { areaId },
});
throw error;
}
const path = join(directory, 'owner.json');
const nonce = randomUUID();
let startTicks;
try { startTicks = nativeProcess(process.pid).startTicks; } catch { /* Unavailable hosts remain observationally unknown. */ }
const record = { nonce, pid: process.pid, areaId, projectId: controlPlane.projectId, startTicks, phase: 'starting' };
writeFileSync(path, JSON.stringify(record), { flag: 'wx', mode: 0o600 });
const setPhase = phase => {
if (!['starting', 'working'].includes(phase)) throw new TorchError('Invalid runtime phase.', { code: 'INVALID_RUNTIME_INPUT' });
const current = JSON.parse(readFileSync(path, 'utf8'));
if (current.nonce !== nonce || current.pid !== process.pid) throw new TorchError('Runtime guard changed.', { code: 'RUNTIME_TURN_ACTIVE' });
const temporary = `${path}.${nonce}.tmp`;
writeFileSync(temporary, JSON.stringify({ ...record, phase }), { flag: 'wx', mode: 0o600 });
renameSync(temporary, path);
};
try { return run(setPhase); } finally {
// Delete only the exact guard created by this invocation; preserve foreign/changed guards.
const record = JSON.parse(readFileSync(path, 'utf8'));
if (record.nonce === nonce && record.pid === process.pid) {
unlinkSync(path);
rmdirSync(directory);
}
}
}
+12
View File
@@ -6,6 +6,18 @@ import '../../site/live-refresh.js';
const project = (snapshot) => globalThis.TorchAttentionProjection.groups(snapshot);
test('SCN-console-continuation-stop: exhausted budget is an owner policy decision; no-progress remains manager-owned', () => {
const groups = project({ continuation: { available: true, stopReason: 'daily-turn-cap', attempts: 12,
maxTurnsPerDay: 12, day: '2026-10-03', held: [{ areaId: 'owner-console' }] } });
assert.equal(groups.owner.length, 1);
assert.match(groups.owner[0].detail, /explicitly change the limit/);
assert.equal(groups.owner[0].decisionApprovalId, null, 'No fake approval or automatic budget expansion');
assert.equal(groups.fleet.length, 1);
assert.equal(groups.fleet[0].owner, 'session-manager');
assert.equal(groups.fleet[0].requestOwner, 'session-manager');
assert.equal(project({ continuation: { available: true, stopReason: 'paused', held: [] } }).owner.length, 0);
});
test('SCN-console-attention-ownership: only owner-addressed pending decisions enter Waiting on you', () => {
const groups = project({
agents: [
+420
View File
@@ -0,0 +1,420 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { execFileSync, spawn } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, join } from 'node:path';
import test from 'node:test';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { IntegrationService } from '../../src/integration/service.mjs';
const sha = value => execFileSync('git', ['-C', value, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
function fixture(name) {
const root = mkdtempSync(join(tmpdir(), `${name}-`));
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'package.json'), `${JSON.stringify({ name, scripts: {} })}\n`);
writeFileSync(join(root, 'app.mjs'), 'export const value = 1;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture base']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.domains.push({
id: 'review-area', title: 'Review Area', kind: 'development',
scope: ['review fixtures'], not_scope: ['application source'], owned_paths: ['review/**'],
shared_paths: [], neighbours: [], required_checks: [], resources: [], runtime: 'claude', evidence: ['app.mjs'],
});
proposal.checks = [];
proposal.review = { status: 'approved', reviewedAt: '2026-10-03T00:00:00.000Z', reviewedBy: 'fixture-owner', notes: [] };
const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-state`) };
installProject({ repository, proposal, env, projectId: name });
execFileSync('git', ['-C', root, 'add', '.torch']);
execFileSync('git', ['-C', root, 'commit', '-m', 'install fixture control plane']);
const base = sha(root);
execFileSync('git', ['-C', root, 'checkout', '-b', 'candidate']);
writeFileSync(join(root, 'candidate.mjs'), 'export const candidate = true;\n');
execFileSync('git', ['-C', root, 'add', 'candidate.mjs']);
execFileSync('git', ['-C', root, 'commit', '-m', 'candidate contribution']);
const candidate = sha(root);
execFileSync('git', ['-C', root, 'checkout', 'main']);
const control = openControlPlane({ repositoryRoot: root, env });
const service = new IntegrationService({ repositoryRoot: root, controlPlane: control, checkService: { exactPasses: () => false } });
const contributor = proposal.domains[0].id;
return { root, env, control, service, contributor, destination: 'review-area', base, candidate };
}
function concurrentAdoptedRequest(context, requestInput) {
const childSource = `
import { pathToFileURL } from 'node:url';
import { resolve } from 'node:path';
const { repositoryRoot, stateRoot, requestInput } = JSON.parse(process.argv[1]);
const sourceRoot = process.cwd();
const { openControlPlane } = await import(pathToFileURL(resolve(sourceRoot, 'src/control-plane/service.mjs')).href);
const { IntegrationService } = await import(pathToFileURL(resolve(sourceRoot, 'src/integration/service.mjs')).href);
const control = openControlPlane({ repositoryRoot, env: { ...process.env, XDG_DATA_HOME: stateRoot } });
const service = new IntegrationService({ repositoryRoot, controlPlane: control, checkService: { exactPasses: () => false } });
try {
const result = service.requestAdoptedCandidate(requestInput);
console.log(JSON.stringify({ outcome: 'requested', integrationRequestId: result.integrationRequest.id }));
} catch (error) {
console.log(JSON.stringify({ outcome: 'refused', code: error.code ?? null }));
} finally {
control.close();
}
`;
const encoded = JSON.stringify({ repositoryRoot: context.root, stateRoot: context.env.XDG_DATA_HOME, requestInput });
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, ['--input-type=module', '-e', childSource, encoded], {
cwd: process.cwd(), env: process.env, stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = ''; let stderr = '';
child.stdout.on('data', chunk => { stdout += chunk; });
child.stderr.on('data', chunk => { stderr += chunk; });
child.on('error', reject);
child.on('close', (code) => {
if (code !== 0) return reject(new Error(`concurrent adopted request child failed: ${stderr}`));
try { resolve(JSON.parse(stdout.trim().split('\n').at(-1))); }
catch (error) { reject(new Error(`invalid concurrent adopted request output: ${stdout}\n${stderr}\n${error.message}`)); }
});
});
}
function authority(context, { action, adoptionId }) {
const provenance = ['fixture:owner-approved', `candidate:${context.candidate}`];
const binding = {
schema: 'torch.dev/integration-adoption-authority/v1alpha1', action, adoptionId,
candidateSha: context.candidate, baseTargetSha: context.base,
provenanceManifestDigest: digest(provenance), contributorArea: context.contributor,
destinationArea: context.destination, targetBranch: 'main',
candidateRef: `refs/torch/integration-candidates/${adoptionId}`,
expectedOldRef: 'refs/heads/main', adapter: {
name: context.service.adoption.refAdapter.name, id: context.service.adoption.refAdapter.id,
},
};
const pending = context.control.requestApproval({
requester: context.contributor, approver: 'owner', task: 'TASK-f40126ee',
title: `Fixture ${action} authority`, summary: 'Hermetic exact-source adoption scenario.',
});
const approved = context.control.decideApproval({
approvalId: pending.id, decidedBy: 'owner', decision: 'approved', expectedRevision: pending.revision,
note: JSON.stringify(binding),
});
return {
approvalId: approved.id, approvalRevision: approved.revision, provenance,
provenanceManifestDigest: binding.provenanceManifestDigest, adapter: binding.adapter,
};
}
function input(context, action, adoptionId, extra = {}) {
return {
actorId: 'owner', adoptionId, candidateSha: context.candidate, baseTargetSha: context.base,
contributorArea: context.contributor, destinationArea: context.destination,
targetBranch: 'main', candidateRef: `refs/torch/integration-candidates/${adoptionId}`,
expectedOldRef: 'refs/heads/main', ...authority(context, { action, adoptionId }), ...extra,
};
}
function prepare(context, adoptionId = 'fixture-adoption') {
const preparedInput = input(context, 'prepare', adoptionId);
const plan = context.service.planAdoption({ ...preparedInput, action: 'prepare' });
assert.deepEqual(plan.blockers, [], `preparation must begin from an exact clean fixture: ${JSON.stringify(plan.blockers)}`);
return context.service.prepareAdoption(preparedInput);
}
function lifecycleInput(context, action, adoptionId, extra = {}) {
const { approvalId, approvalRevision, adapter } = authority(context, { action, adoptionId });
const record = context.service.getPreparedAdoption(adoptionId);
return { actorId: 'owner', adoptionId, expectedRecordRevision: record.revision, approvalId, approvalRevision, adapter, ...extra };
}
function blockedCodes(error) {
assert.equal(error.code.endsWith('_BLOCKED'), true, `expected a blocked adoption transition, got ${error.code}`);
return error.details.map((blocker) => blocker.code);
}
function lifecycleBoundary(context, adoptionId) {
const record = context.service.getPreparedAdoption(adoptionId);
return {
record,
refs: context.service.adoption.refAdapter.readRefs([
record.expectedOldRef, record.candidateRef, record.archiveRef,
]),
queue: context.service.list(),
auditCount: context.control.database.prepare(`SELECT COUNT(*) AS count FROM audit_events
WHERE project_id = ? AND entity_type = 'integration-adoption' AND entity_id = ?`)
.get(context.control.projectId, adoptionId).count,
};
}
function assertLifecycleBoundaryUnchanged(context, adoptionId, before) {
assert.deepEqual(lifecycleBoundary(context, adoptionId), before);
}
test('SCN-integration-adoption-private-ref-cas: a real temporary Git transaction atomically creates private archive and candidate refs, and stale expected refs refuse', () => {
const context = fixture('torch-adoption-cas');
const adoptionId = 'fixture-adoption';
prepare(context, adoptionId);
const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
const refs = context.service.adoption.refAdapter.readRefs([
'refs/heads/main', finalized.candidateRef, finalized.archiveRef,
]);
assert.deepEqual(refs, {
'refs/heads/main': context.base,
[finalized.candidateRef]: context.candidate,
[finalized.archiveRef]: context.base,
});
assert.throws(() => context.service.adoption.refAdapter.compareAndSwapRefs({
expected: [{ ref: finalized.candidateRef, sha: context.base }],
writes: [{ ref: finalized.candidateRef, sha: null }],
}), (error) => error.code === 'ADOPTION_REF_CAS_FAILED');
assert.equal(context.service.adoption.getPrepared(adoptionId).state, 'finalized');
context.control.close();
});
test('SCN-integration-adoption-journal-and-binding: a duplicate preparation cannot overwrite the journal and lifecycle input cannot alter its immutable candidate binding', () => {
const context = fixture('torch-adoption-journal');
const adoptionId = 'fixture-adoption';
const prepared = prepare(context, adoptionId);
assert.throws(() => context.service.prepareAdoption(input(context, 'prepare', adoptionId)),
(error) => error.code === 'ADOPTION_RECORD_REVISION_STALE');
assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, { candidateSha: context.base })),
(error) => error.code === 'ADOPTION_IMMUTABLE_BINDING_STALE');
assert.throws(() => context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId, {
expectedRecordRevision: prepared.revision + 1,
})), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE');
const refs = context.service.adoption.refAdapter.readRefs([
prepared.expectedOldRef, prepared.candidateRef, prepared.archiveRef,
]);
assert.deepEqual(refs, {
[prepared.expectedOldRef]: context.base,
[prepared.candidateRef]: null,
[prepared.archiveRef]: null,
});
const { mutationPerformed, ...durablePrepared } = prepared;
assert.equal(mutationPerformed, true);
assert.deepEqual(context.service.adoption.getPrepared(adoptionId), durablePrepared);
context.control.close();
});
test('SCN-integration-adoption-lifecycle-revision-cas: stale or omitted revisions for every lifecycle action refuse before refs, journal, queue, or audit effects', () => {
const cases = [
{
action: 'finalize',
setup: (context, adoptionId) => prepare(context, adoptionId),
invoke: (context, input) => context.service.finalizeAdoption(input),
},
{
action: 'reconcile',
setup: (context, adoptionId) => prepare(context, adoptionId),
invoke: (context, input) => context.service.reconcileAdoption(input),
},
{
action: 'rollback',
setup: (context, adoptionId) => {
prepare(context, adoptionId);
return context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
},
invoke: (context, input) => context.service.rollbackAdoption(input),
},
{
action: 'request',
setup: (context, adoptionId) => {
prepare(context, adoptionId);
return context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
},
invoke: (context, input) => context.service.requestAdoptedCandidate(input),
},
];
for (const { action, setup, invoke } of cases) {
const context = fixture(`torch-adoption-${action}-revision-boundary`);
const adoptionId = 'fixture-adoption';
setup(context, adoptionId);
for (const variant of ['omitted', 'stale']) {
const current = lifecycleInput(context, action, adoptionId);
const input = variant === 'omitted'
? (({ expectedRecordRevision: _expectedRecordRevision, ...withoutRevision }) => withoutRevision)(current)
: { ...current, expectedRecordRevision: current.expectedRecordRevision + 1 };
const before = lifecycleBoundary(context, adoptionId);
assert.throws(() => invoke(context, input), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE',
`${action} must reject a ${variant} lifecycle revision`);
assertLifecycleBoundaryUnchanged(context, adoptionId, before);
}
context.control.close();
}
});
test('SCN-integration-adoption-finalize-recovery: a crash after real ref CAS but before the journal transition reconciles once and never duplicates refs', () => {
const context = fixture('torch-adoption-finalize-recovery');
const adoptionId = 'fixture-adoption';
const prepared = prepare(context, adoptionId);
context.service.adoption.refAdapter.compareAndSwapRefs({
expected: [
{ ref: prepared.expectedOldRef, sha: prepared.baseTargetSha },
{ ref: prepared.candidateRef, sha: null },
{ ref: prepared.archiveRef, sha: null },
],
writes: [
{ ref: prepared.archiveRef, sha: prepared.baseTargetSha },
{ ref: prepared.candidateRef, sha: prepared.candidateSha },
],
});
const recovered = context.service.reconcileAdoption(lifecycleInput(context, 'reconcile', adoptionId));
assert.equal(recovered.state, 'recovered-finalized');
assert.equal(recovered.record.recovery, 'private-ref-cas-completed-before-journal');
const replay = context.service.reconcileAdoption(lifecycleInput(context, 'reconcile', adoptionId));
assert.equal(replay.state, 'consistent');
assert.equal(replay.mutationPerformed, false);
context.control.close();
});
test('SCN-integration-adoption-rollback-recovery: a journal-missing rollback completes without a second deletion, while divergent candidate bytes refuse', () => {
const context = fixture('torch-adoption-rollback-recovery');
const adoptionId = 'fixture-adoption';
prepare(context, adoptionId);
const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
assert.equal(finalized.state, 'finalized');
context.service.adoption.refAdapter.compareAndSwapRefs({
expected: [{ ref: finalized.candidateRef, sha: finalized.candidateSha }],
writes: [{ ref: finalized.candidateRef, sha: null }],
});
const recovered = context.service.rollbackAdoption(lifecycleInput(context, 'rollback', adoptionId));
assert.equal(recovered.state, 'rolled-back');
assert.equal(recovered.recovery, 'private-ref-rollback-completed-before-journal');
const divergent = fixture('torch-adoption-rollback-divergence');
const divergentId = 'fixture-adoption';
prepare(divergent, divergentId);
const divergentFinal = divergent.service.finalizeAdoption(lifecycleInput(divergent, 'finalize', divergentId));
divergent.service.adoption.refAdapter.compareAndSwapRefs({
expected: [{ ref: divergentFinal.candidateRef, sha: divergentFinal.candidateSha }],
writes: [{ ref: divergentFinal.candidateRef, sha: divergent.base }],
});
assert.throws(() => divergent.service.rollbackAdoption(lifecycleInput(divergent, 'rollback', divergentId)),
(error) => error.code === 'ADOPTION_ROLLBACK_REF_DIVERGED');
context.control.close();
divergent.control.close();
});
test('SCN-integration-adoption-private-ref-intake: finalized private refs enter the ordinary native integration queue, not a specialist branch shortcut', () => {
const unfinalized = fixture('torch-adoption-unfinalized-intake');
const unfinalizedId = 'fixture-adoption';
const unfinalizedRef = `refs/torch/integration-candidates/${unfinalizedId}`;
unfinalized.service.adoption.refAdapter.compareAndSwapRefs({
expected: [{ ref: unfinalizedRef, sha: null }],
writes: [{ ref: unfinalizedRef, sha: unfinalized.candidate }],
});
assert.throws(() => unfinalized.service.requestCandidate({
areaId: unfinalized.contributor, commit: unfinalized.candidate, candidateRef: unfinalizedRef,
}), (error) => error.code === 'INTEGRATION_ADOPTED_INTAKE_INTERNAL_ONLY');
assert.deepEqual(unfinalized.service.list(), []);
unfinalized.control.close();
const context = fixture('torch-adoption-native-intake');
const adoptionId = 'fixture-adoption';
prepare(context, adoptionId);
const finalized = context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
assert.throws(() => context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId, {
expectedRecordRevision: finalized.revision + 1,
})), (error) => error.code === 'ADOPTION_RECORD_REVISION_STALE');
assert.deepEqual(context.service.list(), []);
const requested = context.service.requestAdoptedCandidate(lifecycleInput(context, 'request', adoptionId));
assert.equal(requested.state, 'requested');
assert.equal(requested.integrationRequest.sourceBranch, finalized.candidateRef);
assert.equal(requested.integrationRequest.sourceCommit, context.candidate);
assert.equal(requested.integrationRequest.state, 'testing');
assert.equal(requested.integrationRequest.targetBranch, 'main');
context.control.close();
});
test('SCN-integration-adoption-approval-boundaries: unrelated or source-only approval records cannot authorize an owner adoption, and actor, action, revision, and provenance mismatches each refuse', () => {
const unrelated = fixture('torch-adoption-unrelated-approval');
const unrelatedId = 'fixture-adoption';
const unrelatedApproval = unrelated.control.requestApproval({
requester: unrelated.contributor, approver: 'owner', task: 'TASK-unrelated',
title: 'Ordinary source branch review', summary: 'Not an integration-adoption authority.',
});
const unrelatedApproved = unrelated.control.decideApproval({
approvalId: unrelatedApproval.id, decidedBy: 'owner', decision: 'approved', expectedRevision: unrelatedApproval.revision,
note: JSON.stringify({ schema: 'torch.dev/ordinary-source-review/v1alpha1', sourceArea: unrelated.contributor }),
});
const unrelatedInput = {
actorId: 'owner', adoptionId: unrelatedId, candidateSha: unrelated.candidate, baseTargetSha: unrelated.base,
contributorArea: unrelated.contributor, destinationArea: unrelated.destination, targetBranch: 'main',
candidateRef: `refs/torch/integration-candidates/${unrelatedId}`, expectedOldRef: 'refs/heads/main',
approvalId: unrelatedApproved.id, approvalRevision: unrelatedApproved.revision,
provenance: ['fixture:owner-approved', `candidate:${unrelated.candidate}`],
provenanceManifestDigest: digest(['fixture:owner-approved', `candidate:${unrelated.candidate}`]),
adapter: { name: unrelated.service.adoption.refAdapter.name, id: unrelated.service.adoption.refAdapter.id },
};
assert.throws(() => unrelated.service.prepareAdoption(unrelatedInput), (error) => blockedCodes(error).includes('ADOPTION_APPROVAL_BINDING_INVALID'));
assert.deepEqual(unrelated.service.list(), []);
unrelated.control.close();
const wrongActor = fixture('torch-adoption-wrong-actor');
assert.throws(() => wrongActor.service.prepareAdoption({
...input(wrongActor, 'prepare', 'fixture-adoption'), actorId: wrongActor.contributor,
}), (error) => blockedCodes(error).includes('OWNER_ACTOR_REQUIRED'));
wrongActor.control.close();
const wrongAction = fixture('torch-adoption-wrong-action');
prepare(wrongAction, 'fixture-adoption');
assert.throws(() => wrongAction.service.finalizeAdoption(lifecycleInput(wrongAction, 'rollback', 'fixture-adoption')),
(error) => blockedCodes(error).includes('ADOPTION_APPROVAL_BINDING_INVALID'));
wrongAction.control.close();
const staleRevision = fixture('torch-adoption-stale-revision');
prepare(staleRevision, 'fixture-adoption');
const staleInput = lifecycleInput(staleRevision, 'finalize', 'fixture-adoption');
assert.throws(() => staleRevision.service.finalizeAdoption({ ...staleInput, approvalRevision: staleInput.approvalRevision - 1 }),
(error) => blockedCodes(error).includes('ADOPTION_APPROVAL_REVISION_STALE'));
staleRevision.control.close();
const wrongProvenance = fixture('torch-adoption-wrong-provenance');
assert.throws(() => wrongProvenance.service.prepareAdoption({
...input(wrongProvenance, 'prepare', 'fixture-adoption'), provenance: ['fixture:owner-approved', 'unapproved:alternate-provenance'],
}), (error) => blockedCodes(error).includes('ADOPTION_PROVENANCE_DIGEST_MISMATCH'));
wrongProvenance.control.close();
});
test('SCN-integration-adoption-request-replay: concurrent authenticated API callers serialize on the adoption journal and create exactly one private-ref queue request', async () => {
const context = fixture('torch-adoption-request-replay');
const adoptionId = 'fixture-adoption';
prepare(context, adoptionId);
context.service.finalizeAdoption(lifecycleInput(context, 'finalize', adoptionId));
const requestInput = lifecycleInput(context, 'request', adoptionId);
const outcomes = await Promise.all([
concurrentAdoptedRequest(context, requestInput),
concurrentAdoptedRequest(context, requestInput),
]);
assert.deepEqual(outcomes.map((entry) => entry.outcome).sort(), ['refused', 'requested']);
assert.equal(outcomes.find((entry) => entry.outcome === 'refused').code, 'ADOPTION_RECORD_REVISION_STALE');
const requests = context.service.list();
assert.equal(requests.length, 1);
assert.equal(context.service.getPreparedAdoption(adoptionId).state, 'requested');
context.control.close();
});
test('SCN-integration-adoption-legacy-retries: historical ordinary-request retries remain readable when integration service starts', () => {
const context = fixture('torch-adoption-legacy-retries');
const insert = context.control.database.prepare(`INSERT INTO integration_requests (
id, project_id, source_area, source_branch, source_commit, target_branch,
base_target_commit, state, reason, required_checks, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
for (const [id, state] of [['historical-failed', 'blocked'], ['historical-retry', 'superseded']]) {
insert.run(id, context.control.projectId, context.contributor, 'refs/heads/candidate', context.candidate,
'main', context.base, state, 'historical-retry', '[]', '2026-10-03T00:00:00.000Z', '2026-10-03T00:00:00.000Z');
}
assert.doesNotThrow(() => new IntegrationService({
repositoryRoot: context.root, controlPlane: context.control, checkService: { exactPasses: () => false },
}));
assert.equal(context.service.list().length, 2);
context.control.close();
});
+433
View File
@@ -0,0 +1,433 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { issueStoppedExecutorEvidence } from '../../src/runtime/stopped-executor-evidence.mjs';
import { observeRuntimeTurn, withRuntimeTurnGuard } from '../../src/runtime/turn-guard.mjs';
import { observeProject } from '../../src/observability/snapshot.mjs';
import { configureContinuation, observeContinuation, planContinuation, runContinuation } from '../../src/runtime/continuation.mjs';
function fixture({ recoveryWorktree = true, extraWorker = false, workerCount = 0 } = {}) {
const root = mkdtempSync(join(tmpdir(), 'torch-legacy-recovery-'));
for (const args of [['init', '-b', 'main'], ['config', 'user.email', '[email protected]'],
['config', 'user.name', 'Recovery Fixture']]) execFileSync('git', args, { cwd: root, stdio: 'pipe' });
writeFileSync(join(root, 'app.js'), 'export const ready = true;\n');
execFileSync('git', ['add', '.'], { cwd: root });
execFileSync('git', ['commit', '-m', 'fixture'], { cwd: root, stdio: 'pipe' });
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
if (extraWorker) proposal.domains.push({ ...proposal.domains[0], id: 'extra-worker', title: 'Extra worker', owned_paths: ['extra/**'] });
for (let i = 0; i < workerCount; i++) proposal.domains.push({ ...proposal.domains[0], id: `worker-${i}`, title: `Worker ${i}`, owned_paths: [`worker-${i}/**`] });
proposal.review = { status: 'approved', reviewedAt: '2026-10-03T00:00:00Z', reviewedBy: 'owner', notes: [] };
const env = { ...process.env, XDG_DATA_HOME: join(root, '.data') };
installProject({ repository, proposal, env, projectId: 'recovery-fixture' });
const actualPath = join(root, '.torch', 'install-manifest.json');
const manifest = JSON.parse(readFileSync(actualPath, 'utf8'));
const areaId = proposal.domains[0].id;
if (recoveryWorktree) manifest.external.push({ type: 'worktree', area: areaId, path: join(root, 'worker') });
writeFileSync(actualPath, JSON.stringify(manifest));
let time = Date.parse('2026-10-03T00:00:00Z');
const control = openControlPlane({ repositoryRoot: root, env, clock: () => new Date(time) });
control.reportStatus({ areaId, state: 'working', runtime: 'codex', runtimeSessionId: 'fixture-session',
task: 'TASK-preserved', summary: 'Runtime executor completion is unknown; duplicate launch is blocked pending reconciliation.' });
const identity = control.getAgent(areaId);
const unit = `torch-${control.projectId}-worker.service`;
const properties = { Id: unit, ActiveState: 'failed', SubState: 'failed', MainPID: '0', ControlPID: '0',
ControlGroup: '', InvocationID: 'a'.repeat(32), ExecStart: `node /tmp/probe.mjs ${areaId} ;`,
WorkingDirectory: root, ExecMainExitTimestamp: identity.updatedAt };
let processes = [];
const issue = () => issueStoppedExecutorEvidence({ controlPlane: control, actorId: 'owner', areaId, unit,
invocationId: 'a'.repeat(32), readUnit: () => ({ ...properties }), readProcesses: () => processes, now: () => time });
const apply = issued => control.recoverOwnerStoppedUnknownIdentity({ actorId: 'owner', areaId,
expectedIdentity: issued.plan.expectedIdentity, evidence: issued.evidence });
return { root, env, control, identity, areaId, properties, issue, apply,
advance: ms => { time += ms; }, processes: value => { processes = value; } };
}
test('SCN-legacy-runtime-recovery: owner recovery preserves task/session/unknown and records exactly one owner audit', () => {
const f = fixture();
try {
const issued = f.issue();
assert.equal(f.control.getAgent(f.areaId).state, 'working');
const result = f.apply(issued);
assert.equal(result.identity.state, 'offline');
assert.equal(result.identity.currentTask, 'TASK-preserved');
assert.equal(result.identity.runtimeSessionId, 'fixture-session');
assert.equal(result.priorOutcome, 'unknown');
assert.equal(result.sessionsStarted, false);
const audits = f.control.readAudit({ limit: 100 }).filter(x => x.operation === 'runtime.legacy-unknown.recover-stopped');
assert.equal(audits.length, 1);
assert.equal(audits[0].actorId, 'owner');
assert.equal(audits[0].details.outcome, 'unknown');
assert.throws(() => f.apply(issued), { code: 'RUNTIME_RECOVERY_SNAPSHOT_CONFLICT' });
} finally { f.control.close(); }
});
test('SCN-legacy-runtime-recovery-refusal: live, foreign invocation, area, time and process mismatches cannot issue proof', () => {
for (const patch of [{ MainPID: '99' }, { ControlPID: '99' }, { ActiveState: 'active' },
{ InvocationID: 'b'.repeat(32) }, { ExecStart: 'node other-area' },
{ ExecMainExitTimestamp: '2026-10-02T00:00:00Z' }, { ControlGroup: '/live' }]) {
const f = fixture();
try { Object.assign(f.properties, patch); assert.throws(f.issue, { code: 'RUNTIME_RECOVERY_UNVERIFIED' });
assert.equal(f.control.getAgent(f.areaId).state, 'working'); } finally { f.control.close(); }
}
const f = fixture();
try { f.processes([{ pid: 999, argv: ['codex', 'resume', 'fixture-session'], cwd: '/tmp' }]);
assert.throws(f.issue, { code: 'RUNTIME_RECOVERY_UNVERIFIED' }); } finally { f.control.close(); }
});
test('SCN-legacy-runtime-recovery-cas: changed snapshot, live recheck, expiry and caller-forged proof refuse without mutation', () => {
for (const scenario of ['changed', 'live', 'expired', 'forged']) {
const f = fixture();
try {
const issued = f.issue();
if (scenario === 'changed') { f.advance(1); f.control.reportStatus({ areaId: f.areaId, state: 'working', runtimeSessionId: 'different', summary: 'Runtime executor completion is unknown' }); }
if (scenario === 'live') f.properties.MainPID = '99';
if (scenario === 'expired') f.advance(60_001);
if (scenario === 'forged') issued.evidence = { stopped: true };
assert.throws(() => f.apply(issued));
assert.equal(f.control.getAgent(f.areaId).state, 'working');
assert.equal(f.control.readAudit({ limit: 100 }).filter(x => x.operation === 'runtime.legacy-unknown.recover-stopped').length, 0);
} finally { f.control.close(); }
}
});
test('SCN-legacy-runtime-recovery-authority: specialist and incomplete snapshot cannot use owner recovery', () => {
const f = fixture();
try {
const issued = f.issue();
assert.throws(() => f.control.recoverOwnerStoppedUnknownIdentity({ actorId: f.areaId, areaId: f.areaId,
expectedIdentity: issued.plan.expectedIdentity, evidence: issued.evidence }), { code: 'OWNER_AUTHORITY_REQUIRED' });
assert.throws(() => f.control.recoverOwnerStoppedUnknownIdentity({ actorId: 'owner', areaId: f.areaId,
expectedIdentity: {}, evidence: issued.evidence }), { code: 'RUNTIME_RECOVERY_SNAPSHOT_REQUIRED' });
assert.equal(f.control.getAgent(f.areaId).state, 'working');
} finally { f.control.close(); }
});
test('SCN-runtime-turn-guard: overlapping executors refuse even if an agent reports idle; terminal turns release their own guard', () => {
const f = fixture();
try {
let launches = 0;
withRuntimeTurnGuard(f.control, f.areaId, () => {
launches++;
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Agent reported idle before physical exit' });
assert.throws(() => withRuntimeTurnGuard(f.control, f.areaId, () => { launches++; }), { code: 'RUNTIME_TURN_ACTIVE' });
});
withRuntimeTurnGuard(f.control, f.areaId, () => { launches++; });
assert.equal(launches, 2);
} finally { f.control.close(); }
});
test('SCN-runtime-live-presence: live guard phases override stale idle display without changing durable reports or waits', { skip: process.platform !== 'linux' }, () => {
const f = fixture({ recoveryWorktree: false });
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Previous turn completed' });
const observe = () => observeProject({ repositoryRoot: f.root, env: f.env }).agents.find(a => a.areaId === f.areaId);
withRuntimeTurnGuard(f.control, f.areaId, phase => {
let agent = observe();
assert.equal(agent.state, 'starting');
assert.equal(agent.reportedState, 'idle');
assert.equal(agent.executor.source, 'live-process-and-turn-guard');
phase('working');
agent = observe();
assert.equal(agent.state, 'working');
assert.equal(f.control.getAgent(f.areaId).state, 'idle', 'Observation is not an identity mutation');
f.control.reportStatus({ areaId: f.areaId, state: 'waiting', summary: 'Waiting for named approval' });
assert.equal(observe().state, 'waiting', 'Live execution must not conceal semantic approval waits');
});
assert.equal(observe().executor.state, 'inactive');
assert.equal(observe().state, 'waiting');
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Turn completed' });
assert.equal(observe().state, 'idle');
} finally { f.control.close(); }
});
test('SCN-runtime-live-presence-refusal: dead, replaced, foreign-user and malformed guards never claim active execution', () => {
const f = fixture();
try {
withRuntimeTurnGuard(f.control, f.areaId, () => {
const path = join(f.control.stateRoot, 'sessions', 'turn-guards', f.areaId, 'owner.json');
const original = JSON.parse(readFileSync(path, 'utf8'));
writeFileSync(path, JSON.stringify({ ...original, startTicks: 'fixture-start' }));
const read = live => observeRuntimeTurn({ stateRoot: f.control.stateRoot, repositoryRoot: f.root,
projectId: f.control.projectId, areaId: f.areaId, uid: 1000, inspectProcess: () => live });
const live = { uid: 1000, startTicks: 'fixture-start', state: 'S' };
assert.equal(read(live).state, 'active');
for (const mismatch of [{ ...live, uid: 2000 }, { ...live, startTicks: 'reused-pid' }, { ...live, state: 'Z' }]) {
assert.equal(read(mismatch).state, 'unknown');
}
const absent = observeRuntimeTurn({ stateRoot: f.control.stateRoot, repositoryRoot: f.root,
projectId: f.control.projectId, areaId: f.areaId, inspectProcess: () => { throw new Error('Process gone'); } });
assert.equal(absent.state, 'unknown');
writeFileSync(path, JSON.stringify({ ...original, projectId: 'foreign' }));
assert.equal(read(live).reason, 'guard-invalid');
writeFileSync(path, JSON.stringify(original));
});
} finally { f.control.close(); }
});
test('SCN-runtime-live-presence-legacy: pre-observation guards require exact TORCH identity and project cwd', () => {
const f = fixture();
try {
withRuntimeTurnGuard(f.control, f.areaId, () => {
const path = join(f.control.stateRoot, 'sessions', 'turn-guards', f.areaId, 'owner.json');
const original = JSON.parse(readFileSync(path, 'utf8'));
const legacy = { ...original };
delete legacy.startTicks;
delete legacy.phase;
writeFileSync(path, JSON.stringify(legacy));
const inspect = live => observeRuntimeTurn({ stateRoot: f.control.stateRoot, repositoryRoot: f.root,
projectId: f.control.projectId, areaId: f.areaId, uid: 1000, inspectProcess: () => live });
const live = { uid: 1000, state: 'S', cwd: f.root, argv: ['node', '/runtime/bin/torch.mjs', 'up', '--only', f.areaId] };
assert.equal(inspect(live).phase, 'working');
assert.equal(inspect({ ...live, cwd: '/foreign' }).state, 'unknown');
assert.equal(inspect({ ...live, argv: ['node', 'torch.mjs', 'up', '--only', 'someone-else'] }).state, 'unknown');
writeFileSync(path, JSON.stringify(original));
});
} finally { f.control.close(); }
});
test('SCN-runtime-continuation: explicit enable, event deduplication, daily cap and pause govern authorized turns', async () => {
const f = fixture();
const backlog = { list: () => [] };
const at = new Date('2026-10-03T00:00:00Z');
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Physical turn completed' });
f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'Continue assigned work' });
assert.equal(planContinuation(f.control, backlog, { at }).reason, 'paused');
assert.throws(() => configureContinuation(f.control, { actorId: f.areaId, enabled: true }), { code: 'OWNER_AUTHORITY_REQUIRED' });
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 1 });
assert.equal(planContinuation(f.control, backlog, { at }).candidates.length, 1);
let calls = 0;
const run = () => runContinuation(f.control, backlog, { actorId: 'owner', at, launch: async area => { assert.equal(area, f.areaId); calls++; return 0; } });
assert.equal((await run()).launched.length, 1);
assert.equal((await run()).launched.length, 0);
f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'New work' });
assert.equal(planContinuation(f.control, backlog, { at }).reason, 'daily-turn-cap');
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 2 });
assert.equal(planContinuation(f.control, backlog, { at }).candidates.length, 1);
configureContinuation(f.control, { actorId: 'owner', enabled: false, maxTurnsPerDay: 2 });
assert.equal((await run()).launched.length, 0);
assert.equal(calls, 1);
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-day-override: owner extra allowance preserves charges and expires at UTC reset', async () => {
const f = fixture();
const backlog = { list: () => [] };
const at = new Date('2026-10-03T00:00:00Z');
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Ready' });
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 1 });
f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'First work' });
const run = () => runContinuation(f.control, backlog, { actorId: 'owner', at, launch: async () => 0 });
assert.equal((await run()).launched.length, 1);
assert.equal(planContinuation(f.control, backlog, { at }).reason, 'daily-turn-cap');
assert.throws(() => configureContinuation(f.control, { actorId: f.areaId, enabled: true, todayMaxTurns: 3 }), { code: 'OWNER_AUTHORITY_REQUIRED' });
for (const todayMaxTurns of [0, 97, 1.5]) assert.throws(() => configureContinuation(f.control,
{ actorId: 'owner', enabled: true, todayMaxTurns }), { code: 'CONTINUATION_POLICY_INVALID' });
configureContinuation(f.control, { actorId: 'owner', enabled: true, todayMaxTurns: 3 });
assert.equal(planContinuation(f.control, backlog, { at }).attempts, 1, 'Previously charged turn is retained');
assert.equal(planContinuation(f.control, backlog, { at }).remainingTurns, 2);
f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'Additional work' });
assert.equal((await run()).launched.length, 1);
configureContinuation(f.control, { actorId: 'owner', enabled: false });
const sameDay = observeContinuation({ stateRoot: f.control.stateRoot, now: () => at });
assert.equal(sameDay.attempts, 2);
assert.equal(sameDay.maxTurnsPerDay, 3);
assert.equal(sameDay.stopReason, 'paused');
const nextDay = observeContinuation({ stateRoot: f.control.stateRoot, now: () => new Date('2026-10-04T00:00:00Z') });
assert.equal(nextDay.maxTurnsPerDay, 1, 'Override cannot widen future daily allowances');
assert.equal(nextDay.attempts, 0);
assert.ok(f.control.readAudit({ limit: 100 }).some(a => a.operation === 'runtime.continuation.configure'
&& a.details?.dailyOverride?.maxTurns === 3));
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-concurrency: seven authorized starts preserve budget and obey tighter limits', async () => {
const f = fixture({ workerCount: 5 });
const backlog = { list: () => [] };
const at = new Date('2026-10-03T00:00:00Z');
try {
for (const agent of f.control.listAgents()) {
f.control.reportStatus({ areaId: agent.areaId, state: 'idle', summary: 'Ready' });
f.control.sendOwnerRequest({ actorId: 'owner', recipient: agent.areaId, body: 'Review work' });
}
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 48 });
assert.equal(planContinuation(f.control, backlog, { at }).maxConcurrency, 3);
assert.equal(planContinuation(f.control, backlog, { at, limit: 7 }).candidates.length, 3, 'Tick cannot exceed approved capacity');
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxConcurrency: 7 });
assert.equal(observeContinuation({ stateRoot: f.control.stateRoot }).maxTurnsPerDay, 48);
assert.equal(planContinuation(f.control, backlog, { at, limit: 2 }).candidates.length, 2);
for (const value of [0, 8, 1.5]) assert.throws(() => configureContinuation(f.control,
{ actorId: 'owner', enabled: true, maxConcurrency: value }), { code: 'CONTINUATION_POLICY_INVALID' });
const completions = [];
const running = runContinuation(f.control, backlog, { actorId: 'owner', at,
launch: () => new Promise(resolve => completions.push(resolve)) });
assert.equal(completions.length, 7, 'All seven start before any finishes');
assert.equal(planContinuation(f.control, backlog, { at }).attempts, 7);
configureContinuation(f.control, { actorId: 'owner', enabled: false });
assert.equal(observeContinuation({ stateRoot: f.control.stateRoot, now: () => at }).attempts, 7);
assert.equal(observeContinuation({ stateRoot: f.control.stateRoot }).maxConcurrency, 7);
completions.forEach(resolve => resolve(0));
assert.equal((await running).launched.length, 7);
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-signals: acknowledgements do not wake; eligible turns start concurrently within the limit', async () => {
const f = fixture();
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' });
const messages = [];
for (const recipient of [f.areaId, 'session-manager']) {
messages.push(f.control.sendOwnerRequest({ actorId: 'owner', recipient, body: 'Review current work' }));
f.control.sendOwnerRequest({ actorId: 'owner', recipient, body: 'Second handoff' });
}
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 4 });
const completions = [];
const running = runContinuation(f.control, { list: () => [] }, { actorId: 'owner', limit: 2,
launch: () => new Promise(resolve => completions.push(resolve)) });
assert.equal(completions.length, 2, 'Both starts occur without waiting for the first provider to finish');
assert.equal((await runContinuation(f.control, { list: () => [] }, { actorId: 'owner', launch: () => 0 })).reason,
'controller-active-or-unreconciled');
completions.forEach(resolve => resolve(0));
assert.equal((await running).launched.length, 2);
for (const message of messages) f.control.ackMessage({ recipient: message.recipient, messageId: message.id });
assert.equal(planContinuation(f.control, { list: () => [] }).candidates.length, 0);
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-pause: a pause during one turn prevents another queued turn', async () => {
const f = fixture();
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' });
for (const recipient of [f.areaId, 'session-manager']) f.control.sendOwnerRequest({ actorId: 'owner', recipient, body: 'Review current work' });
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 3 });
const result = await runContinuation(f.control, { list: () => [] }, { actorId: 'owner',
launch: async () => { configureContinuation(f.control, { actorId: 'owner', enabled: false, maxTurnsPerDay: 3 }); return 0; } });
assert.equal(result.launched.length, 1);
assert.equal(planContinuation(f.control, { list: () => [] }).reason, 'paused');
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-refill: a short turn frees capacity while the manager remains running', async () => {
const f = fixture({ extraWorker: true });
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' });
for (const recipient of ['session-manager', f.areaId, 'extra-worker']) {
f.control.sendOwnerRequest({ actorId: 'owner', recipient, body: 'Eligible handoff' });
}
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 3 });
const starts = [];
const finishes = new Map();
let thirdStarted;
const third = new Promise(resolve => { thirdStarted = resolve; });
const run = runContinuation(f.control, { list: () => [] }, { actorId: 'owner', limit: 2,
launch: area => { starts.push(area); if (starts.length === 3) thirdStarted();
return new Promise(resolve => finishes.set(area, resolve)); } });
assert.deepEqual(starts, ['session-manager', f.areaId]);
finishes.get(f.areaId)(0);
await third;
assert.deepEqual(starts, ['session-manager', f.areaId, 'extra-worker']);
assert.equal(finishes.has('session-manager'), true, 'Manager completion was not required for refill');
finishes.get('extra-worker')(0);
finishes.get('session-manager')(0);
assert.equal((await run).launched.length, 3);
assert.equal(planContinuation(f.control, { list: () => [] }).reason, 'daily-turn-cap');
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-no-progress: unfinished assignment continues once then escalates rather than busy-looping', async () => {
const f = fixture();
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' });
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 8 });
const task = { id: 'TASK-continue', owner: f.areaId, state: 'assigned', revision: 1, dependencies: [] };
let specialistTurns = 0;
const result = await runContinuation(f.control, { list: () => [task] }, { actorId: 'owner', limit: 1,
launch: area => { if (area === f.areaId) specialistTurns++; return 0; } });
assert.equal(specialistTurns, 2);
assert.equal(result.launched.filter(t => t.areaId === f.areaId).length, 2);
const messages = f.control.readMessages({ recipient: 'session-manager', unacknowledgedOnly: true, limit: 1000 });
assert.equal(messages.filter(m => m.body.includes('two turns without advancing')).length, 1);
assert.equal(planContinuation(f.control, { list: () => [task] }).candidates.some(c => c.areaId === f.areaId), false);
task.revision++;
assert.equal(planContinuation(f.control, { list: () => [task] }).candidates.some(c => c.areaId === f.areaId), true);
task.dependencies = ['TASK-unfinished'];
assert.equal(planContinuation(f.control, { list: () => [task] }).candidates.some(c => c.areaId === f.areaId), false);
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-capacity: a manually held executor counts against automatic fleet capacity', async () => {
const f = fixture();
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' });
f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'Eligible handoff' });
configureContinuation(f.control, { actorId: 'owner', enabled: true });
let running;
withRuntimeTurnGuard(f.control, 'session-manager', () => {
running = runContinuation(f.control, { list: () => [] }, { actorId: 'owner', limit: 1,
launch: () => { throw new Error('Capacity must not be exceeded'); } });
});
assert.deepEqual((await running).launched, []);
assert.equal(planContinuation(f.control, { list: () => [] }).attempts, 0);
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-fairness: a fresh task revision does not let one specialist starve an unstarted peer', async () => {
const f = fixture({ extraWorker: true });
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' });
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 3 });
const task = { id: 'TASK-changing', owner: f.areaId, state: 'assigned', revision: 1, dependencies: [] };
f.control.sendOwnerRequest({ actorId: 'owner', recipient: 'extra-worker', body: 'Peer work' });
const starts = [];
await runContinuation(f.control, { list: () => [task] }, { actorId: 'owner', limit: 1,
launch: area => { starts.push(area); if (area === f.areaId) task.revision++; return 0; } });
assert.deepEqual(starts.slice(0, 2), [f.areaId, 'extra-worker']);
} finally { f.control.close(); }
});
test('SCN-runtime-continuation-health: budget stops, day reset, pause and unreadable ledgers are visible without mutation', async () => {
const f = fixture({ recoveryWorktree: false });
const at = new Date('2026-10-03T00:00:00Z');
try {
f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' });
f.control.sendOwnerRequest({ actorId: 'owner', recipient: f.areaId, body: 'Work' });
configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 1 });
await runContinuation(f.control, { list: () => [] }, { actorId: 'owner', at, launch: () => 0 });
const path = join(f.control.stateRoot, 'continuation.json');
const before = readFileSync(path, 'utf8');
const health = observeContinuation({ stateRoot: f.control.stateRoot, now: () => at });
assert.equal(health.stopReason, 'daily-turn-cap');
assert.equal(health.remainingTurns, 0);
assert.equal(health.manualTurnsIncluded, false);
assert.equal(health.timerInstallationVerified, false);
const snapshot = observeProject({ repositoryRoot: f.root, env: f.env, now: () => at });
assert.equal(snapshot.continuation.stopReason, 'daily-turn-cap');
assert.equal(readFileSync(path, 'utf8'), before);
assert.equal(observeContinuation({ stateRoot: f.control.stateRoot, now: () => new Date('2026-10-04T00:00:00Z') }).remainingTurns, 1);
configureContinuation(f.control, { actorId: 'owner', enabled: false, maxTurnsPerDay: 1 });
assert.equal(observeContinuation({ stateRoot: f.control.stateRoot, now: () => at }).stopReason, 'paused');
writeFileSync(path, 'not-json');
assert.equal(observeContinuation({ stateRoot: f.control.stateRoot, now: () => at }).stopReason, 'policy-or-ledger-unreadable');
assert.equal(readFileSync(path, 'utf8'), 'not-json', 'Observation must not repair or reset the attempt ledger');
} finally { f.control.close(); }
});
test('SCN-legacy-runtime-recovery-project: evidence cannot be applied to a different checkout with cloned project/session fields', () => {
const first = fixture();
const second = fixture();
try {
assert.throws(() => second.control.recoverOwnerStoppedUnknownIdentity({ actorId: 'owner', areaId: second.areaId,
expectedIdentity: second.identity, evidence: first.issue().evidence }), { code: 'RUNTIME_RECOVERY_UNVERIFIED' });
assert.equal(second.control.getAgent(second.areaId).state, 'working');
} finally { first.control.close(); second.control.close(); }
});
+38
View File
@@ -14,6 +14,7 @@ import { installProject } from '../../src/kernel/install.mjs';
import { defaultManagerCheckInSchedule, organizationGraphFromConfig } from '../../src/kernel/organization.mjs';
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
import { ResourceService } from '../../src/resources/service.mjs';
import { createTorchToolset } from '../../src/mcp/tools.mjs';
import {
createFleetBrief, detachFleet, planAreaUp, planFleetDetach, planFleetDown, planFleetUp, startFleet, stopFleet,
} from '../../src/runtime/lifecycle.mjs';
@@ -83,6 +84,43 @@ function fleetFixture({ workerRuntime = 'claude', hierarchy = false } = {}) {
return { root, env, worker: proposal.domains[0].id, middleManager: hierarchy ? 'runtime-lead' : null };
}
test('SCN-fleet-startup-inbox: fresh and resumed launch instructions retrieve newer unread handoffs beyond the default history page', () => {
const fixture = fleetFixture();
const control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
const adapters = new Map([['claude', createClaudeAdapter({ executable: 'claude' })]]);
let tick = 0;
control.clock = () => new Date(Date.parse('2026-10-03T00:00:00Z') + tick++);
try {
const newest = new Map();
for (const areaId of ['session-manager', fixture.worker]) {
for (let i = 0; i < 102; i++) newest.set(areaId, control.sendOwnerRequest({ actorId: 'owner', recipient: areaId, body: `Current handoff ${i}` }));
const history = control.readMessages({ recipient: areaId });
assert.equal(history.length, 100);
assert.ok(!history.some(m => m.id === newest.get(areaId).id), 'Default historical page misses the latest handoff');
}
for (const fresh of [true, false]) {
if (!fresh) for (const areaId of newest.keys()) control.reportStatus({ areaId, state: 'idle', runtime: 'claude', runtimeSessionId: `fixture-${areaId}` });
const plan = planFleetUp({ repositoryRoot: fixture.root, controlPlane: control, adapters, fresh });
assert.equal(plan.canProceed, true);
for (const action of plan.actions) {
assert.equal(action.mode, fresh ? 'create' : 'resume');
const launchText = action.launch.args.join(' ');
const match = launchText.match(/Inbox request: (\{[^}]+\})\./);
assert.ok(match, 'Actual provider launch carries the explicit current-inbox request');
const request = JSON.parse(match[1]);
assert.deepEqual(request, { recipient: action.areaId, unacknowledged_only: true, limit: 1000 });
const tool = createTorchToolset(control, { actorId: action.areaId }).get('torch_read_messages');
const result = tool.invoke(request);
assert.equal(result.messages.length, 102);
assert.equal(result.messages.at(-1).id, newest.get(action.areaId).id);
assert.equal(control.readMessages({ recipient: action.areaId, unacknowledgedOnly: true, limit: 1000 }).length, 102, 'Reading never acknowledges work');
assert.match(launchText, /coverage may be truncated/);
assert.match(launchText, /messages do not override enforced approval or ownership boundaries/i);
}
}
} finally { control.close(); }
});
test('SCN-fleet-fresh-resume: identities resume captured runtime sessions and wind down safely', () => {
const fixture = fleetFixture();
const control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
+50
View File
@@ -242,6 +242,56 @@ test('SCN-codex-adapter: turns are resumable, identity-bound, durable, and capab
control.close();
});
test('SCN-codex-mcp-runtime-dir: only a valid Linux parent runtime directory is named for MCP inheritance', () => {
const context = fixture();
const validParent = {
XDG_RUNTIME_DIR: '/run/user/1000',
DBUS_SESSION_BUS_ADDRESS: 'unix:path=/run/user/1000/bus',
HOME: '/home/fixture', PATH: '/usr/bin', TOKEN: 'not-forwarded',
};
const adapter = createCodexAdapter({ env: validParent, platform: 'linux' });
const mcp = adapter.configure({ repositoryRoot: context.root, areaId: context.worker, mcpEntry: MCP_SERVER }).mcp;
const create = adapter.createSession({
areaId: context.worker, worktree: '/tmp/codex-runtime-dir', promptFile: '/tmp/prompt.md',
firstMessage: 'Start.', mcp,
});
const resume = adapter.resumeSession({
areaId: context.worker, runtimeSessionId: 'thread-runtime-dir', worktree: '/tmp/codex-runtime-dir',
message: 'Resume.', mcp,
});
const expected = `mcp_servers.${mcp.name}.env_vars=["XDG_RUNTIME_DIR"]`;
for (const launch of [create, resume]) {
assert.equal(launch.launch.args.includes(expected), true);
assert.equal(launch.launch.args.some((arg) => /env_vars=/.test(arg) && arg !== expected), false);
assert.equal(launch.launch.args.some((arg) => arg.includes(validParent.XDG_RUNTIME_DIR)
|| arg.includes(validParent.DBUS_SESSION_BUS_ADDRESS) || arg.includes(validParent.HOME)
|| arg.includes(validParent.PATH) || arg.includes(validParent.TOKEN)), false);
}
for (const invalid of [
{ env: {}, platform: 'linux' },
{ env: { XDG_RUNTIME_DIR: '' }, platform: 'linux' },
{ env: { XDG_RUNTIME_DIR: 'relative/runtime' }, platform: 'linux' },
{ env: validParent, platform: 'darwin' },
]) {
const unsupported = createCodexAdapter(invalid);
const unsupportedMcp = unsupported.configure({
repositoryRoot: context.root, areaId: context.worker, mcpEntry: MCP_SERVER,
}).mcp;
const createPlan = unsupported.createSession({
areaId: context.worker, worktree: '/tmp/codex-runtime-dir', promptFile: '/tmp/prompt.md',
firstMessage: 'Start.', mcp: unsupportedMcp,
});
const resumePlan = unsupported.resumeSession({
areaId: context.worker, runtimeSessionId: 'thread-runtime-dir', worktree: '/tmp/codex-runtime-dir',
message: 'Resume.', mcp: unsupportedMcp,
});
for (const plan of [createPlan, resumePlan]) {
assert.equal(plan.launch.args.some((arg) => /env_vars=/.test(arg)), false);
}
}
});
test('SCN-mcp-stdio: an MCP host can initialize and list the TORCH tool surface over stdio', async () => {
const context = fixture();
const child = spawn(process.execPath, [MCP_SERVER, '--root', context.root, '--area', context.worker], {
+145
View File
@@ -0,0 +1,145 @@
import assert from 'node:assert/strict';
import { execFileSync, spawn } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
const MCP_SERVER = new URL('../../src/mcp/server.mjs', import.meta.url).pathname;
function installedFixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-mcp-owner-reply-'));
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'index.js'), 'export const fixture = true;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.review = { status: 'approved', reviewedAt: '2026-10-03T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
const env = { ...process.env, XDG_DATA_HOME: join(root, '.data') };
installProject({ repository, proposal, env, projectId: 'mcp-owner-reply-fixture' });
const control = openControlPlane({ repositoryRoot: root, env });
const identities = control.listAgents().map((agent) => agent.areaId);
control.close();
const worker = identities.find((areaId) => areaId !== 'qa' && areaId !== 'session-manager')
?? identities.find((areaId) => areaId !== 'qa')
?? identities[0];
const other = identities.find((areaId) => areaId !== worker);
assert.ok(other, 'fixture requires a second registered Fleet identity');
return { root, env, worker, other };
}
async function connectMcp({ root, env, actorId }) {
const child = spawn(process.execPath, [MCP_SERVER, '--root', root, '--area', actorId], {
cwd: root, env, stdio: ['pipe', 'pipe', 'pipe'],
});
const responses = new Map();
let stdout = '';
let stderr = '';
child.stderr.on('data', (chunk) => { stderr += chunk; });
child.stdout.on('data', (chunk) => {
stdout += chunk;
const lines = stdout.split('\n');
stdout = lines.pop();
for (const line of lines.filter(Boolean)) {
const message = JSON.parse(line);
if (message.id !== undefined) responses.get(message.id)?.(message);
}
});
let nextId = 0;
const request = (method, params) => new Promise((resolve, reject) => {
const id = ++nextId;
const timeout = setTimeout(() => reject(new Error(`MCP response timeout. stderr: ${stderr}`)), 5000);
responses.set(id, (response) => { clearTimeout(timeout); resolve(response); });
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
});
const initialized = await request('initialize', {
protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'torch-test', version: '1.0.0' },
});
assert.equal(initialized.result.serverInfo.name, 'torch-agent-fleet');
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n`);
return {
request,
async close() {
child.stdin.end();
await new Promise((resolve) => {
const timeout = setTimeout(() => { child.kill('SIGTERM'); resolve(); }, 1000);
child.once('exit', () => { clearTimeout(timeout); resolve(); });
});
},
};
}
test('SCN-mcp-owner-reply-public-boundary: a bound MCP identity can reply only to its own owner request', async () => {
const fixture = installedFixture();
let control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
const request = control.sendOwnerRequest({ actorId: 'owner', recipient: fixture.worker, body: 'Report the durable outcome.' });
control.close();
const workerMcp = await connectMcp({ ...fixture, actorId: fixture.worker });
try {
const listed = await workerMcp.request('tools/list', {});
const replyTool = listed.result.tools.find((tool) => tool.name === 'torch_reply_to_owner');
assert.ok(replyTool);
assert.deepEqual(Object.keys(replyTool.inputSchema.properties).sort(), ['body', 'request_id']);
assert.equal(replyTool.inputSchema.additionalProperties, false);
assert.equal(listed.result.tools.some((tool) => /owner.*conversation|history/i.test(tool.name)), false);
const replied = await workerMcp.request('tools/call', {
name: 'torch_reply_to_owner', arguments: { request_id: request.id, body: 'The durable outcome is ready.' },
});
assert.equal(replied.result.isError, undefined);
const reply = JSON.parse(replied.result.content[0].text);
control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
const conversation = control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 });
assert.deepEqual(conversation.replies.map((message) => message.id), [reply.id]);
assert.equal(conversation.replies[0].references.replyTo, request.id);
const audit = control.readAudit().find((event) => event.operation === 'message.reply-owner' && event.entityId === reply.id);
assert.deepEqual(audit && { actorId: audit.actorId, details: audit.details }, { actorId: fixture.worker, details: { requestId: request.id } });
const before = { audit: control.readAudit().length, replies: conversation.replies.length };
control.close();
const schemaRefusal = await workerMcp.request('tools/call', {
name: 'torch_reply_to_owner',
arguments: { request_id: request.id, body: 'Attempt to select a sender.', sender: fixture.other },
});
assert.equal(schemaRefusal.result.isError, true);
control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
assert.equal(control.readAudit().length, before.audit);
assert.equal(control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 }).replies.length, before.replies);
control.close();
} finally {
await workerMcp.close();
}
control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
const beforeWrongActor = {
audit: control.readAudit().length,
replies: control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 }).replies.length,
};
control.close();
const otherMcp = await connectMcp({ ...fixture, actorId: fixture.other });
try {
const wrongActor = await otherMcp.request('tools/call', {
name: 'torch_reply_to_owner', arguments: { request_id: request.id, body: 'A valid identity cannot reply for another agent.' },
});
assert.equal(wrongActor.result.isError, true);
assert.match(wrongActor.result.content[0].text, /owner request addressed to the sender/i);
} finally {
await otherMcp.close();
}
control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
assert.equal(control.readAudit().length, beforeWrongActor.audit);
assert.equal(control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 }).replies.length, beforeWrongActor.replies);
control.close();
});
@@ -0,0 +1,91 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
function installedFixture({ projectId = 'owner-reply-fixture', sequencePrefix = 'main' } = {}) {
const root = mkdtempSync(join(tmpdir(), 'torch-owner-reply-'));
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'index.js'), 'export const fixture = true;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.review = { status: 'approved', reviewedAt: '2026-10-03T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
const env = { ...process.env, XDG_DATA_HOME: join(root, '.data') };
installProject({ repository, proposal, env, projectId });
let sequence = 0;
let seconds = 0;
return {
root, env,
worker: proposal.domains.find((domain) => domain.id !== 'qa')?.id ?? proposal.domains[0].id,
options: {
repositoryRoot: root, env,
idFactory: () => `${sequencePrefix}-${++sequence}`,
clock: () => new Date(Date.UTC(2026, 9, 3, 12, 0, seconds++)),
},
};
}
test('SCN-owner-addressed-reply-durability: a registered agent replies to its owner request with durable attribution', () => {
const fixture = installedFixture();
let control = openControlPlane(fixture.options);
const request = control.sendOwnerRequest({ actorId: 'owner', recipient: fixture.worker, body: 'Report the persisted outcome.' });
const first = control.sendOwnerReply({ sender: fixture.worker, requestId: request.id, body: 'The durable outcome is ready.' });
const second = control.sendOwnerReply({ sender: fixture.worker, requestId: request.id, body: 'The evidence remains bounded.' });
control.close();
control = openControlPlane(fixture.options);
const firstPage = control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 1 });
assert.equal(firstPage.request.id, request.id);
assert.deepEqual(firstPage.replies.map((message) => message.id), [first.id]);
assert.equal(firstPage.replies[0].recipient, 'owner');
assert.equal(firstPage.replies[0].references.replyTo, request.id);
assert.equal(firstPage.page.truncated, true);
const secondPage = control.readOwnerConversation({ actorId: 'owner', requestId: request.id, cursor: firstPage.page.nextCursor, limit: 1 });
assert.deepEqual(secondPage.replies.map((message) => message.id), [second.id]);
assert.equal(secondPage.page.complete, true);
assert.equal(secondPage.page.nextCursor, null);
const audit = control.readAudit().find((event) => event.operation === 'message.reply-owner' && event.entityId === first.id);
assert.deepEqual(audit && { actorId: audit.actorId, details: audit.details }, { actorId: fixture.worker, details: { requestId: request.id } });
control.close();
});
test('SCN-owner-addressed-reply-refusal: forged owner recipients, senders, and references leave durable state untouched', () => {
const fixture = installedFixture();
const foreign = installedFixture({ projectId: 'foreign-owner-reply-fixture', sequencePrefix: 'foreign' });
const control = openControlPlane(fixture.options);
const request = control.sendOwnerRequest({ actorId: 'owner', recipient: fixture.worker, body: 'Respond only from your registered identity.' });
const foreignControl = openControlPlane(foreign.options);
const foreignRequest = foreignControl.sendOwnerRequest({ actorId: 'owner', recipient: foreign.worker, body: 'Foreign request.' });
foreignControl.close();
const beforeAudit = control.readAudit().length;
const beforeMessages = control.readMessages({ recipient: fixture.worker }).length;
const beforeConversation = control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 });
assert.deepEqual(beforeConversation.replies, []);
assert.throws(() => control.sendMessage({ sender: fixture.worker, recipient: 'owner', body: 'Bypass the reply boundary.' }), (error) => error.code === 'UNKNOWN_FLEET_IDENTITY');
assert.throws(() => control.sendOwnerReply({ sender: 'owner', requestId: request.id, body: 'Spoofed sender.' }), (error) => error.code === 'UNKNOWN_FLEET_IDENTITY');
assert.throws(() => control.sendOwnerReply({ sender: 'session-manager', requestId: request.id, body: 'Valid identity, wrong addressee.' }), (error) => error.code === 'OWNER_REPLY_REFERENCE_INVALID');
assert.throws(() => control.sendOwnerReply({ sender: fixture.worker, requestId: foreignRequest.id, body: 'Foreign reference.' }), (error) => error.code === 'OWNER_REPLY_REFERENCE_INVALID');
assert.throws(() => control.sendOwnerReply({ sender: fixture.worker, requestId: 'forged-message-id', body: 'Forged reference.' }), (error) => error.code === 'OWNER_REPLY_REFERENCE_INVALID');
assert.throws(() => control.readOwnerConversation({ actorId: fixture.worker, requestId: request.id, limit: 1 }), (error) => error.code === 'OWNER_AUTHORITY_REQUIRED');
assert.equal(control.readAudit().length, beforeAudit);
assert.equal(control.readMessages({ recipient: fixture.worker }).length, beforeMessages);
const afterConversation = control.readOwnerConversation({ actorId: 'owner', requestId: request.id, limit: 10 });
assert.deepEqual(afterConversation.replies, []);
assert.deepEqual(afterConversation.page, beforeConversation.page);
const direct = control.sendMessage({ sender: 'session-manager', recipient: fixture.worker, body: 'Existing direct delivery.' });
const broadcast = control.sendMessage({ sender: fixture.worker, recipient: 'all', body: 'Existing broadcast delivery.' });
assert.deepEqual(control.readMessages({ recipient: fixture.worker }).slice(-2).map((message) => message.id), [direct.id, broadcast.id]);
control.close();
});
+163
View File
@@ -0,0 +1,163 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { chmodSync, existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import test from 'node:test';
import { openControlPlane, openControlPlaneReadOnly } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { projectStatePath } from '../../src/kernel/paths.mjs';
import { setupProject } from '../../src/kernel/setup.mjs';
const now = () => new Date('2026-10-03T10:00:00Z');
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-read-only-control-plane-'));
const git = (args) => execFileSync('git', ['-C', root, ...args], {
env: { ...process.env, GIT_AUTHOR_DATE: '2026-10-03T10:00:00Z', GIT_COMMITTER_DATE: '2026-10-03T10:00:00Z' },
});
git(['init', '-b', 'main']);
git(['config', 'user.email', '[email protected]']);
git(['config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'app.js'), 'export const value = 1;\n');
git(['add', '.']);
git(['commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.review = { status: 'approved', reviewedAt: now().toISOString(), reviewedBy: 'owner', notes: [] };
const env = { ...process.env, XDG_DATA_HOME: mkdtempSync(join(tmpdir(), 'torch-read-only-state-')) };
const projectId = 'read-only-fixture';
installProject({ repository, proposal, env, projectId });
setupProject({ repository: inspectRepository(root), env, parentOverride: join(tmpdir(), `${basename(root)}-trees`), authorized: true });
const writer = openControlPlane({ repositoryRoot: root, env, clock: now });
const worker = proposal.domains[0].id;
writer.reportStatus({ areaId: worker, state: 'idle', summary: 'registered v2 snapshot' });
writer.close();
const stateRoot = projectStatePath(projectId, env);
const stable = new DatabaseSync(join(stateRoot, 'state.db'));
stable.exec('PRAGMA journal_mode = DELETE');
stable.close();
return { root, env, worker, stateRoot };
}
function sidecars(stateRoot) {
return readdirSync(stateRoot).filter((name) => name === 'state.db-wal' || name === 'state.db-shm').sort();
}
test('SCN-read-only-v2-control-plane: a write-refusing state directory yields a stable v2 snapshot and rejects mutation', (t) => {
const context = fixture();
const statePath = join(context.stateRoot, 'state.db');
const before = readFileSync(statePath);
const beforeSidecars = sidecars(context.stateRoot);
assert.deepEqual(beforeSidecars, [], 'fixture must prove sidecar-free reader behavior');
chmodSync(statePath, 0o444);
chmodSync(context.stateRoot, 0o555);
t.after(() => {
chmodSync(context.stateRoot, 0o755);
chmodSync(statePath, 0o644);
});
const reader = openControlPlaneReadOnly({ repositoryRoot: context.root, env: context.env, clock: now });
assert.equal(reader.identity(context.worker).summary, 'registered v2 snapshot');
assert.deepEqual(reader.observeMessages({ recipient: context.worker, selection: 'unread', limit: 1 }), {
recipient: context.worker,
messages: [],
observation: { selection: 'unread', requestedLimit: 1, returnedCount: 0, complete: true, truncated: false, pendingUnreadCount: 0 },
});
assert.throws(
() => reader.reportStatus({ areaId: context.worker, state: 'working', summary: 'must not persist' }),
(error) => error.code === 'CONTROL_PLANE_READ_ONLY',
);
reader.close();
assert.deepEqual(readFileSync(statePath), before, 'the database bytes remain unchanged');
assert.deepEqual(sidecars(context.stateRoot), beforeSidecars, 'the reader creates no WAL or SHM sidecar');
});
test('SCN-read-only-v2-control-plane-race: a concurrent v2 writer produces one coherent snapshot or a typed refusal', () => {
const context = fixture();
const statePath = join(context.stateRoot, 'state.db');
const wal = new DatabaseSync(statePath);
wal.exec('PRAGMA journal_mode = WAL');
wal.close();
const writer = new DatabaseSync(statePath);
writer.exec('BEGIN IMMEDIATE');
writer.prepare('UPDATE identities SET summary = ?, updated_at = ? WHERE area_id = ?')
.run('writer committed after reader snapshot', '2026-10-03T10:01:00Z', context.worker);
let reader;
let refusal = null;
try { reader = openControlPlaneReadOnly({ repositoryRoot: context.root, env: context.env, clock: now }); } catch (error) { refusal = error; }
writer.exec('COMMIT');
writer.close();
if (refusal) {
assert.equal(refusal.code, 'CONTROL_PLANE_READ_ONLY_UNAVAILABLE');
return;
}
const observed = reader.identity(context.worker).summary;
assert.ok(observed === 'registered v2 snapshot' || observed === 'writer committed after reader snapshot', `reader must expose one whole SQLite snapshot, received ${observed}`);
reader.close();
});
test('SCN-read-only-pinned-correlated-snapshot: one reader cannot combine generations across an atomic writer commit', () => {
const context = fixture();
const statePath = join(context.stateRoot, 'state.db');
const generationA = '2026-10-03T10:00:00.000Z';
const generationB = '2026-10-03T10:01:00.000Z';
const correlated = [context.worker, 'session-manager'];
const seed = new DatabaseSync(statePath);
seed.exec('PRAGMA journal_mode = WAL');
const setGeneration = seed.prepare('UPDATE identities SET summary = ?, updated_at = ? WHERE area_id = ?');
for (const areaId of correlated) setGeneration.run('generation A', generationA, areaId);
seed.close();
const reader = openControlPlaneReadOnly({ repositoryRoot: context.root, env: context.env, clock: now });
const first = reader.identity(context.worker);
assert.deepEqual({ summary: first.summary, updatedAt: first.updatedAt }, { summary: 'generation A', updatedAt: generationA });
const writer = new DatabaseSync(statePath);
writer.exec('BEGIN IMMEDIATE');
const update = writer.prepare('UPDATE identities SET summary = ?, updated_at = ? WHERE area_id = ?');
for (const areaId of correlated) update.run('generation B', generationB, areaId);
writer.exec('COMMIT');
writer.close();
let second;
try { second = reader.identity('session-manager'); } catch (error) { assert.equal(error.code, 'CONTROL_PLANE_READ_ONLY_UNAVAILABLE'); }
if (second) {
assert.deepEqual({ summary: second.summary, updatedAt: second.updatedAt }, { summary: 'generation A', updatedAt: generationA }, 'the same reader must not combine generations');
}
reader.close();
const fresh = openControlPlaneReadOnly({ repositoryRoot: context.root, env: context.env, clock: now });
assert.deepEqual(
Object.fromEntries(correlated.map((areaId) => {
const identity = fresh.identity(areaId);
return [areaId, { summary: identity.summary, updatedAt: identity.updatedAt }];
})),
Object.fromEntries(correlated.map((areaId) => [areaId, { summary: 'generation B', updatedAt: generationB }])),
);
fresh.close();
});
test('SCN-read-only-v2-control-plane-schema: unknown schema revisions are refused instead of guessed', () => {
const context = fixture();
const statePath = join(context.stateRoot, 'state.db');
const writer = new DatabaseSync(statePath);
writer.exec('PRAGMA user_version = 3');
writer.close();
assert.ok(existsSync(statePath));
assert.throws(
() => openControlPlaneReadOnly({ repositoryRoot: context.root, env: context.env }),
(error) => error.code === 'CONTROL_PLANE_READ_ONLY_UNAVAILABLE',
);
});
test('SCN-read-only-v2-control-plane-input-drift: tracked identity inputs changing after snapshot make reads unavailable', () => {
const context = fixture();
const rosterPath = join(context.root, '.torch', 'roster.yaml');
const original = readFileSync(rosterPath, 'utf8');
const reader = openControlPlaneReadOnly({ repositoryRoot: context.root, env: context.env });
writeFileSync(rosterPath, `${original}\n`);
assert.throws(() => reader.identity(context.worker), (error) => error.code === 'CONTROL_PLANE_READ_ONLY_UNAVAILABLE');
writeFileSync(rosterPath, original);
reader.close();
});