Fix canonical worktree briefs and add guided setup and safe reinstall

This commit is contained in:
thePR0M3TH3AN committed 2026-09-30 15:14:10 -04:00
1 parent f72daadcba
commit c11754ab18
13 files changed
+311 -13

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,
+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
+12
View File
@@ -451,6 +451,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
+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: ""
```
+2 -2
View File
@@ -37,7 +37,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 +45,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
+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,29 @@
# 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
Pending accepted alpha.2 build and normal uninstall/restore verification.
This is a retained-state reinstall, not a purge or deletion of the tool's
historical versions. Provider launches remain deliberately disabled.
+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",