Merge commit '28ee61bf62c3c99a845c97540570401a47bd6aa3' into torch/session-manager

This commit is contained in:
thePR0M3TH3AN committed 2026-09-30 15:42:40 -04:00
commit e758f1bfca
15 files changed
+567 -15

No files matched your search

+4 -1
View File
@@ -6,7 +6,10 @@ Repository state outranks conversation memory. Query ownership before crossing a
This initial launch is a bounded installation and communications smoke test.
Use the installed `torch` launcher and identity-bound TORCH MCP tools. Query
identity, ownership, inbox and next backlog item. Read current canonical
identity, ownership and inbox. Specialists query their next backlog item;
the Session Manager inspects fleet backlog rather than claiming specialist work.
Run `torch brief --area <your-area> --json` at startup and before each new item.
Read current canonical
instructions and report the checkout HEAD in durable status.
Do not implement code, create or assign development tasks, enable timers,
+8
View File
@@ -6,6 +6,14 @@ At startup and when recurring cross-domain friction appears, run `torch_assess_h
## Backlog loop
The owner has seeded conversation follow-up as `TASK-conversation-*` proposals
in this project's existing backlog. Read `docs/TODO.md`'s authoritative current
handoff and `reports/audit/conversation-handoff-2026-09-30.md` before prioritizing.
Proposed tasks are not ready assignments. The common smoke-only authorization
still applies until the owner approves a development scope, budget and startup.
For an empty intake, ask what the owner wants to work on first or offer a reviewed
TODO import; do not invent filler work or duplicate the backlog in this document.
Keep one durable Fleet backlog. Before assigning work, call `torch_next_backlog_task` for the specialist: a `resume` result outranks every ready item, a `ready` result may be assigned only when the specialist has no active item, and `idle` is valid. After completion, repeat the same lookup. Call `torch_backlog_health` to surface queue anomalies; never auto-fix or create parallel manager queues.
## Fleet evolution
+16 -2
View File
@@ -292,14 +292,28 @@ Review the evidence in `fleet-proposal.json`, edit its `review.status` to
node bin/torch.mjs install --proposal fleet-proposal.json --runtime codex --dry-run --json
node bin/torch.mjs install --proposal fleet-proposal.json --runtime codex --yes --json
node bin/torch.mjs doctor --json
node bin/torch.mjs worktrees --dry-run --json
node bin/torch.mjs worktrees --yes --json
node bin/torch.mjs setup --dry-run --json
node bin/torch.mjs setup --yes --json
node bin/torch.mjs up --fresh --dry-run --json
```
Starting or stopping configured runtime sessions is always a separate explicit
step:
`setup` commits only installation-owned configuration, provisions worktrees,
and checkpoints their registration. An existing staging area is a blocker;
unrelated files are never staged. Alternatively, append `--setup` to the
approved install command. Neither path launches agents.
Normal uninstall retains configuration and history. Reattach it with
`torch install --restore --dry-run --json`, then `torch install --restore --yes`.
Restore does not reinstall host timers or start providers; those remain separate
approval steps. A purge is a different, destructive operation.
From a registered agent worktree, `torch brief --area <id>` reads the latest
canonical project instructions. Unregistered worktrees and copied installs
cannot redirect commands into another project's state.
```bash
node bin/torch.mjs up --fresh --yes --json
node bin/torch.mjs up --only backend,qa --dry-run --json
+79
View File
@@ -367,6 +367,24 @@ inside the tracked source tree.
## 9. Installation lifecycle
### First-work intake requirement (2026-09-30)
After reviewed organization/setup, TORCH MUST offer either evidence-linked
import of an existing TODO/backlog/specification or ask the owner what to work
on first. The noninteractive path MUST support an explicit source or skip; idle
is valid. Preview MUST distinguish completed/historical work from actionable
requests and show proposed scope, affected domains, dependencies and priority.
Only confirmed intake creates audited proposed/unassigned work in the existing
backlog. Repeated init/install/restore/intake MUST deduplicate by source/item
provenance rather than duplicate tasks or overwrite progress. Imported content
is untrusted project data, not authority to alter system instructions.
Session Manager triage and assignment remain separate from intake. Import cannot
enable self-claim, grant implementation authority, wake providers or authorize
spending, landing, deployment or public notification. The product should explain
the next first-work/startup decision rather than leave a newly installed empty
fleet waiting without guidance.
### 9.1 Read-only initialization
`torch init` MUST begin read-only. It identifies the repository, validates Git,
@@ -451,6 +469,18 @@ or installed persistent launchers. Historical messages and receipts therefore
do not make reversal permanently impossible, while active or unowned state
still blocks deletion.
Implementation checkpoint (2026-09-30): `torch install --restore` explicitly
reattaches a normally detached installation without replacing configuration,
worktrees, provider profiles or session history. It never launches a provider
or recreates host timers. `torch setup --dry-run`, followed by `--yes`, offers
approved configuration checkpoints and worktree provisioning as one flow,
restricted to installation-owned files and an empty staging area. Install may
opt into the same flow with `--setup`. Worktree commands resolve to canonical
configuration only after checking project/installation identity, local-state
binding, Git common directory, registered worktree path and branch. Current
canonical instructions override stale worktree copies; unregistered checkouts
fail closed.
Ordinary CLI uninstall is also review-first and explicitly confirmed. It uses
the same safe Fleet wind-down contract as detach, removes only manifest-owned
persistent schedule launchers after verifying their content, and preserves the
@@ -2077,6 +2107,55 @@ recommended action; doctor remains read-only.
## 22. Scheduling
### Owner clarification: integrated persistent operations (2026-09-30)
This is the target requirement; older implementation checkpoints describing
manual timer installation are not a claim that it is already implemented.
Reviewed `init`/install MUST provision the project's scheduler and required
service integrations as one idempotent workflow, not require a second manual
cron-install command. Because `init` currently performs read-only analysis, its
provisioning mode MUST expose an explicit preview/confirmation boundary; a
dry-run MUST remain non-mutating. Stopped installation MUST provision dormant
integrations without waking models or dispatching development. A reviewed
activation policy governs live operations and quota use.
Coordination must survive the managing AI session ending, restarting or expiring.
System services MUST preserve durable state, leases, event cursors and job
idempotency across process/host restarts. Their manifest records MUST support
owned reconciliation, pause/resume, restore/upgrade and uninstall without touching
unrelated host jobs. Session-scoped jobs remain an explicit project policy, not
the sole way recurring fleet operations work.
Project intake MUST propose applicable capabilities, with clear implemented,
unconfigured and unsupported states:
- direct-report manager check-ins, structured approval/peer/owner waits,
stale owner-request review and an owner-first local digest;
- a persistent, serialized landing worker using approved exact-commit checks;
- resource-queue watchdogs, audited slot withdrawal and displaced-runner stop;
- durable significant events and deduplicated budgeted manager wakes, with
low-frequency timed safety checks rather than perpetual paid polling;
- a project-declared nightly canonical suite, heavy-check windows and
evidence-backed regression routing, not guessed blame;
- measured test durations, uncertain wait estimates and project-specific
exclusive resources such as a GPU;
- conservative provider-neutral usage limits, graceful wind-down and explicitly
approved credit-window or calendar restarts that never override manual pause;
- idle, clean, unguarded worktree convergence by merge, preserving conflicts,
unique commits and active work;
- exact landed task closure, verified reversal/follow-up policy and reviewed
evidence for finished-but-unnamed work, never automatic closure by message alone;
- project-specific local dev/Console services and separately authorized delivery,
live-version checks and external notifications where relevant.
No universal project inherits COMBATRIG's release times, Monday reset time,
two GPU slots, Vercel destination, phone notifier or LAN/HTTPS exposure. Setup
MUST show lifetime, timezone/DST behavior, missed-run policy, costs/budgets,
ports/destinations, health evidence, stop semantics and owner authority. Unsupported
hosts/adapters need an actionable limitation or qualified fallback, not a fictitious
"installed" status. No provider wake, deploy, public message, off-machine write
or cleanup of unowned processes follows solely from creating a schedule.
TORCH recognizes two schedule classes.
**System schedules** survive AI sessions and may perform approved release,
+31
View File
@@ -2237,3 +2237,34 @@ test_integrity_note:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
# Setup and canonical worktree briefs (2026-09-30)
```yaml
test_integrity_note:
change_type: new_tests
scenarios:
- id: SCN-guided-setup
given: "An approved installed project with uncommitted TORCH files"
when: "Owner reviews and authorizes setup"
then: "Only owned files are committed; registered worktrees exist; no providers start; staged user work blocks setup"
- id: SCN-worktree-canonical-brief
given: "A registered worktree has older instructions"
when: "Its installation root is resolved and a brief requested"
then: "Canonical instructions appear; unregistered worktrees, copied installs and stale installation IDs fail closed"
- id: SCN-install-restore
given: "A normally detached project retains configuration and worktree commits"
when: "Owner explicitly restores the install"
then: "Binding is reattached without changing profiles, worktree history or launching providers"
observable_outcomes:
- "Git commits, clean canonical checkout, worktree registrations, brief text and local detach metadata"
determinism_controls:
- "Isolated temporary Git repositories and XDG state; no provider or network calls"
anti_cheat_rationale:
prevents:
- "Hard-coded success without Git worktrees or commits"
- "Accepting copied or unregistered installation state"
- "Staging unrelated owner work"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
+63 -4
View File
@@ -1,5 +1,64 @@
# TORCH remaining work
## 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
worktree briefs have been verified. The active organization is Session Manager
plus six specialists; all providers are offline. The default Git branch remains
legacy TORCH; the configured development/integration boundary is
`rewrite/portable-agent-fleet`. No publication or default-branch promotion is
authorized. Later sections retain historical audit snapshots, including old
"uncommitted/no launcher" statements; those are superseded by this handoff.
The conversation's unfinished work is now owner-proposed in the **existing
authoritative backlog in the Session Manager worktree**, not a second TODO queue.
Use `torch backlog list --json` to see it from any registered checkout. Tasks use
the `TASK-conversation-` prefix, have acceptance criteria, affected domains and
dependencies, and remain unassigned for manager triage. See
`reports/audit/conversation-handoff-2026-09-30.md` for the domain review and index.
### New owner requirements: first work and automatic operations
- [ ] `TASK-conversation-first-work-intake`: init/install/setup offers reviewed
TODO/backlog import or asks what the owner wants to begin with; previews and
confirms proposed tasks; deduplicates resumes; never activates stale/completed
TODOs or launches agents silently.
- [ ] `TASK-conversation-persistent-operations`: reviewed init/install provisions
machine-level scheduler/services automatically, not merely schedule JSON or a
separate manual cron-install command. Paused installs remain dormant. Services
declare system/session lifetimes, budgets, timezone, restart/missed-run policy,
pause/resume and manifest-owned uninstall/restore behavior. This extends the
earlier `TASK-conversation-automatic-scheduler` requirement.
- [ ] `TASK-conversation-event-driven-wakes`: durable deduplicated significant
events wake the appropriate manager under budget; periodic checks are the quiet
safety net, not repeated usage-consuming busywork.
- [ ] `TASK-conversation-queue-watchdog`: detect stuck resource queues and
disappearing slots, audit removals and stop displaced waiters; never infer
browser ownership/termination from a process name or runner death.
- [ ] `TASK-conversation-nightly-main-tests`: project-declared full canonical
suite on an exact frozen commit, resource-aware idle window and evidence-backed
regression routing. Do not guess the offending change.
- [ ] `TASK-conversation-usage-governor`: provider-neutral usage/credit evidence,
conservative fallback budgets, graceful wind-down and reviewed reset/restart
policy that cannot override manual pause or detach.
- [ ] `TASK-conversation-idle-worktree-refresh`: scheduled merges into clean,
idle, unguarded branches; skip active/dirty/unknown work and preserve conflicts.
- [ ] `TASK-conversation-queue-planning`: measured test durations, uncertain wait
estimates, slowdown visibility, GPU-exclusive resources and approved heavy-run
windows; preserve the resource queue's authority.
- [ ] `TASK-conversation-closure-revert-review`: verified revert/follow-up policy
and evidence-backed review of unnamed finished work; never mass-close or reopen
tasks based only on untrusted commit trailers.
- [ ] `TASK-conversation-project-services`: reviewed project-specific landing
worker, local dev/Console server, local summary and optional release jobs.
No COMBATRIG schedules, Vercel deployment, credentials, LAN exposure or automatic
phone notifications are assumed.
These are proposed development tasks, **not implemented automatic setup**. The
existing 15-minute manager schedule is configured, but no TORCH host timer is
installed. Common instructions still authorize only the smoke test; development
activation, self-claim and recurring provider wakes remain separately reviewed.
## First real self-host run (2026-09-30)
### Portable production pipelines (new roadmap element)
@@ -37,7 +96,7 @@
updates, retained previous installs, offline previews and fail-closed errors.
Four deterministic scenarios added; live download qualification recorded in
the first-run audit, not implied by mocked installer tests.
- [ ] Fix `torch brief` invoked from a managed worktree: both live sessions hit
- [x] Fix `torch brief` invoked from a managed worktree: both live sessions hit
`LOCAL_STATE_METADATA_MISMATCH`. Resolve canonical project binding with
checked worktree membership, never weaken root/installation identity checks.
- [ ] Surface Codex JSON stdout failure events: first Sol launch hid the actual
@@ -45,7 +104,7 @@
- [ ] Add exact executable/version/model/auth preflight to owner Console and
setup flow. PATH selected CLI 0.158.0 while the current session used 0.159.2;
the older child rejected Sol 6.1 and required manual investigation/retry.
- [ ] Make configuration checkpoint/worktree/manifest handoffs explicit or
- [x] Make configuration checkpoint/worktree/manifest handoffs explicit or
guided: generated tracked files and provisioning manifest updates require
deliberate commits; a healthy doctor does not prove startup readiness.
- [ ] Isolate irrelevant inherited global MCP servers without removing user
@@ -349,7 +408,7 @@ in `reports/audit/self-host-readiness-2026-09-30.md`. No agent or timer was star
the collapsed panel; observation never releases a launch lock.
- [ ] Qualify interrupted-wake recovery against live runtime lifecycle evidence;
owner attestation is explicit but is not independent proof of termination.
- [ ] Qualify the updated source as a clean commit before self-host activation;
- [x] Qualify the updated source as a clean commit before self-host activation;
older artifact receipts predate these changes. No fleet or timer has started.
Review verification (2026-09-30): full regression suite through isolated
@@ -961,7 +1020,7 @@ COMBATRIG's proposed hierarchy as the default.
multi-domain coordination/resume scenario with a fixed provider budget.
- [ ] Install and reverse the user-systemd scheduler, verifying one real
dispatch and its receipts.
- [ ] Install an accepted TORCH version in user-local versioned state and use
- [x] Install an accepted TORCH version in user-local versioned state and use
its stable launcher to manage this source checkout.
- [ ] Resolve COMBATRIG's 56 ownership/exclusion blockers, then separately
authorize an import/cutover trial. Do not mutate COMBATRIG beforehand.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "torch-agent-fleet",
"version": "0.1.0-alpha.1",
"version": "0.1.0-alpha.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "torch-agent-fleet",
"version": "0.1.0-alpha.1",
"version": "0.1.0-alpha.2",
"bundleDependencies": [
"@modelcontextprotocol/server",
"typebox",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "torch-agent-fleet",
"version": "0.1.0-alpha.1",
"version": "0.1.0-alpha.2",
"type": "module",
"description": "TORCH — a portable, provider-independent AI development fleet for Git repositories",
"main": "src/cli.mjs",
@@ -0,0 +1,90 @@
# TORCH conversation-to-fleet handoff
## Installed domains
The installed organization is appropriately scoped to this repository: one
Session Manager and six specialists. A current read-only ownership query over
129 tracked files under `src`, `bin`, `site`, `schemas`, `scripts` and `test`
found zero unowned files and zero duplicate primary owners. This claim does not
cover every historical report or future new directory.
| Domain | Responsibility | Effective Codex model |
| --- | --- | --- |
| Session Manager | Routing, ownership, priority and fleet health | GPT-6.1 Sol high |
| Project Kernel | Bootstrap, identity, canonical instructions and organization | GPT-5.6 Terra high |
| Provider Runtime | Harnesses, MCP, lifecycle, schedules and telemetry | GPT-5.6 Terra high |
| Work and Integration | Backlog, checks, resources, landing and recovery | GPT-5.6 Terra high |
| Owner Console | Dashboard, artifacts, feedback and observability | GPT-6 Luna high |
| Release and Self-host | CLI, package, candidates, upgrades and removal | GPT-6 Luna high |
| Independent QA | Scenario integrity and real qualification | GPT-5.6 Terra high |
The six-role grouping was a reviewed consolidation of repository evidence,
not a copy of COMBATRIG's specialists and not proof of a fully autonomous AI
Architect first-run. Proposed extra domain leads are inactive. Headcount alone
does not justify adding managers. Significant pipeline work may justify different
model effort or a new domain later, through the existing review path.
## Authoritative backlog
Owner-request intake created 34 proposed, unassigned items in the existing
Session Manager worktree backlog. There are 33 current follow-ups plus the
earlier narrower `automatic-scheduler` request, retained for manager triage
against the newer `persistent-operations` requirement rather than silently
deleted. All have acceptance criteria, affected domains and observed commit
binding; dependent slices reference existing task IDs. No assignment, provider
start, self-claim enabling or host service installation occurred.
Run `torch backlog list --json` from any registered checkout. The canonical
backlog service resolves the manager's tracked ledger; do not create a second
copy in the source checkout. Task IDs below are prefixed `TASK-conversation-`.
| Group | Task suffixes |
| --- | --- |
| First work / readiness | `first-work-intake`, `development-activation`, `selfhost-development-trial` |
| Persistent operations | `persistent-operations`, `automatic-scheduler`, `event-driven-wakes`, `queue-watchdog`, `nightly-main-tests`, `usage-governor`, `idle-worktree-refresh`, `queue-planning`, `project-services` |
| Provider reliability | `runtime-diagnostics`, `startup-preflight`, `mcp-isolation`, `update-startpaths`, `mixed-provider-qualification` |
| Pipelines | `pipeline-definitions`, `pipeline-provenance`, `pipeline-flow`, `pipeline-stalls`, `pipeline-console`, `pipeline-metrics`, `pipeline-pilot` |
| Live qualification | `hierarchy-pilot`, `scheduler-qualification`, `browser-portability`, `landing-closure`, `closure-revert-review`, `delivery-qualification` |
| Owner/product/release | `owner-digest`, `console-workflow`, `documentation-current`, `release-qualification` |
## What is not yet automatic
Setup and retained-state restore have been qualified; installed alpha.2 is
healthy. All agents remain offline. The current manager schedule declares a
15-minute interval, but no TORCH-owned systemd units are installed or active.
Read-only host timer enumeration confirmed no TORCH timer; unrelated Archive
timers were untouched. The owner's latest request is that init/install provision
these integrations automatically. That is now an explicit target in specification
section 22 and the backlog, not an already delivered feature.
The shared common prompt still authorizes only an installation smoke test and
the project self-claim policy is disabled. Starting agents under it does not
authorize development. The manager should read and triage proposed work, recommend
a first bounded slice and budget, and obtain the development/startup approval
before delegation. New installs should offer reviewed TODO import or ask the
owner what they want first; repeated intake must deduplicate rather than queue
every historical checkbox.
## Suggested first implementation sequence
1. First-work intake and persistent operations planning/provisioning: make
the product's installation genuinely complete while stopped until ready.
2. Runtime diagnostics and exact launch preflight: make startup failures legible.
3. Separately approved bounded self-host development, landing/closure and
scheduler qualification, followed by persistent event/watchdog/nightly work.
4. Pipeline definitions, immutable provenance, checked transitions and Console.
5. Finish mixed-provider, organization-growth and release qualification.
There is no authorization here to promote the rewrite to default `main`, deploy
the public website, send notifications, alter COMBATRIG, spend on asset generation
or install active recurring AI wakes. Those boundaries remain in the task criteria.
## Validation
Installed `torch backlog list` sees all 34 items. Acceptance criteria and domains
are populated; every dependency exists and the dependency graph is acyclic.
Installed `torch backlog health` reports healthy with no anomalous assignments.
Its bounded historical activity coverage remains incomplete, correctly labelled
unknown rather than proof of inactivity. Existing backlog/scheduling regression
suite: 23/23 PASS. All seven identities remain offline. These are backlog/spec
changes, not implementation of the newly requested onboarding or automation.
@@ -0,0 +1,59 @@
# Guided setup and reinstall verification
## Changes
- `torch brief` and normal installed-project CLI commands resolve registered
worktrees to the canonical project. The resolver checks project and installation
IDs, XDG state binding, Git common directory, worktree membership and branch.
Current canonical instructions win over an old worktree copy.
- `torch setup --dry-run`, then `--yes`, checkpoints owned configuration,
provisions worktrees and checkpoints registration. Existing staged work is
rejected; unrelated files are not staged. Install can opt in with `--setup`.
- `torch install --restore` reattaches the retained state from normal uninstall.
It does not overwrite profiles, erase history, recreate timers or start agents.
- The self-host smoke brief distinguishes manager backlog inspection from
specialist next-task lookup and explicitly requires current briefs.
## Evidence before installation
All 39 source acceptance groups, lint and syntax: PASS.
Receipt: `/tmp/torch-alpha2-source-acceptance.json`.
New scenarios: guided setup, canonical worktree briefs, safe detach/restore.
The actual Owner Console worktree returned its current brief successfully
through the patched source CLI, including its preserved offline runtime ID.
## Installation trial
Accepted alpha.2 source: `c11754ab18fa690355ead3f6f87b3accb2975153`.
Candidate digest: `d4b6ba38a51cc1804047b175c963806850f017a92d88863d187043314414dd90`.
All 39 acceptance groups passed again from the copied candidate. Active version
is `0.1.0-alpha.2`; rollback retains alpha.1.
Normal uninstall: PASS. Retained-state restore: PASS. All seven actual agent
worktrees returned briefs through `/home/user/.local/bin/torch`: PASS. Their
briefs include the updated canonical smoke instructions despite older worktree
copies. Model configuration hash, project/installation IDs, offline states,
saved Codex runtime IDs and all seven branch tips were unchanged across
uninstall/restore. No provider was launched.
The restored project's `torch setup --yes --json` was a no-op: no commits,
no mutations, no sessions started. Final doctor reports healthy. Modified owned
configuration and unique manager history remain legitimate safety warnings,
not authorization to purge. Uninstall refreshed the manager resume brief;
that generated file was separately checkpointed on its existing branch, without
rewriting prior history or landing anything.
Receipts:
- `/tmp/torch-alpha2-candidate-build.json`
- `/tmp/torch-alpha2-uninstall-plan.json`
- `/tmp/torch-alpha2-uninstall-result.json`
- `/tmp/torch-alpha2-restore-result.json`
- `/tmp/torch-alpha2-reinstall-verification.json`
- `/tmp/torch-alpha2-real-setup-result.json`
- `/tmp/torch-alpha2-real-doctor.json`
This tests retained-state reinstall, not destructive purge or a fresh installation
with discarded configuration. New-install setup is covered by isolated real-Git
regression tests. Provider-authentication and new agent launches were deliberately
not repeated. Host timers remain uninstalled.
+19 -4
View File
@@ -24,6 +24,8 @@ import { diagnoseProject } from './kernel/doctor.mjs';
import { loadProjectConfig, validateProjectConfig } from './kernel/config.mjs';
import { asErrorRecord, TorchError } from './kernel/errors.mjs';
import { inspectRepository } from './kernel/git.mjs';
import { resolveInstalledProjectRoot } from './kernel/project-root.mjs';
import { setupProject, restoreProject } from './kernel/setup.mjs';
import { inspectSpecifications } from './kernel/specifications.mjs';
import { installProject, planInstall, uninstallProject } from './kernel/install.mjs';
import { IntegrationService } from './integration/service.mjs';
@@ -72,6 +74,8 @@ Usage:
torch domains [--repo <path>] [--spec <path>] [--output <path>] [--json]
torch install --proposal <path> [--runtime <adapter,adapter>] [--default-runtime <adapter>] [--update-runtimes] [--dry-run] [--yes] [--json]
torch worktrees [--parent <path>] [--dry-run] [--yes] [--json]
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 down [--dry-run] [--yes] [--json]
torch capture [--json]
@@ -414,7 +418,7 @@ export const CANDIDATE_ACCEPTANCE_EVIDENCE = Object.freeze({
'forge-migration': ['SCN-forge-migration'],
'delivery-lifecycle': ['SCN-delivery-lifecycle', 'SCN-delivery-attempts', 'SCN-delivery-retry-boundaries', 'SCN-delivery-succeeded-recovery',
'SCN-owner-digest', 'SCN-owner-digest-schedule', 'SCN-owner-digest-receipts'],
'review-install-roster': ['SCN-cli-domain-review', 'SCN-cli-install-provider-default', 'SCN-install-provider-inheritance'],
'review-install-roster': ['SCN-cli-domain-review', 'SCN-cli-install-provider-default', 'SCN-install-provider-inheritance', 'SCN-guided-setup'],
'branches-worktrees': ['SCN-worktree-bootstrap'],
'runtime-identities': ['SCN-mixed-runtime'],
'runtime-profiles': ['SCN-runtime-profiles', 'SCN-runtime-cost-ceiling', 'SCN-runtime-profile-cli'],
@@ -471,7 +475,7 @@ export const CANDIDATE_ACCEPTANCE_EVIDENCE = Object.freeze({
'capture-stop-resume': [
'SCN-fleet-fresh-resume', 'SCN-hierarchy-aware-start-stop', 'SCN-hierarchy-startup-cycle',
],
'detach-uninstall': ['SCN-install-doctor-purge'],
'detach-uninstall': ['SCN-install-doctor-purge', 'SCN-install-restore', 'SCN-worktree-canonical-brief'],
'combatrig-compatibility': ['SCN-combatrig-import', 'SCN-cli-combatrig-import'],
'product-surface': ['SCN-product-site', 'SCN-dashboard-demo', 'SCN-console-readonly', 'SCN-console-lifecycle-blockers',
'SCN-console-owner-digest', 'SCN-demo-owner-digest', 'SCN-console-check-evidence', 'SCN-console-task-activity', 'SCN-console-operation-outcomes', 'SCN-console-live-refresh'],
@@ -796,7 +800,9 @@ export async function runCli(argv = process.argv.slice(2), {
}
const designCommand = ['init', 'analyze', 'domains', 'design', 'bootstrap'].includes(command);
const repositoryRoot = designCommand ? resolve(cwd, optionValue(argv, '--repo') ?? '.') : cwd;
const repository = inspectRepository(repositoryRoot);
const inspectedRepository = inspectRepository(repositoryRoot);
const repository = designCommand ? inspectedRepository
: inspectRepository(resolveInstalledProjectRoot(inspectedRepository.root, env));
if (command === 'profile') {
const operation = argv[1] ?? 'show';
const areaId = optionValue(argv, '--area');
@@ -997,6 +1003,11 @@ export async function runCli(argv = process.argv.slice(2), {
return 0;
}
if (command === 'install') {
if (argv.includes('--restore')) {
const result = restoreProject({ repository, env, dryRun: argv.includes('--dry-run'), authorized: argv.includes('--yes') });
print(result, { json });
return 0;
}
const proposalPath = optionValue(argv, '--proposal');
const runtimeOption = optionValue(argv, '--runtime');
const defaultRuntime = optionValue(argv, '--default-runtime');
@@ -1038,7 +1049,11 @@ export async function runCli(argv = process.argv.slice(2), {
}
persistProfileConfig(repository.root, config);
}
print(installed, { json });
print(argv.includes('--setup') ? { ...installed, setup: setupProject({ repository: inspectRepository(repository.root), env, authorized: true, parentOverride: optionValue(argv, '--parent') }) } : installed, { json });
return 0;
}
if (command === 'setup') {
print(setupProject({ repository, env, authorized: argv.includes('--yes'), dryRun: argv.includes('--dry-run'), parentOverride: optionValue(argv, '--parent') }), { json });
return 0;
}
if (command === 'doctor') {
+38
View File
@@ -0,0 +1,38 @@
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, realpathSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { projectStatePath } from './paths.mjs';
import { TorchError } from './errors.mjs';
const read = path => JSON.parse(readFileSync(path, 'utf8'));
const git = (root, args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
// A worktree's copied configuration is not authoritative. Only registered,
// same-installation worktrees may resolve to the current canonical definition.
export function resolveInstalledProjectRoot(root, env = process.env) {
root = realpathSync(root);
const path = join(root, '.torch', 'install-manifest.json');
if (!existsSync(path)) return root;
const manifest = read(path);
const stateRoot = projectStatePath(manifest.projectId, env);
const local = manifest.external?.find(entry => entry.type === 'local-state');
if (!local || resolve(local.path) !== resolve(stateRoot)) {
throw new TorchError('Installation state path does not match this project', { code: 'LOCAL_STATE_PATH_MISMATCH' });
}
const metadata = read(join(stateRoot, 'project.json'));
if (metadata.projectId !== manifest.projectId) {
throw new TorchError('Installation metadata belongs to another project', { code: 'LOCAL_STATE_METADATA_MISMATCH' });
}
const canonical = realpathSync(metadata.root);
if (canonical === root) return root;
const current = read(join(canonical, '.torch', 'install-manifest.json'));
const registered = current.external?.find(entry => entry.type === 'worktree' && existsSync(entry.path) && realpathSync(entry.path) === root);
const common = directory => realpathSync(resolve(directory, git(directory, ['rev-parse', '--git-common-dir'])));
const members = git(canonical, ['worktree', 'list', '--porcelain']).split('\n').filter(line => line.startsWith('worktree ')).map(line => realpathSync(line.slice(9)));
if (current.projectId !== manifest.projectId || current.installationId !== manifest.installationId
|| !registered || common(root) !== common(canonical) || !members.includes(root)
|| git(root, ['branch', '--show-current']) !== registered.branch) {
throw new TorchError('This checkout is not a registered worktree of the installation', { code: 'UNREGISTERED_PROJECT_WORKTREE' });
}
return canonical;
}
+55
View File
@@ -0,0 +1,55 @@
import { execFileSync } from 'node:child_process';
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { inspectRepository } from './git.mjs';
import { readInstallManifest } from './install.mjs';
import { projectStatePath } from './paths.mjs';
import { resolveInstalledProjectRoot } from './project-root.mjs';
import { createWorktrees, planWorktrees } from './worktrees.mjs';
import { TorchError } from './errors.mjs';
const git = (root, args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
export function setupProject({ repository, env = process.env, authorized = false, dryRun = false, parentOverride }) {
const root = resolveInstalledProjectRoot(repository.root, env);
repository = inspectRepository(root);
const manifest = readInstallManifest(root);
const owned = [...new Set([...manifest.created.map(record => record.path), '.torch/install-manifest.json'])];
const changed = owned.filter(path => git(root, ['status', '--porcelain=v1', '--', path]));
const staged = git(root, ['diff', '--cached', '--name-only']);
if (staged) throw new TorchError('Setup requires an empty staging area; preserve and finish your existing staged work first', { code: 'SETUP_STAGED_WORK', details: staged });
const unrelated = repository.dirtyEntries.filter(entry => !owned.includes(entry.slice(3)));
const worktrees = planWorktrees({ repository: { ...repository, dirtyEntries: unrelated }, parentOverride });
const plan = { action: 'setup', projectId: manifest.projectId, checkpointPaths: changed, worktrees, canProceed: worktrees.canProceed, sessionsStarted: false, mutationPerformed: false };
if (dryRun) return plan;
if (!authorized) throw new TorchError('Setup commits TORCH-owned configuration and provisions worktrees; review --dry-run then use --yes', { code: 'APPROVAL_REQUIRED' });
if (!plan.canProceed) throw new TorchError('Setup has blockers', { code: 'SETUP_BLOCKED', details: worktrees.conflicts });
const checkpoint = (paths, message) => {
const pending = paths.filter(path => git(root, ['status', '--porcelain=v1', '--', path]));
if (!pending.length) return null;
git(root, ['add', '--', ...pending]);
git(root, ['commit', '--only', '-m', message, '--', ...pending]);
return git(root, ['rev-parse', 'HEAD']);
};
const configurationCommit = checkpoint(changed, 'Checkpoint approved TORCH configuration');
const provisioned = createWorktrees({ repository: inspectRepository(root), parentOverride });
const registrationCommit = checkpoint(['.torch/install-manifest.json'], 'Checkpoint TORCH worktree registration');
return { ...plan, configurationCommit, registrationCommit, provisioned, mutationPerformed: Boolean(configurationCommit || registrationCommit || provisioned.mutationPerformed) };
}
export function restoreProject({ repository, env = process.env, authorized = false, dryRun = false }) {
const root = resolveInstalledProjectRoot(repository.root, env);
const manifest = readInstallManifest(root);
const path = join(projectStatePath(manifest.projectId, env), 'project.json');
const metadata = JSON.parse(readFileSync(path, 'utf8'));
const result = { action: 'restore-installation', projectId: manifest.projectId, installationId: manifest.installationId, wasDetached: Boolean(metadata.detachedAt), preserved: ['configuration', 'worktrees', 'branches', 'session history', 'provider profiles'], sessionsStarted: false, mutationPerformed: false };
if (dryRun) return result;
if (!authorized) throw new TorchError('Restoring a retained installation requires --yes', { code: 'APPROVAL_REQUIRED' });
if (metadata.detachedAt) {
const temporary = `${path}.restore-${process.pid}`;
writeFileSync(temporary, `${JSON.stringify({ ...metadata, detachedAt: null, attachedAt: new Date().toISOString() }, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
renameSync(temporary, path);
result.mutationPerformed = true;
}
return result;
}
+101
View File
@@ -0,0 +1,101 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, writeFileSync, cpSync } 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 { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject, readInstallManifest } from '../../src/kernel/install.mjs';
import { resolveInstalledProjectRoot } from '../../src/kernel/project-root.mjs';
import { setupProject, restoreProject } from '../../src/kernel/setup.mjs';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { createFleetBrief, detachFleet, planFleetDetach } from '../../src/runtime/lifecycle.mjs';
const git = (root, args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-setup-'));
const env = { ...process.env, XDG_DATA_HOME: join(root, 'external-state') };
git(root, ['init', '-b', 'main']);
git(root, ['config', 'user.name', 'Setup Test']);
git(root, ['config', 'user.email', '[email protected]']);
writeFileSync(join(root, '.gitignore'), 'external-state/\nworktrees/\n');
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
git(root, ['add', '.']);
git(root, ['commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.review = { status: 'approved', reviewedAt: '2026-09-30T00:00:00Z', reviewedBy: 'owner', notes: [] };
installProject({ repository, proposal, env });
return { root, env, parentOverride: join(root, 'worktrees') };
}
test('SCN-guided-setup: approved setup checkpoints owned files and provisions without agents', () => {
const f = fixture();
const before = git(f.root, ['rev-parse', 'HEAD']);
assert.equal(setupProject({ ...f, repository: inspectRepository(f.root), dryRun: true }).canProceed, true);
assert.equal(git(f.root, ['rev-parse', 'HEAD']), before);
assert.throws(() => setupProject({ ...f, repository: inspectRepository(f.root) }), { code: 'APPROVAL_REQUIRED' });
writeFileSync(join(f.root, 'app.js'), 'export const app = false;\n');
git(f.root, ['add', 'app.js']);
assert.throws(() => setupProject({ ...f, repository: inspectRepository(f.root), authorized: true }), { code: 'SETUP_STAGED_WORK' });
assert.equal(git(f.root, ['diff', '--cached', '--name-only']), 'app.js');
git(f.root, ['restore', '--staged', 'app.js']);
writeFileSync(join(f.root, 'app.js'), 'export const app = true;\n');
const result = setupProject({ ...f, repository: inspectRepository(f.root), authorized: true });
assert.equal(result.sessionsStarted, false);
assert.ok(result.configurationCommit);
assert.ok(result.registrationCommit);
assert.equal(git(f.root, ['status', '--porcelain']), '');
assert.ok(git(f.root, ['diff', '--name-only', before, 'HEAD']).split('\n').every(path => path.startsWith('.torch/')));
assert.ok(readInstallManifest(f.root).external.some(entry => entry.type === 'worktree'));
assert.equal(setupProject({ ...f, repository: inspectRepository(f.root), authorized: true }).mutationPerformed, false);
});
test('SCN-worktree-canonical-brief: current instructions win and unregistered or copied worktrees fail closed', () => {
const f = fixture();
setupProject({ ...f, repository: inspectRepository(f.root), authorized: true });
const entry = readInstallManifest(f.root).external.find(item => item.type === 'worktree');
const marker = 'Current canonical instructions, not the old worktree copy.';
writeFileSync(join(f.root, '.torch/prompts/COMMON.md'), marker);
const canonical = resolveInstalledProjectRoot(entry.path, f.env);
assert.equal(canonical, f.root);
const control = openControlPlane({ repositoryRoot: canonical, env: f.env });
try {
const brief = createFleetBrief({ repositoryRoot: canonical, controlPlane: control, areaId: entry.area });
assert.ok(brief.areas[0].prompt.includes(marker));
} finally { control.close(); }
const stray = join(f.root, 'worktrees', 'stray');
git(f.root, ['worktree', 'add', '-b', 'stray', stray]);
assert.throws(() => resolveInstalledProjectRoot(stray, f.env), { code: 'UNREGISTERED_PROJECT_WORKTREE' });
const copied = mkdtempSync(join(tmpdir(), 'torch-copy-'));
cpSync(join(entry.path, '.torch'), join(copied, '.torch'), { recursive: true });
git(copied, ['init', '-b', entry.branch]);
assert.throws(() => resolveInstalledProjectRoot(copied, f.env), { code: 'UNREGISTERED_PROJECT_WORKTREE' });
const localManifest = JSON.parse(readFileSync(join(entry.path, '.torch/install-manifest.json')));
localManifest.installationId = 'stale-installation';
writeFileSync(join(entry.path, '.torch/install-manifest.json'), JSON.stringify(localManifest));
assert.throws(() => resolveInstalledProjectRoot(entry.path, f.env), { code: 'UNREGISTERED_PROJECT_WORKTREE' });
});
test('SCN-install-restore: detach and explicit restore preserve configuration and worktree history without starting sessions', () => {
const f = fixture();
setupProject({ ...f, repository: inspectRepository(f.root), authorized: true });
const manifest = readInstallManifest(f.root);
const metadataPath = join(manifest.external.find(entry => entry.type === 'local-state').path, 'project.json');
const config = readFileSync(join(f.root, '.torch/torch.yaml'), 'utf8');
const heads = manifest.external.filter(entry => entry.type === 'worktree').map(entry => git(entry.path, ['rev-parse', 'HEAD']));
const control = openControlPlane({ repositoryRoot: f.root, env: f.env });
try {
detachFleet({ plan: planFleetDetach({ repositoryRoot: f.root, controlPlane: control, adapters: {} }), controlPlane: control, stopRuntime: () => { throw new Error('Offline setup must not call a provider'); } });
} finally { control.close(); }
const before = readFileSync(metadataPath, 'utf8');
assert.equal(restoreProject({ repository: inspectRepository(f.root), env: f.env, dryRun: true }).wasDetached, true);
assert.equal(readFileSync(metadataPath, 'utf8'), before);
assert.throws(() => restoreProject({ repository: inspectRepository(f.root), env: f.env }), { code: 'APPROVAL_REQUIRED' });
assert.equal(restoreProject({ repository: inspectRepository(f.root), env: f.env, authorized: true }).sessionsStarted, false);
assert.equal(JSON.parse(readFileSync(metadataPath)).detachedAt, null);
assert.equal(readFileSync(join(f.root, '.torch/torch.yaml'), 'utf8'), config);
assert.deepEqual(manifest.external.filter(entry => entry.type === 'worktree').map(entry => git(entry.path, ['rev-parse', 'HEAD'])), heads);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"schema": "torch.dev/release/v1alpha1",
"version": "0.1.0-alpha.1",
"version": "0.1.0-alpha.2",
"state": {
"reads": ["torch.dev/state/v1alpha1"],
"writes": "torch.dev/state/v1alpha1",