Merge commit '5e7db85bee1b07108ee03c49a2ba040d20cf55c4' into torch/work-integration

# Conflicts:
#	docs/TEST_INTEGRITY.md
This commit is contained in:
thePR0M3TH3AN committed 2026-10-03 16:51:24 -04:00
commit d96f252098
37 files changed
+3436 -53

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.
+348
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
@@ -2755,6 +2922,55 @@ test_integrity_note:
if_true_explain_spec_basis: ""
```
# Console attention advisory cohorts (2026-10-01)
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-attention-advisory-cohort-cardinality-and-exact-references
given: "A fixed 49-task snapshot with equivalent stale or missing observed-commit findings in doctor and backlogHealth"
when: "The read-only attention projection groups those findings"
then: "Exactly one separately labelled advisory/recheck cohort contains the exact 49 task references and retains each owner, observedAt, current observed commit, and UNKNOWN reproduction status"
- id: SCN-attention-advisory-provenance-and-semantic-conflicts
given: "Doctor and backlogHealth findings that either match or differ in task, code, owner, observedAt, current observed commit, or reproduction status"
when: "The cohort uses the complete semantic fingerprint"
then: "Equal findings retain both provenance sources once; any differing fingerprint field remains a separate visible observation"
- id: SCN-attention-advisory-does-not-hide-owner-or-fleet-risks
given: "An advisory warning alongside an unsafe error, schema error, unknown wake, delivery operation, and owner-addressed pending decision"
when: "Attention is projected"
then: "Only the intended stale/missing warnings enter Advisory; the owner decision remains Owner action and all unsafe, schema, wake, and delivery findings remain Fleet action"
- id: SCN-attention-advisory-projection-is-read-only
given: "A frozen snapshot containing an advisory finding and a revisioned owner decision preview boundary"
when: "The attention projection runs"
then: "Snapshot, decision evidence, preview token, revision, and draft remain unchanged; projection invokes no acknowledgement, close, recovery, or decision action"
- id: SCN-attention-advisory-unknown-fields-stay-unknown
given: "A stale/missing observation whose owner, observedAt, and current commit provenance are absent"
when: "The advisory drilldown is formed"
then: "The task reference remains visible while missing owner and provenance remain null and reproduction is labelled UNKNOWN"
observable_outcomes:
- "One 49-reference advisory cohort replaces duplicated stale/missing attention cards while preserving exact drilldown references"
- "A populated advisory group remains enumerable and JSON-serializable; an empty projection preserves its original owner/fleet/arbiter object shape"
- "Each deduplicated observation retains doctor and backlogHealth provenance; conflicting records are never merged by code alone"
- "Owner decisions, unsafe recovery, schema errors, unknown wakes, delivery operations, and same-code errors remain in their existing action groups"
- "Observed source/revision displays the exact observedAt SHA without presenting it as a clock timestamp"
- "Projection does not mutate snapshot data or invoke state-changing actions, and existing owner quick-action preview/revision/draft assertions remain unchanged"
- "Source/scenario qualification is not actual live desktop/mobile visual acceptance"
determinism_controls:
- "Fixed hermetic task IDs, owner values, observedAt values, observed commit, finding codes, and provenance inputs"
- "Deep-frozen projection snapshots and an isolated demo serving one fixed in-memory snapshot; no existing fixture source, network, live project mutation, retries, or sleeps"
anti_cheat_rationale:
prevents:
- "Hard-coded cohort counts that omit or invent task references"
- "Deduplication by code alone that merges conflicting observations"
- "Discarding one source while presenting a deduplicated observation"
- "Hiding unsafe errors or owner decisions inside advisory summaries"
- "Changing preview, revision, draft, or acknowledgement behavior while rendering evidence"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Candidate source B2 authenticated-attempt spec correction (2026-10-01)
```yaml
@@ -2795,3 +3011,135 @@ test_integrity_note:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Compact Console Overview density (2026-10-01)
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-console-overview-owner-decisions-and-urgent-hazards-lead-with-distinct-counts
given: "An owner-addressed pending decision, unsafe Fleet findings, a manager-wake reservation, and advisory evidence"
when: "The Console renders its Overview attention summary"
then: "Owner decisions and urgent hazards lead, exact owner/action/advisory counts stay distinct, and only the guarded owner decision shortcuts appear"
- id: SCN-console-overview-clean-unique-commits-stay-informational-and-unsafe-workstays-reviewable
given: "One clean branch-ahead worktree and a separate dirty worktree"
when: "The Overview projects worktree evidence"
then: "Unique commits remain informational and inspectable while dirty work remains a named review finding"
- id: SCN-console-overview-crowded-preview-preserves-every-fleet-and-advisory-record-at-desktop-and-mobile
given: "A fixed 49-reference advisory cohort with one conflicting observation and 17 Fleet findings including unknown ownership and long evidence"
when: "The Overview is measured at 1440x1000 and 390x844 and View all Fleet opens the full queue"
then: "Shown/total/remaining counts are exact, urgent findings stay visible, every Fleet record and all advisory provenance/conflicts/unknowns remain reachable, and no horizontal overflow or clipped record occurs"
- id: SCN-console-overview-refresh-and-navigation-preserve-owner-drafts-without-adding-unsafe-actions
given: "An unsent task draft and a pending owner decision in the isolated demo"
when: "The owner navigates away and refreshes the Console"
then: "The draft remains intact, the owner shortcut boundary remains explicit, and no acknowledgement, close, recovery, or non-owner decision action is introduced"
- id: SCN-console-overview-retains-ten-destinations-keyboard-routing-and-manager-wake-deep-links
given: "The ten existing Console view destinations and an urgent manager-wake finding on a mobile viewport"
when: "The owner uses keyboard navigation and opens the manager-wakes route directly and through browser history"
then: "Each route selects its owning view, nested targets receive focus, Back/Forward remain coherent, and the urgent manager-wakes target is reachable"
observable_outcomes:
- "Owner and urgent counts are separate from advisory counts and clean unique commits do not inflate hazards"
- "A bounded Fleet preview states exact shown, total, and remaining values and links to every original Fleet record"
- "The full queue retains long evidence, conflicting advisory observations, exact task references, source provenance, and unknown ownership"
- "The ten existing destinations, guarded owner action, drafts, mobile keyboard focus, and manager-wakes deep link remain observable"
- "Fixed crowded data has no horizontal overflow or clipped urgent/evidence content at 1440x1000 and 390x844"
determinism_controls:
- "Hermetic loopback Console server with the isolated in-memory demo snapshot replaced by fixed task, owner, finding, evidence, and commit values"
- "Fixed Playwright viewport dimensions and deterministic route/history actions; no live project mutation, external service, retry, or sleep"
anti_cheat_rationale:
prevents:
- "Hard-coded preview/count labels that omit original findings"
- "Hiding urgent or unknown-owner evidence behind advisory totals"
- "Classifying clean unique commits as unsafe work"
- "Losing conflicting source observations or inventing provenance"
- "Adding owner actions that bypass the existing decision preview boundary"
relaxation:
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: ""
```
+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.
+100 -9
View File
@@ -12,9 +12,50 @@ function worktreeProblemLabel(problem) {
function attentionGroups(snapshot) {
const groups = { owner: [], fleet: [], arbiter: [] };
const agents = new Map((snapshot.agents ?? []).map((agent) => [agent.areaId, agent]));
const tasks = new Map((snapshot.backlog ?? []).map((task) => [task.id, task]));
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']);
const rechecks = new Map();
const recheckValue = (finding, key, fallback) => Object.hasOwn(finding, key) && finding[key] !== undefined
? finding[key] : fallback;
const addRecheck = (finding, source) => {
if (finding.severity !== 'warning' || !recheckCodes.has(finding.code)) return;
const taskId = recheckValue(finding, 'taskId', null);
const task = tasks.get(taskId);
const observation = {
taskId,
code: finding.code,
owner: recheckValue(finding, 'owner', task?.owner ?? null),
observedAt: recheckValue(finding, 'observedAt', task?.observedAt ?? null),
currentObservedCommit: recheckValue(finding, 'currentObservedCommit',
snapshot.repository?.head ?? snapshot.project?.head ?? null),
reproductionStatus: finding.reproductionStatus ?? finding.reproductionState ?? 'UNKNOWN',
sources: [],
};
const fingerprint = JSON.stringify([
observation.taskId, observation.code, observation.owner, observation.observedAt,
observation.currentObservedCommit, observation.reproductionStatus,
]);
const retained = rechecks.get(fingerprint) ?? observation;
if (!retained.sources.includes(source)) retained.sources.push(source);
rechecks.set(fingerprint, retained);
};
for (const approval of approvals) {
const isOwner = approval.approver === 'owner';
@@ -76,7 +117,8 @@ function attentionGroups(snapshot) {
}
for (const finding of findings.filter((entry) => ['error', 'warning'].includes(entry.severity)
&& !['MESSAGE_BACKLOG', 'WORKTREE_PROBLEM'].includes(entry.code))) {
&& !['MESSAGE_BACKLOG', 'WORKTREE_PROBLEM'].includes(entry.code)
&& !(entry.severity === 'warning' && recheckCodes.has(entry.code)))) {
const recoveryGap = finding.code === 'RECOVERABILITY' && !finding.offMachine;
const humanTitle = recoveryGap
? 'Off-machine recovery copy is not verified'
@@ -136,16 +178,65 @@ function attentionGroups(snapshot) {
evidence: 'Review the recorded task activity before asking for a change.', href: '#activity-review', action: 'Open activity review',
});
const staleEvidence = (snapshot.backlogHealth?.findings ?? []).filter((finding) =>
['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING'].includes(finding.code));
if (staleEvidence.length) add('fleet', {
tone: 'review', title: `${staleEvidence.length} task evidence record${staleEvidence.length === 1 ? '' : 's'} need refresh`,
owner: 'Task owner not recorded', detail: 'Some backlog evidence refers to an old or unreachable commit. Re-check it before dispatch.',
evidence: staleEvidence.map((finding) => `${finding.taskId ?? 'task not recorded'} · ${finding.code}`).join('; '),
href: '#backlog-board', action: 'Inspect backlog evidence',
for (const finding of findings) addRecheck(finding, 'doctor');
for (const finding of snapshot.backlogHealth?.findings ?? []) addRecheck(finding, 'backlogHealth');
const observations = [...rechecks.values()].sort((left, right) => {
const leftKey = JSON.stringify([left.taskId, left.code, left.owner, left.observedAt,
left.currentObservedCommit, left.reproductionStatus]);
const rightKey = JSON.stringify([right.taskId, right.code, right.owner, right.observedAt,
right.currentObservedCommit, right.reproductionStatus]);
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
});
if (observations.length) {
const taskReferences = [...new Set(observations.map((item) => item.taskId).filter((id) => id != null))];
const countLabel = taskReferences.length === observations.length
? `${taskReferences.length} task evidence recheck${taskReferences.length === 1 ? '' : 's'}`
: `${observations.length} observed evidence record${observations.length === 1 ? '' : 's'} to recheck`;
advisory.push({
tone: 'info', title: countLabel, owner: 'Advisory recheck',
detail: 'Observed backlog evidence may refer to an old or unreachable commit. Recheck each observation before dispatch; reproduction remains UNKNOWN until independently verified.',
evidence: `${observations.length} distinct observation${observations.length === 1 ? '' : 's'} · ${taskReferences.length} recorded task reference${taskReferences.length === 1 ? '' : 's'}`,
href: '#backlog-board', action: 'Inspect backlog evidence',
observations, taskReferences,
});
groups.advisory = advisory;
}
return groups;
}
global.TorchAttentionProjection = Object.freeze({ groups: attentionGroups });
function attentionSummary(groups) {
const fleet = groups.fleet ?? [];
const owner = groups.owner ?? [];
const arbiter = groups.arbiter ?? [];
const advisory = groups.advisory ?? [];
const actionableFleet = fleet.filter((item) => item.tone !== 'info');
const advisoryReferences = advisory.reduce((count, item) => count
+ (Array.isArray(item.taskReferences) ? item.taskReferences.length : item.observations?.length ?? 0), 0);
return {
ownerDecisions: owner.length,
urgentHazards: fleet.filter((item) => item.tone === 'urgent').length,
actionable: owner.length + actionableFleet.length + arbiter.filter((item) => item.tone !== 'info').length,
fleetTotal: fleet.length,
fleetInformational: fleet.length - actionableFleet.length,
advisoryCohorts: advisory.length,
advisoryReferences,
};
}
function fleetPreview(groups, additionalLimit = 3) {
const fleet = groups.fleet ?? [];
const urgent = fleet.filter((item) => item.tone === 'urgent');
const remaining = fleet.filter((item) => item.tone !== 'urgent')
.sort((left, right) => {
const rank = { decision: 0, review: 1, info: 2 };
return (rank[left.tone] ?? 3) - (rank[right.tone] ?? 3);
});
return [...urgent, ...remaining.slice(0, Math.max(0, additionalLimit))];
}
global.TorchAttentionProjection = Object.freeze({
groups: attentionGroups,
summary: attentionSummary,
fleetPreview,
});
})(globalThis);
+19 -1
View File
@@ -67,8 +67,17 @@
</div>
</div>
<aside class="attention-panel" aria-labelledby="attention-title">
<div class="panel-heading"><h2 id="attention-title">Needs your attention</h2><span id="attention-count">0 items</span></div>
<div class="panel-heading"><h2 id="attention-title">Decisions and risks</h2><span id="attention-count">0 actionable</span></div>
<div class="attention-counts" aria-label="Actionable and advisory counts">
<p><strong id="owner-decision-count">0 owner decisions</strong><span>Only decisions addressed to you</span></p>
<p><strong id="urgent-hazard-count">0 urgent hazards</strong><span>Unsafe or lost-work evidence</span></p>
<p><strong id="advisory-count">0 advisory cohorts</strong><span>Rechecks are not owner actions</span></p>
</div>
<div id="attention-list" class="attention-list"></div>
<div class="attention-footer">
<span id="fleet-preview-count" role="status" aria-live="polite">Showing 0 of 0 Fleet findings; 0 remaining</span>
<a href="#fleet-attention-queue">View all Fleet</a>
</div>
</aside>
</section>
@@ -174,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">
@@ -199,6 +209,14 @@
</article>
<article class="work-surface"><div class="panel-heading"><h3>Ownership</h3><span>Implementation stays with specialists</span></div><div id="ownership-list" class="console-list"></div></article>
</div>
<section id="fleet-attention-queue" class="fleet-attention-queue" aria-labelledby="fleet-attention-title" tabindex="-1">
<div class="section-heading"><div><p class="section-kicker">Every finding remains inspectable</p><h3 id="fleet-attention-title">Fleet review queue</h3></div><span>Full evidence and provenance</span></div>
<label class="fleet-attention-filter" for="fleet-attention-filter">Filter Fleet findings
<input id="fleet-attention-filter" type="search" autocomplete="off" placeholder="Search owner, task, evidence, or reference">
</label>
<p id="fleet-attention-result-count" class="fleet-attention-result-count" role="status" aria-live="polite">Loading findings…</p>
<div id="fleet-attention-list" class="fleet-attention-list"></div>
</section>
</section>
<section id="organization" class="console-section organization-section" data-console-view="organization" aria-labelledby="organization-title">
+86 -26
View File
@@ -954,38 +954,97 @@ function renderFlowWatch(snapshot) {
function attentionGroups(snapshot) {
return globalThis.TorchAttentionProjection.groups(snapshot);
}
function attentionSummary(groups) {
return globalThis.TorchAttentionProjection.summary(groups);
}
function renderAttentionItem(item) {
const severity = ({ urgent: 'Urgent', review: 'Review', decision: 'Decision required', info: 'Progress' })[item.tone] ?? 'Unknown';
return `<article class="attention-item tone-${escapeHtml(item.tone)}" data-attention-record>
<span class="attention-mark" aria-hidden="true"></span><div class="attention-copy">
<strong>${escapeHtml(item.title)}</strong><p>${escapeHtml(item.detail)}</p>
<small>Severity: ${escapeHtml(severity)} · Owner: ${escapeHtml(item.owner)}${item.waitSince ? ` · Waiting ${escapeHtml(ageLabel(item.waitSince))}` : ''}</small>
${item.evidence.length > 120
? `<details class="attention-evidence"><summary>Evidence and references</summary><p>${escapeHtml(item.evidence)}</p></details>`
: `<small class="attention-evidence-short">Evidence: ${escapeHtml(item.evidence)}</small>`}
${item.observations ? `<details class="attention-cohort-details"><summary>Recheck observations (${item.observations.length})</summary><ul>${item.observations.map((observation) => `<li>
<code>${escapeHtml(observation.taskId ?? 'Task reference not recorded')}</code> · ${escapeHtml(observation.code)}
· Owner: ${escapeHtml(observation.owner ?? 'Owner not recorded')}
· Observed source/revision: ${escapeHtml(observation.observedAt ?? 'Not recorded')}
· Current observed commit: ${escapeHtml(observation.currentObservedCommit ?? 'Not recorded')}
· Reproduction: ${escapeHtml(observation.reproductionStatus ?? 'UNKNOWN')}
· Sources: ${escapeHtml(observation.sources.join(', ') || 'Not recorded')}
</li>`).join('')}</ul></details>` : ''}
<a href="${escapeHtml(item.href)}">${escapeHtml(item.action)}</a>
${item.decisionApprovalId ? `<div class="attention-quick-actions" role="group" aria-label="Owner decision shortcuts for ${escapeHtml(item.title)}">
<button type="button" data-attention-decision="approved" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Approve</button>
<button type="button" class="quiet-action" data-attention-decision="rejected" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Reject</button>
<p class="attention-action-status" role="status" aria-live="polite" hidden></p>
</div>` : ''}
${item.requestOwner ? `<a href="#owner-request-form" data-attention-recipient="${escapeHtml(item.requestOwner)}">Request review from ${escapeHtml(item.owner)}</a>` : ''}
</div></article>`;
}
function renderAttentionGroup(key, title, description, items, idPrefix) {
const headingId = `${idPrefix}-${key}-title`;
return `<section class="attention-group attention-group-${key}" aria-labelledby="${headingId}">
<div class="attention-group-heading"><h3 id="${headingId}">${title}</h3><span>${items.length}</span></div>
${items.length ? items.map(renderAttentionItem).join('') : `<p class="attention-empty">${escapeHtml(description)}</p>`}
</section>`;
}
function applyFleetAttentionFilter(query = '') {
const list = $('#fleet-attention-list');
if (!list) return;
const normalized = query.trim().toLocaleLowerCase();
const records = [...list.querySelectorAll('[data-attention-record]')];
let visible = 0;
for (const record of records) {
const matches = !normalized || record.textContent.toLocaleLowerCase().includes(normalized);
record.hidden = !matches;
if (matches) visible += 1;
}
for (const group of list.querySelectorAll('.attention-group')) {
group.hidden = ![...group.querySelectorAll('[data-attention-record]')].some((record) => !record.hidden);
}
setText('#fleet-attention-result-count', normalized
? `${visible} of ${records.length} findings match.`
: `${records.length} Fleet and advisory findings available.`);
}
function renderAttention(groups) {
const definitions = [
['owner', 'Waiting on you', 'Only requests that name the project owner as decision-maker.'],
['fleet', 'Fleet handling', 'A named domain owns the next step; unknown ownership stays explicit.'],
['advisory', 'Advisory / recheck cohorts', 'Observed evidence grouped for rechecking; these findings do not assign an owner action.'],
['arbiter', 'Arbiter handling', 'No observed item has structured evidence assigning its next step exclusively to an arbiter.'],
];
const total = Object.values(groups).reduce((sum, items) => sum + items.length, 0);
setText('#attention-count', `${total} ${total === 1 ? 'item' : 'items'}`);
setHtml('#attention-list', definitions.map(([key, title, description]) => {
const items = groups[key];
return `<section class="attention-group attention-group-${key}" aria-labelledby="attention-${key}-title">
<div class="attention-group-heading"><h3 id="attention-${key}-title">${title}</h3><span>${items.length}</span></div>
${items.length ? items.map((item) => `<article class="attention-item tone-${escapeHtml(item.tone)}">
<span class="attention-mark" aria-hidden="true"></span><div class="attention-copy">
<strong>${escapeHtml(item.title)}</strong><p>${escapeHtml(item.detail)}</p>
<small>Severity: ${escapeHtml(({ urgent: 'Urgent', review: 'Review', decision: 'Decision required', info: 'Progress' })[item.tone] ?? 'Unknown')} · Owner: ${escapeHtml(item.owner)}${item.waitSince ? ` · Waiting ${escapeHtml(ageLabel(item.waitSince))}` : ''}</small>
${item.evidence.length > 120
? `<details class="attention-evidence"><summary>Evidence and references</summary><p>${escapeHtml(item.evidence)}</p></details>`
: `<small class="attention-evidence-short">Evidence: ${escapeHtml(item.evidence)}</small>`}
<a href="${escapeHtml(item.href)}">${escapeHtml(item.action)}</a>
${item.decisionApprovalId ? `<div class="attention-quick-actions" role="group" aria-label="Owner decision shortcuts for ${escapeHtml(item.title)}">
<button type="button" data-attention-decision="approved" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Approve</button>
<button type="button" class="quiet-action" data-attention-decision="rejected" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Reject</button>
<p class="attention-action-status" role="status" aria-live="polite" hidden></p>
</div>` : ''}
${item.requestOwner ? `<a href="#owner-request-form" data-attention-recipient="${escapeHtml(item.requestOwner)}">Request review from ${escapeHtml(item.owner)}</a>` : ''}
</div></article>`).join('') : `<p class="attention-empty">${escapeHtml(description)}</p>`}
</section>`;
}).join(''));
const summary = attentionSummary(groups);
const fleetPreview = globalThis.TorchAttentionProjection.fleetPreview(groups);
const remaining = Math.max(0, summary.fleetTotal - fleetPreview.length);
const priorQuery = $('#fleet-attention-filter')?.value ?? '';
setText('#attention-count', `${summary.actionable} actionable`);
setText('#owner-decision-count', `${summary.ownerDecisions} owner decision${summary.ownerDecisions === 1 ? '' : 's'}`);
setText('#urgent-hazard-count', `${summary.urgentHazards} urgent hazard${summary.urgentHazards === 1 ? '' : 's'}`);
setText('#advisory-count', summary.advisoryReferences
? `${summary.advisoryCohorts} cohort${summary.advisoryCohorts === 1 ? '' : 's'} · ${summary.advisoryReferences} task references`
: `${summary.advisoryCohorts} advisory cohort${summary.advisoryCohorts === 1 ? '' : 's'}`);
setText('#fleet-preview-count', `Showing ${fleetPreview.length} of ${summary.fleetTotal} Fleet findings; ${remaining} remaining`);
setHtml('#attention-list', definitions.map(([key, title, description]) => renderAttentionGroup(
key, title, description, key === 'fleet' ? fleetPreview : groups[key] ?? [], 'overview-attention',
)).join(''));
setHtml('#fleet-attention-list', [
renderAttentionGroup('fleet', 'Fleet handling', 'No Fleet-owned findings are recorded.', groups.fleet ?? [], 'fleet-queue'),
renderAttentionGroup('advisory', 'Advisory / recheck cohorts', 'No advisory cohorts are recorded.', groups.advisory ?? [], 'fleet-queue'),
].join(''));
const filter = $('#fleet-attention-filter');
if (filter) filter.value = priorQuery;
applyFleetAttentionFilter(priorQuery);
}
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 ?? [];
@@ -1012,9 +1071,9 @@ function render(snapshot) {
const parts = [`${agents.length} persistent ${agents.length === 1 ? 'identity' : 'identities'}`,
`${working} ${working === 1 ? 'task' : 'tasks'} in progress`];
if (blocked) parts.push(`${blocked} blocked`);
const attentionCount = Object.values(attention).reduce((sum, items) => sum + items.length, 0);
const attentionCounts = attentionSummary(attention);
setText('#project-summary', snapshot.mode === 'installed'
? `${parts.join(' · ')}. ${attentionCount ? `${attentionCount} item${attentionCount === 1 ? '' : 's'} need a named next step.` : 'No owner decision is currently waiting in the observed queues.'}`
? `${parts.join(' · ')}. ${attentionCounts.ownerDecisions} owner decision${attentionCounts.ownerDecisions === 1 ? '' : 's'} · ${attentionCounts.urgentHazards} urgent hazard${attentionCounts.urgentHazards === 1 ? '' : 's'} · ${attentionCounts.actionable} actionable across the observed queues.`
: 'This repository has not been installed as a TORCH Fleet. The view is showing repository intake only.');
renderTaskCreateOptions(agents, tasks, snapshot.mode === 'installed');
renderOwnerRequestOptions(agents, tasks, snapshot.mode === 'installed');
@@ -1034,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.'));
@@ -2123,6 +2182,7 @@ $('#attention-list')?.addEventListener('click', (event) => {
});
$('#refresh-console')?.addEventListener('click', () => refresh());
$('#fleet-attention-filter')?.addEventListener('input', (event) => applyFleetAttentionFilter(event.currentTarget.value));
$('#toggle-live-refresh')?.addEventListener('click', () => {
liveUpdatesPaused = !liveUpdatesPaused;
$('#toggle-live-refresh').textContent = liveUpdatesPaused ? 'Resume updates' : 'Pause updates';
+28 -8
View File
@@ -166,30 +166,30 @@ footer div { display: flex; gap: 1.5rem; }
.task-reference { margin-top: .55rem; color: var(--muted); font-size: .75rem; overflow-wrap: anywhere; }
.task-reference code { display: inline-block; margin-top: .3rem; color: var(--ink); }
.console-error { margin: 1.2rem 0 0; background: var(--warning-surface); color: var(--warning-ink); border-left: 4px solid var(--signal); padding: 1rem; }
.pulse-zone { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(270px, .8fr); border-bottom: 1px solid var(--ink); }
.pulse-main { min-width: 0; padding: 2rem 2rem 1.6rem 0; }
.pulse-zone { display: grid; grid-template-columns: minmax(0, .82fr) minmax(0, 1.18fr); gap: clamp(1.25rem, 2.8vw, 2.6rem); border-bottom: 1px solid var(--ink); }
.pulse-main { min-width: 0; align-self: start; padding: 1.55rem 0 1.25rem; }
.pulse-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.25rem; }
.section-kicker { margin: 0 0 .35rem; color: var(--muted); font-size: .78rem; font-weight: 700; }
.pulse-heading h1, .section-heading h2 { max-width: none; font-size: clamp(1.75rem, 2.8vw, 2.5rem); line-height: 1.05; letter-spacing: -.035em; }
.health-flag { display: inline-flex; align-items: center; border: 1px solid var(--field); color: var(--field); padding: .25rem .55rem; font-size: .78rem; font-weight: 700; }
.pulse-summary { max-width: 60ch; margin: 1rem 0 1.65rem; color: var(--muted); font-size: 1.05rem; }
.pulse-summary { max-width: 50ch; margin: .75rem 0 1.15rem; color: var(--muted); font-size: .9rem; line-height: 1.5; }
.pulse-readouts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--line); }
.pulse-readouts div { min-width: 0; display: grid; align-content: start; gap: .3rem; padding: .8rem .8rem .2rem 0; }
.pulse-readouts div + div { padding-left: .8rem; border-left: 1px solid var(--line); }
.pulse-readouts span { color: var(--muted); font-size: .74rem; }
.pulse-readouts strong { overflow-wrap: anywhere; font-family: var(--display); font-size: .95rem; }
.attention-panel { padding: 1.45rem 0 1rem 1.35rem; border-left: 1px solid var(--line); }
.attention-panel { min-width: 0; align-self: start; padding: 1.15rem 0 1rem 1.2rem; border-left: 1px solid var(--line); }
.panel-heading, .section-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
.panel-heading { padding-bottom: .65rem; border-bottom: 1px solid var(--line); }
.panel-heading h2, .panel-heading h3 { margin: 0; font: 650 1rem/1.2 var(--display); }
.panel-heading span, .section-heading > span { color: var(--muted); font-size: .76rem; text-align: right; }
.attention-list { display: grid; gap: 1rem; }
.attention-group { min-width: 0; padding-top: .6rem; border-top: 1px solid var(--line); }
.attention-list { display: grid; gap: .6rem; }
.attention-group { min-width: 0; padding-top: .4rem; border-top: 1px solid var(--line); }
.attention-group-heading { display: flex; align-items: baseline; justify-content: space-between; gap: .8rem; }
.attention-group-heading h3 { margin: 0; font: 650 .9rem/1.25 var(--display); }
.attention-group-heading span, .attention-empty { color: var(--muted); font-size: .72rem; }
.attention-empty { margin: .35rem 0 0; line-height: 1.4; }
.attention-item { display: grid; grid-template-columns: 9px minmax(0, 1fr); gap: .7rem; padding: .75rem 0; border-bottom: 1px solid var(--line); }
.attention-item { display: grid; grid-template-columns: 8px minmax(0, 1fr); gap: .55rem; padding: .5rem 0; border-bottom: 1px solid var(--line); }
.attention-mark { width: 7px; height: 7px; margin-top: .45rem; background: var(--field); }
.tone-urgent .attention-mark { background: var(--signal); }
.tone-review .attention-mark { background: var(--route); }
@@ -197,7 +197,7 @@ footer div { display: flex; gap: 1.5rem; }
.tone-info .attention-mark { background: var(--field); }
.attention-copy { min-width: 0; }
.attention-item strong { display: block; font-size: .86rem; overflow-wrap: anywhere; }
.attention-item p { margin: .18rem 0 .35rem; color: var(--muted); font-size: .78rem; line-height: 1.4; }
.attention-item p { margin: .18rem 0 .35rem; color: var(--muted); font-size: .78rem; line-height: 1.4; overflow-wrap: anywhere; }
.attention-item small { display: block; color: var(--muted); font-size: .69rem; line-height: 1.35; overflow-wrap: anywhere; }
.attention-item a { display: inline-block; margin: .35rem .8rem 0 0; color: var(--route); font-size: .76rem; font-weight: 650; }
.attention-evidence { margin-top: .28rem; color: var(--muted); font-size: .69rem; }
@@ -207,6 +207,25 @@ footer div { display: flex; gap: 1.5rem; }
.attention-quick-actions button { min-height: 2rem; padding: .3rem .55rem; }
.attention-action-status { flex-basis: 100%; margin: .1rem 0 0; color: var(--muted); font-size: .7rem; line-height: 1.4; }
.attention-action-status[hidden] { display: none; }
.attention-counts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .45rem; padding: .6rem 0 .25rem; }
.attention-counts p { min-width: 0; margin: 0; padding-left: .55rem; border-left: 2px solid var(--line); }
.attention-counts p:first-child { border-left-color: #f3bd55; }
.attention-counts p:nth-child(2) { border-left-color: var(--signal); }
.attention-counts p:last-child { border-left-color: var(--field); }
.attention-counts strong, .attention-counts span { display: block; overflow-wrap: anywhere; }
.attention-counts strong { font-size: .74rem; line-height: 1.25; }
.attention-counts span { margin-top: .12rem; color: var(--muted); font-size: .65rem; line-height: 1.3; }
.attention-footer { display: flex; justify-content: space-between; align-items: baseline; gap: .75rem; padding-top: .6rem; color: var(--muted); font-size: .7rem; }
.attention-footer a { color: var(--route); font-weight: 700; white-space: nowrap; }
.fleet-attention-queue { min-width: 0; margin-top: 1.2rem; padding-top: 1.2rem; border-top: 1px solid var(--ink); scroll-margin-top: 1rem; }
.fleet-attention-filter { display: grid; gap: .3rem; max-width: 34rem; color: var(--muted); font-size: .75rem; font-weight: 650; }
.fleet-attention-filter input { width: 100%; min-width: 0; min-height: 2.5rem; padding: .45rem .6rem; border: 1px solid var(--line); border-radius: 0; background: var(--deep); color: var(--ink); font: 500 .82rem/1.3 var(--body); }
.fleet-attention-result-count { color: var(--muted); font-size: .75rem; }
.fleet-attention-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.1rem; }
.fleet-attention-list > .attention-group { padding: .7rem; border: 1px solid var(--line); background: var(--paper); }
.fleet-attention-list .attention-evidence[open] p { max-width: 80ch; }
.attention-cohort-details li, .attention-cohort-details code { overflow-wrap: anywhere; }
.fleet-attention-queue [hidden] { display: none !important; }
.board-section, .console-section { padding-block: 1.65rem; border-bottom: 1px solid var(--line); scroll-margin-top: 1rem; }
.section-heading { margin-bottom: 1rem; }
.section-heading h2 { margin: 0; }
@@ -629,6 +648,7 @@ footer div { display: flex; gap: 1.5rem; }
.console-workspace { padding-inline: .8rem; }
.console-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pulse-zone, .fleet-columns, .evidence-columns, .delivery-columns, .flow-watch-grid { grid-template-columns: 1fr; }
.fleet-attention-list { grid-template-columns: 1fr; }
.owner-request-fields, .runtime-profile-fields { grid-template-columns: 1fr; }
.schedule-launcher-actions { align-items: stretch; flex-direction: column; }
.lifecycle-heading { flex-direction: column; gap: .25rem; }
+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 } 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]
@@ -576,6 +580,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') {
+32 -1
View File
@@ -7,6 +7,7 @@ import { readInstallManifest } from '../kernel/install.mjs';
import { projectStatePath } from '../kernel/paths.mjs';
import { loadProjectConfig } from '../kernel/config.mjs';
import { organizationGraphFromConfig } from '../kernel/organization.mjs';
import { consumeStoppedExecutorEvidence } from '../runtime/stopped-executor-evidence.mjs';
export const PRESENCE_STATES = Object.freeze([
'starting', 'working', 'waiting', 'idle', 'stale', 'stopping', 'offline',
@@ -459,6 +460,36 @@ export class ControlPlane {
return { messageId: id, recipient: recipientId, acknowledgedAt };
}
recoverOwnerStoppedUnknownIdentity({ actorId, areaId, expectedIdentity, evidence } = {}) {
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 } = {}) {
const id = this.assertIdentity(areaId);
const normalizedState = requiredText(state, 'state');
@@ -751,7 +782,7 @@ export class ControlPlane {
auditOwnerAction({ actorId, operation, entityType, entityId = null, details = null } = {}) {
const actor = this.assertOwnerActor(actorId);
if (!['artifact.feedback', 'backlog.priority', 'backlog.create', 'agent.request', 'approval.decide', 'approval.notify', 'forge.sync', 'canonical.fetch', 'profile.change', 'schedule.launcher.install', 'schedule.launcher.reconcile', 'schedule.wake.recover', 'hierarchy.conclude-pilot', 'check.recover-prepared'].includes(operation)) {
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 },
});
+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);
}
}
}
@@ -0,0 +1,239 @@
import assert from 'node:assert/strict';
import { once } from 'node:events';
import test from 'node:test';
import { chromium } from '@playwright/test';
import { createConsoleServer } from '../../src/console/server.mjs';
import '../../site/attention-projection.js';
import '../../site/attention-actions.js';
const project = (snapshot) => globalThis.TorchAttentionProjection.groups(snapshot);
const taskId = (index) => `TASK-COHORT-${String(index).padStart(2, '0')}`;
function deepFreeze(value) {
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
for (const child of Object.values(value)) deepFreeze(child);
return Object.freeze(value);
}
function finding(task, source = {}) {
return {
severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE', taskId: task.id,
observedAt: task.observedAt, owner: task.owner, ...source,
};
}
function cohortFixture(count = 49) {
const tasks = Array.from({ length: count }, (_, index) => ({
id: taskId(index + 1), owner: `domain-${String(index + 1).padStart(2, '0')}`,
observedAt: String(index + 1).padStart(40, '0'),
}));
const findings = tasks.map((task, index) => finding(task, {
code: index % 2 ? 'BACKLOG_OBSERVED_COMMIT_MISSING' : 'BACKLOG_OBSERVED_COMMIT_STALE',
}));
return {
repository: { head: '316d8b7480c2e10925ebdff8310fe26ef5a881a8' },
backlog: tasks,
doctor: { findings: structuredClone(findings) },
backlogHealth: { findings: structuredClone(findings), mutationPerformed: false },
};
}
test('SCN-attention-advisory-cohort-cardinality-and-exact-references: 49 equivalent observations render as one recheck cohort', async (t) => {
const groups = project(cohortFixture());
const expectedReferences = Array.from({ length: 49 }, (_, index) => taskId(index + 1));
assert.equal(groups.advisory.length, 1);
assert.equal(groups.advisory[0].title, '49 task evidence rechecks');
assert.equal(groups.fleet.filter((item) => /BACKLOG_OBSERVED_COMMIT_(STALE|MISSING)/.test(item.title)).length, 0);
assert.deepEqual(groups.advisory[0].taskReferences, expectedReferences);
assert.equal(groups.advisory[0].observations.length, 49);
assert.ok(Object.keys(groups).includes('advisory'));
assert.equal(Object.values(groups).reduce((count, group) => count + group.length, 0), 1,
'the enumerable cohort participates in the existing group-count boundary');
const serialized = JSON.parse(JSON.stringify(groups));
assert.equal(serialized.advisory.length, 1);
assert.deepEqual(serialized.advisory[0].taskReferences, expectedReferences);
assert.equal(serialized.advisory[0].observations.length, 49);
const fixture = cohortFixture();
for (const observation of groups.advisory[0].observations) {
const task = fixture.backlog.find((entry) => entry.id === observation.taskId);
assert.ok(task, `retained reference ${observation.taskId} belongs to the fixed 49-task scenario`);
assert.equal(observation.owner, task.owner);
assert.equal(observation.observedAt, task.observedAt);
assert.equal(observation.currentObservedCommit, '316d8b7480c2e10925ebdff8310fe26ef5a881a8');
assert.equal(observation.reproductionStatus, 'UNKNOWN');
assert.deepEqual(observation.sources, ['doctor', 'backlogHealth']);
}
const server = createConsoleServer();
server.listen(0, '127.0.0.1');
await once(server, 'listening');
let browser;
t.after(async () => {
await browser?.close();
await new Promise((resolve) => server.close(resolve));
});
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
await page.clock.install({ time: new Date('2026-10-01T12:00:00.000Z') });
await page.goto(`http://127.0.0.1:${server.address().port}/console?demo=1#overview`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
await page.evaluate((fixedSnapshot) => {
const demo = globalThis.TorchConsoleDemo;
const originalSnapshot = demo.snapshot;
const originalRequest = demo.request;
let actionCalls = 0;
demo.snapshot = () => structuredClone(fixedSnapshot);
demo.request = (...args) => { actionCalls += 1; return originalRequest(...args); };
globalThis.__attentionCohortReadOnlyProbe = {
originalSnapshot, get actionCalls() { return actionCalls; },
};
}, fixture);
await page.locator('#refresh-console').click();
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
const advisory = page.locator('#attention-list .attention-group-advisory');
assert.equal(await advisory.count(), 1);
assert.match(await advisory.locator('h3').innerText(), /Advisory \/ recheck cohorts/);
assert.equal(await advisory.locator('.attention-item').count(), 1);
assert.equal(await advisory.locator('.attention-cohort-details li').count(), 49);
assert.deepEqual(await advisory.locator('.attention-cohort-details li code').allTextContents(), expectedReferences);
assert.match(await advisory.locator('.attention-cohort-details li').first().textContent(), /Observed source\/revision: 0{39}1/);
assert.equal(await page.locator('#attention-list .attention-group-fleet .attention-item').filter({
hasText: /BACKLOG_OBSERVED_COMMIT_(STALE|MISSING)|task evidence record.*need refresh/,
}).count(), 0);
assert.equal(await page.evaluate(() => globalThis.__attentionCohortReadOnlyProbe.actionCalls), 0);
assert.deepEqual(await page.evaluate(() => {
const current = globalThis.__attentionCohortReadOnlyProbe.originalSnapshot();
return [current.doctor.findings.length, current.backlogHealth.findings.length];
}), [0, 0], 'rendering the synthetic snapshot did not mutate isolated demo state');
});
test('SCN-attention-advisory-provenance-and-semantic-conflicts: equal sources merge and any fingerprint difference stays distinct', () => {
const conflictCases = [
{ field: 'taskId', firstId: 'TASK-ID-A', secondId: 'TASK-ID-B' },
{ field: 'code', change: { code: 'BACKLOG_OBSERVED_COMMIT_MISSING' } },
{ field: 'owner', change: { owner: 'domain-conflict' } },
{ field: 'observedAt', change: { observedAt: '1'.repeat(40) } },
{ field: 'currentObservedCommit', change: { currentObservedCommit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' } },
{ field: 'reproductionStatus', change: { reproductionStatus: 'REPRODUCED' } },
];
const doctor = [];
const backlogHealth = [];
const tasks = [];
for (const [index, conflict] of conflictCases.entries()) {
const id = `TASK-CONFLICT-${conflict.field}`;
const firstTask = { id: conflict.firstId ?? id, owner: 'domain-a', observedAt: String(index + 1).padStart(40, '0') };
const secondTask = conflict.secondId
? { ...firstTask, id: conflict.secondId }
: firstTask;
tasks.push(firstTask, secondTask);
doctor.push(finding(firstTask, {
currentObservedCommit: '316d8b7480c2e10925ebdff8310fe26ef5a881a8', reproductionStatus: 'UNKNOWN',
}));
backlogHealth.push(finding(secondTask, {
currentObservedCommit: '316d8b7480c2e10925ebdff8310fe26ef5a881a8', reproductionStatus: 'UNKNOWN',
...(conflict.change ?? {}),
}));
}
const groups = project({ repository: { head: '316d8b7480c2e10925ebdff8310fe26ef5a881a8' }, backlog: tasks,
doctor: { findings: doctor }, backlogHealth: { findings: backlogHealth } });
assert.equal(groups.advisory.length, 1, 'the advisory label remains one cohort across conflicting records');
assert.equal(groups.advisory[0].observations.length, 12);
assert.equal(groups.advisory[0].observations.filter((entry) => entry.sources.length === 2).length, 0,
'no mismatched fingerprint may inherit both provenance labels');
for (const observation of groups.advisory[0].observations) {
assert.equal(observation.sources.length, 1);
assert.ok(['doctor', 'backlogHealth'].includes(observation.sources[0]));
}
const equalTask = { id: 'TASK-EQUAL', owner: 'domain-equal', observedAt: 'b'.repeat(40) };
const equalGroups = project({ repository: { head: '316d8b7480c2e10925ebdff8310fe26ef5a881a8' }, backlog: [equalTask],
doctor: { findings: [finding(equalTask)] }, backlogHealth: { findings: [finding(equalTask)] } });
assert.equal(equalGroups.advisory[0].observations.length, 1);
assert.deepEqual(equalGroups.advisory[0].observations[0].sources, ['doctor', 'backlogHealth']);
});
test('SCN-attention-advisory-does-not-hide-owner-or-fleet-risks: unsafe and unknown conditions keep their existing classifications', () => {
const groups = project({
agents: [{ areaId: 'provider-runtime', title: 'Provider Runtime' }],
repository: { head: '316d8b7480c2e10925ebdff8310fe26ef5a881a8' },
approvalRequests: { items: [{ id: 'owner-decision', status: 'pending', approver: 'owner', requester: 'provider-runtime',
title: 'Review a real owner decision' }] },
doctor: { findings: [
{ severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE', taskId: 'TASK-ADVISORY' },
{ severity: 'error', code: 'BACKLOG_OBSERVED_COMMIT_STALE', taskId: 'TASK-ERROR', message: 'The source reports an unsafe error.' },
{ severity: 'error', code: 'BACKLOG_HEALTH_INVALID', message: 'Backlog schema validation failed.' },
{ severity: 'warning', code: 'RECOVERABILITY', offMachine: false, recommendation: 'Verify the recovery copy.' },
] },
backlog: [{ id: 'TASK-ADVISORY', owner: 'provider-runtime', observedAt: 'f'.repeat(40) }],
managerWakes: { blockingCount: 1, reservations: [{ managerId: 'provider-runtime', scheduleId: 'wake-a', outcome: 'unknown', blocksManagerWake: true }] },
deliveryOperations: [{ operation: 'publish', state: 'unknown', actor: 'provider-runtime', commit: '316d8b7480c2e10925ebdff8310fe26ef5a881a8' }],
});
assert.equal(groups.owner.length, 1);
assert.equal(groups.owner[0].decisionApprovalId, 'owner-decision');
assert.equal(groups.advisory.length, 1);
assert.ok(groups.fleet.some((item) => item.title === 'Backlog observed commit stale'
&& /unsafe error/.test(item.detail)), 'an error with the advisory code remains a Fleet finding');
assert.ok(groups.fleet.some((item) => item.title === 'Backlog health invalid'));
assert.ok(groups.fleet.some((item) => item.title === 'Off-machine recovery copy is not verified'));
assert.ok(groups.fleet.some((item) => /scheduled manager launch/.test(item.title)));
assert.ok(groups.fleet.some((item) => /delivery operation/.test(item.title)));
assert.equal(groups.fleet.some((item) => item.decisionApprovalId === 'owner-decision'), false);
});
test('SCN-attention-advisory-projection-is-read-only: projection preserves snapshot and existing owner decision shape', () => {
const snapshot = {
repository: { head: '316d8b7480c2e10925ebdff8310fe26ef5a881a8' },
backlog: [{ id: 'TASK-READONLY', owner: 'provider-runtime', observedAt: '0'.repeat(40) }],
doctor: { findings: [{ severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE', taskId: 'TASK-READONLY' }] },
backlogHealth: { findings: [{ severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE', taskId: 'TASK-READONLY' }] },
approvalRequests: { items: [{ id: 'owner-confirm', revision: 7, status: 'pending', approver: 'owner', requester: 'provider-runtime',
title: 'Confirm reviewed scope', evidence: 'receipt:exact', summary: 'Inspect before deciding.' }] },
};
const before = structuredClone(snapshot);
deepFreeze(snapshot);
const groups = project(snapshot);
assert.deepEqual(snapshot, before);
assert.equal(groups.owner[0].decisionApprovalId, 'owner-confirm');
assert.equal(groups.owner[0].evidence, 'receipt:exact');
assert.equal(groups.owner[0].href, '#approval-owner-confirm');
assert.deepEqual(groups.advisory[0].observations[0].sources, ['doctor', 'backlogHealth']);
assert.equal(groups.advisory[0].observations[0].reproductionStatus, 'UNKNOWN');
const selected = { value: 'approved', disabled: true };
const note = { value: 'Keep this unsent decision note.' };
const previewForm = { dataset: { previewToken: 'current-token', revision: '7' },
elements: { namedItem: () => selected }, querySelector: () => note };
let previewCalls = 0;
const shortcut = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(previewForm, 'rejected', () => { previewCalls += 1; });
assert.equal(shortcut.started, false);
assert.equal(previewCalls, 0);
assert.equal(selected.value, 'approved');
assert.equal(note.value, 'Keep this unsent decision note.');
assert.equal(previewForm.dataset.previewToken, 'current-token');
assert.equal(previewForm.dataset.revision, '7');
});
test('SCN-attention-advisory-unknown-fields-stay-unknown: absent owner and observation provenance are not manufactured', () => {
const groups = project({
doctor: { findings: [{ severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_MISSING', taskId: 'TASK-UNKNOWN' }] },
backlogHealth: { findings: [] },
backlog: [{ id: 'TASK-UNKNOWN' }],
});
const [observation] = groups.advisory[0].observations;
assert.equal(groups.advisory.length, 1);
assert.deepEqual(groups.advisory[0].taskReferences, ['TASK-UNKNOWN']);
assert.equal(observation.owner, null);
assert.equal(observation.observedAt, null);
assert.equal(observation.currentObservedCommit, null);
assert.equal(observation.reproductionStatus, 'UNKNOWN');
assert.deepEqual(observation.sources, ['doctor']);
assert.equal(Object.hasOwn(groups, 'advisory'), true);
const emptyGroups = project({ doctor: { findings: [] }, backlogHealth: { findings: [] } });
assert.deepEqual(emptyGroups, { owner: [], fleet: [], arbiter: [] });
assert.equal(Object.hasOwn(emptyGroups, 'advisory'), false);
});
+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: [
@@ -0,0 +1,259 @@
import assert from 'node:assert/strict';
import { once } from 'node:events';
import test, { after, before } from 'node:test';
import { chromium } from '@playwright/test';
import { createConsoleServer } from '../../src/console/server.mjs';
let server;
let browser;
let origin;
before(async () => {
server = createConsoleServer();
server.listen(0, '127.0.0.1');
await once(server, 'listening');
origin = `http://127.0.0.1:${server.address().port}`;
browser = await chromium.launch({ headless: true });
});
after(async () => {
await browser?.close();
if (server?.listening) await new Promise((resolve) => server.close(resolve));
});
async function openOverview(viewport = { width: 1440, height: 1000 }) {
const page = await browser.newPage({ viewport });
await page.goto(`${origin}/console?demo=1#overview`);
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
const snapshot = await page.evaluate(() => structuredClone(globalThis.TorchConsoleDemo.snapshot()));
return { page, snapshot };
}
async function renderSnapshot(page, snapshot) {
await page.evaluate((fixed) => { globalThis.TorchConsoleDemo.snapshot = () => structuredClone(fixed); }, snapshot);
await page.locator('#refresh-console').click();
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
}
function unsafeSnapshot(snapshot) {
snapshot.mode = 'installed';
snapshot.approvalRequests = { available: true, pendingCount: 1, items: [{
id: 'owner-review-1', revision: 4, status: 'pending', approver: 'owner', requester: 'qa',
title: 'Review the exact owner decision', task: 'TASK-CONSOLE-OVERVIEW', evidence: 'same-origin preview and current revision',
}] };
snapshot.fleetChanges = [];
snapshot.hierarchyProposals = [];
snapshot.doctor = { ...(snapshot.doctor ?? {}), findings: [
{ severity: 'error', code: 'BACKLOG_HEALTH_INVALID', message: 'Current schema validation failed; inspect the evidence before action.' },
{ severity: 'error', code: 'WORKTREE_PROBLEM', area: 'qa', problem: 'git-operation:merge', path: '/work/qa' },
{ severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE', taskId: 'TASK-ADVISORY-1', owner: 'provider-runtime', observedAt: 'a'.repeat(40) },
] };
snapshot.backlogHealth = { findings: [] };
snapshot.worktrees = [{ area: 'qa', branch: 'qa/review', path: '/work/qa', ahead: 2, behind: 0 }];
snapshot.agents = [...(snapshot.agents ?? []), { areaId: 'qa', title: 'QA' }];
snapshot.managerWakes = { blockingCount: 1, reservations: [{ managerId: 'qa', scheduleId: 'wake-1', reservedAt: '2026-10-01T00:00:00Z', outcome: 'unknown', blocksManagerWake: true }] };
snapshot.deliveryOperations = [{ operation: 'apply', state: 'unknown', actor: 'qa', commit: 'c'.repeat(40) }];
snapshot.backlog = [{ id: 'TASK-ADVISORY-1', owner: 'provider-runtime', observedAt: 'a'.repeat(40), state: 'in_progress' }];
snapshot.integration = [];
snapshot.backlogActivity = { tasks: [] };
return snapshot;
}
test('SCN-console-overview-owner-decisions-and-urgent-hazards-lead-with-distinct-counts', async (t) => {
const { page, snapshot } = await openOverview();
t.after(() => page.close());
snapshot.messages = { ...(snapshot.messages ?? {}), unacknowledged: 0, recent: [] };
await renderSnapshot(page, unsafeSnapshot(snapshot));
const groups = page.locator('#attention-list > .attention-group');
assert.match(await groups.nth(0).locator('h3').innerText(), /Waiting on you/);
assert.match(await groups.nth(1).locator('h3').innerText(), /Fleet handling/);
assert.equal(await page.locator('#owner-decision-count').innerText(), '1 owner decision');
assert.equal(await page.locator('#urgent-hazard-count').innerText(), '4 urgent hazards');
assert.equal(await page.locator('#attention-count').innerText(), '5 actionable');
assert.match(await page.locator('#advisory-count').innerText(), /1 cohort/);
assert.equal(await page.locator('#attention-list [data-attention-decision]').count(), 2);
assert.equal(await page.locator('#attention-list button').filter({ hasText: /Acknowledge|Close|Recover/ }).count(), 0);
assert.ok(await page.locator('#attention-list .tone-urgent').count() >= 3);
assert.equal(await page.locator('#attention-list a[href="#manager-wakes"]').count(), 1);
});
test('SCN-console-overview-clean-unique-commits-stay-informational-and-unsafe-workstays-reviewable', async (t) => {
const { page, snapshot } = await openOverview();
t.after(() => page.close());
snapshot.doctor = { ...(snapshot.doctor ?? {}), findings: [
{ severity: 'warning', code: 'WORKTREE_PROBLEM', area: 'project-kernel', problem: 'unique-commits:3', path: '/work/kernel' },
{ severity: 'warning', code: 'WORKTREE_PROBLEM', area: 'provider-runtime', problem: 'worktree-dirty', path: '/work/provider' },
] };
snapshot.worktrees = [
{ area: 'project-kernel', branch: 'kernel/progress', path: '/work/kernel', ahead: 3, behind: 0 },
{ area: 'provider-runtime', branch: 'provider/changes', path: '/work/provider', ahead: 0, behind: 0 },
];
snapshot.agents = [...(snapshot.agents ?? []), { areaId: 'project-kernel', title: 'Project Kernel' }, { areaId: 'provider-runtime', title: 'Provider Runtime' }];
await renderSnapshot(page, snapshot);
const records = page.locator('#fleet-attention-list [data-attention-record]');
const clean = records.filter({ hasText: 'has work ahead of main' });
const dirty = records.filter({ hasText: 'worktree needs review' });
assert.equal(await clean.count(), 1);
assert.ok(await clean.evaluate((item) => item.classList.contains('tone-info')));
assert.match(await clean.innerText(), /3 unique commit\(s\) are ahead/);
assert.equal(await dirty.count(), 1);
assert.ok(await dirty.evaluate((item) => item.classList.contains('tone-review')));
assert.match(await dirty.innerText(), /Uncommitted changes are present/);
});
function crowdedSnapshot(snapshot) {
const references = Array.from({ length: 49 }, (_, index) => `TASK-DENSITY-${String(index + 1).padStart(2, '0')}`);
const tasks = references.map((id, index) => ({
id, owner: index === 47 ? null : `domain-${String(index + 1).padStart(2, '0')}`,
observedAt: index === 47 ? null : String(index + 1).padStart(40, '0'), state: 'in_progress',
}));
const doctor = tasks.map((task, index) => ({ severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE',
taskId: task.id, owner: task.owner, observedAt: task.observedAt,
...(index === 47 ? { currentObservedCommit: null } : {}),
}));
const backlogHealth = structuredClone(doctor);
backlogHealth[48].owner = 'conflicting-owner';
snapshot.backlog = tasks;
snapshot.fleetChanges = [];
snapshot.hierarchyProposals = [];
snapshot.approvalRequests = { available: true, pendingCount: 0, items: [] };
snapshot.messages = { ...(snapshot.messages ?? {}), unacknowledged: 0, recent: [] };
snapshot.integration = [];
snapshot.worktrees = [];
snapshot.backlogActivity = { tasks: [] };
snapshot.managerWakes = { blockingCount: 0, reservations: [] };
snapshot.deliveryOperations = [];
snapshot.repository = { ...(snapshot.repository ?? {}), head: 'd'.repeat(40) };
snapshot.doctor = { ...(snapshot.doctor ?? {}), findings: [
...doctor,
...Array.from({ length: 3 }, (_, index) => ({ severity: 'error', code: `FIXED_UNSAFE_${index + 1}`,
area: index === 0 ? null : `domain-${index + 1}`, message: `Urgent evidence ${index + 1}: ${'e'.repeat(180)}` })),
...Array.from({ length: 14 }, (_, index) => ({ severity: 'warning', code: `FIXED_REVIEW_${index + 1}`,
area: index === 0 ? null : `domain-${index + 1}`, message: `Review evidence ${index + 1}: ${'f'.repeat(140)}` })),
] };
snapshot.backlogHealth = { findings: backlogHealth };
return { snapshot, references };
}
test('SCN-console-overview-crowded-preview-preserves-every-fleet-and-advisory-record-at-desktop-and-mobile', async (t) => {
const desktop = await openOverview();
t.after(() => desktop.page.close());
const { snapshot, references } = crowdedSnapshot(desktop.snapshot);
await renderSnapshot(desktop.page, snapshot);
assert.equal(await desktop.page.locator('#advisory-count').innerText(), '1 cohort · 49 task references');
assert.equal(await desktop.page.locator('#fleet-preview-count').innerText(), 'Showing 6 of 17 Fleet findings; 11 remaining');
assert.equal(await desktop.page.locator('#attention-list .tone-urgent').count(), 3);
const desktopWidth = await desktop.page.evaluate(() => ({ viewport: document.documentElement.clientWidth,
document: document.documentElement.scrollWidth,
layout: ['body', '.console-layout', '.console-workspace', '#overview', '.attention-panel', '#attention-list'].map((selector) => {
const item = document.querySelector(selector); const rect = item.getBoundingClientRect();
return `${selector}: ${Math.round(rect.left)}..${Math.round(rect.right)} scroll=${item.scrollWidth}/${item.clientWidth}`;
}),
offenders: [...document.querySelectorAll('body *')].map((item) => ({ item, rect: item.getBoundingClientRect() }))
.filter(({ item, rect }) => !item.closest('[hidden]') && rect.width > 0 && (rect.right > document.documentElement.clientWidth + 1 || rect.left < -1 || item.scrollWidth > item.clientWidth + 2))
.slice(0, 12).map(({ item, rect }) => `${item.tagName.toLowerCase()}${item.id ? `#${item.id}` : ''}.${String(item.className?.baseVal ?? item.className ?? '').split(' ').filter(Boolean).join('.')} ${Math.round(rect.left)}..${Math.round(rect.right)} scroll=${item.scrollWidth}/${item.clientWidth}`) }));
assert.ok(desktopWidth.document <= desktopWidth.viewport, JSON.stringify(desktopWidth));
const overviewUrgentBounds = await desktop.page.locator('#attention-list .tone-urgent').evaluateAll((items) => items.map((item) => {
const rect = item.getBoundingClientRect(); return [rect.left, rect.right];
}));
assert.ok(overviewUrgentBounds.every(([left, right]) => left >= 0 && right <= 1440));
await desktop.page.locator('.attention-footer a[href="#fleet-attention-queue"]').click();
await desktop.page.waitForFunction(() => location.hash === '#fleet-attention-queue' && document.querySelector('.console-workspace').dataset.currentView === 'fleet');
assert.equal(await desktop.page.locator('#fleet-attention-list .attention-group-fleet [data-attention-record]').count(), 17);
assert.equal(await desktop.page.locator('#fleet-attention-list .attention-group-advisory .attention-cohort-details li').count(), 50,
'the conflicting owner remains a separate observation instead of overwriting provenance');
assert.deepEqual(await desktop.page.locator('#fleet-attention-list .attention-group-advisory .attention-cohort-details li code').allTextContents(),
[...references, references[48]]);
assert.equal(await desktop.page.evaluate(() => document.activeElement.id), 'fleet-attention-queue');
assert.match(await desktop.page.locator('#fleet-attention-list').innerText(), /Owner: Responsible domain not recorded/);
await desktop.page.locator('#fleet-attention-list .attention-group-advisory .attention-cohort-details summary').click();
assert.match(await desktop.page.locator('#fleet-attention-list').innerText(), /Owner: Owner not recorded/);
assert.match(await desktop.page.locator('#fleet-attention-list').innerText(), /Sources: doctor, backlogHealth/);
assert.match(await desktop.page.locator('#fleet-attention-list').innerText(), /Urgent evidence 1/);
const queueFilter = desktop.page.locator('#fleet-attention-filter');
await queueFilter.fill(references[0]);
assert.equal(await desktop.page.locator('#fleet-attention-result-count').innerText(), '1 of 18 findings match.');
await queueFilter.fill('');
assert.equal(await desktop.page.locator('#fleet-attention-list [data-attention-record]:visible').count(), 18);
await desktop.page.locator('#fleet-attention-list details').evaluateAll((details) => details.forEach((detail) => { detail.open = true; }));
assert.ok(await desktop.page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth),
'expanded full evidence and cohort references remain horizontally contained');
const queueBounds = await desktop.page.locator('#fleet-attention-list [data-attention-record]').evaluateAll((items) => items.map((item) => {
const rect = item.getBoundingClientRect(); return [rect.left, rect.right];
}));
assert.ok(queueBounds.every(([left, right]) => left >= 0 && right <= 1440));
const mobile = await openOverview({ width: 390, height: 844 });
t.after(() => mobile.page.close());
const mobileFixture = crowdedSnapshot(mobile.snapshot);
await renderSnapshot(mobile.page, mobileFixture.snapshot);
const mobileWidth = await mobile.page.evaluate(() => ({ viewport: document.documentElement.clientWidth,
document: document.documentElement.scrollWidth,
offenders: [...document.querySelectorAll('body *')].map((item) => ({ item, rect: item.getBoundingClientRect() }))
.filter(({ item, rect }) => !item.closest('[hidden]') && rect.width > 0 && rect.right > document.documentElement.clientWidth + 1)
.slice(0, 8).map(({ item, rect }) => `${item.tagName.toLowerCase()}${item.id ? `#${item.id}` : ''}.${String(item.className?.baseVal ?? item.className ?? '').split(' ').filter(Boolean).join('.')} right=${Math.round(rect.right)}`) }));
assert.ok(mobileWidth.document <= mobileWidth.viewport, JSON.stringify(mobileWidth));
await mobile.page.locator('#attention-list .attention-group-advisory summary').click();
const lastReference = mobile.page.locator('#attention-list .attention-group-advisory .attention-cohort-details li').last();
const mobileBounds = await lastReference.boundingBox();
assert.ok(mobileBounds && mobileBounds.x >= 0 && mobileBounds.x + mobileBounds.width <= 390,
'expanded exact references remain within the mobile viewport');
assert.ok(await mobile.page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth),
'expanded advisory provenance does not overflow the mobile viewport');
assert.equal(await mobile.page.locator('#fleet-preview-count').innerText(), 'Showing 6 of 17 Fleet findings; 11 remaining');
});
test('SCN-console-overview-refresh-and-navigation-preserve-owner-drafts-without-adding-unsafe-actions', async (t) => {
const { page, snapshot } = await openOverview();
t.after(() => page.close());
const unreadBefore = snapshot.messages?.unacknowledged ?? 0;
await renderSnapshot(page, unsafeSnapshot(snapshot));
await page.locator('.console-rail a[href="#work"]').click();
await page.locator('.task-create-panel > summary').click();
const title = page.locator('#task-create-form [name="title"]');
await title.fill('Unsent route-change draft for the current revision.');
await page.locator('.console-rail a[href="#overview"]').click();
await page.locator('#refresh-console').click();
await page.locator('#live-refresh-status[data-generation="3"]').waitFor();
await page.locator('.console-rail a[href="#work"]').click();
assert.equal(await title.inputValue(), 'Unsent route-change draft for the current revision.');
assert.equal(await page.locator('#attention-list [data-attention-decision]').count(), 2);
assert.equal(await page.locator('#attention-list button').filter({ hasText: /Acknowledge|Close|Recover/ }).count(), 0);
assert.equal(await page.evaluate(() => globalThis.TorchConsoleDemo.snapshot().messages?.unacknowledged ?? 0), unreadBefore,
'rendering and refresh do not acknowledge durable messages');
});
test('SCN-console-overview-retains-ten-destinations-keyboard-routing-and-manager-wake-deep-links', async (t) => {
const { page, snapshot } = await openOverview({ width: 390, height: 844 });
t.after(() => page.close());
await renderSnapshot(page, unsafeSnapshot(snapshot));
const links = page.locator('.console-rail a[href^="#"]');
assert.equal(await links.count(), 10);
const destinations = ['overview', 'owner-briefing', 'flow-watch', 'work', 'initiative-progress', 'fleet',
'communications', 'organization', 'evidence', 'delivery'];
for (const destination of destinations) {
const link = page.locator(`.console-rail a[href="#${destination}"]`);
if (destination === 'fleet') {
await link.focus();
await page.keyboard.press('Enter');
} else await link.click();
await page.waitForFunction((view) => location.hash === `#${view}`
&& document.querySelector('.console-workspace').dataset.currentView === view, destination);
}
await page.locator('.console-rail a[href="#fleet"]').click();
await page.waitForFunction(() => location.hash === '#fleet'
&& document.querySelector('.console-workspace').dataset.currentView === 'fleet');
await page.goto(`${origin}/console?demo=1#manager-wakes`);
await page.waitForFunction(() => document.querySelector('.console-workspace').dataset.currentView === 'fleet');
await page.waitForFunction(() => document.activeElement.id === 'manager-wakes');
assert.equal(await page.locator('#manager-wakes').isVisible(), true);
await page.goBack();
await page.waitForFunction(() => document.querySelector('.console-workspace').dataset.currentView === 'fleet');
await page.goForward();
await page.waitForFunction(() => document.querySelector('.console-workspace').dataset.currentView === 'fleet');
await page.waitForFunction(() => document.activeElement.id === 'manager-wakes');
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth));
});
+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], {