mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Fix canonical worktree briefs and add guided setup and safe reinstall
This commit is contained in:
1 parent
f72daadcba
commit
c11754ab18
13 files changed
+311
-13
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user