atris 3.35.0 → 3.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +565 -265
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3029 -339
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
package/lib/fleet.js
CHANGED
|
@@ -12,14 +12,28 @@
|
|
|
12
12
|
// is just missions in flight, resumable the normal way.
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const os = require('os');
|
|
15
17
|
const path = require('path');
|
|
16
18
|
const { spawnSync } = require('child_process');
|
|
19
|
+
const {
|
|
20
|
+
appendBriefRecord,
|
|
21
|
+
mirrorBriefRecord,
|
|
22
|
+
stampBriefOutcome,
|
|
23
|
+
worktreeBaseRef,
|
|
24
|
+
} = require('./brief-ledger');
|
|
17
25
|
const { RUNNER_PROFILE_DEFS, buildRunnerCommand } = require('./runner-command');
|
|
26
|
+
const { resolveDefaultVerifier } = require('./default-verifier');
|
|
27
|
+
const { rankEngines } = require('./router-brain');
|
|
28
|
+
const {
|
|
29
|
+
buildOneLapValidatorPrompt,
|
|
30
|
+
parseOneLapValidatorVerdict,
|
|
31
|
+
} = require('./one-lap-validator');
|
|
18
32
|
const { listWorktrees } = require('../commands/worktree');
|
|
19
33
|
|
|
20
34
|
// Lanes a fleet may never staff on its own: the human keeps irreversible
|
|
21
35
|
// calls. Mirrors the autoland denied lanes.
|
|
22
|
-
const DENIED_TAGS = ['billing', 'deploy', 'security', 'customer', 'external', 'feedback'
|
|
36
|
+
const DENIED_TAGS = ['billing', 'deploy', 'security', 'customer', 'external', 'feedback'];
|
|
23
37
|
|
|
24
38
|
// ---------------------------------------------------------------------------
|
|
25
39
|
// T1 — dispatch primitive
|
|
@@ -37,12 +51,28 @@ function parseDoneCheck(text) {
|
|
|
37
51
|
};
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
// Working-method kernel every dispatched engine inherits, distilled from
|
|
55
|
+
// atris/skills/fable-method: verify unpiped, receipts, caller sweeps,
|
|
56
|
+
// hypothesis-driven unsticking, smallest diff.
|
|
57
|
+
const METHOD_KERNEL = [
|
|
58
|
+
'Read this whole brief before acting; locate every file you will touch before editing any of them.',
|
|
59
|
+
'Never pipe a verify or test command through tail/head/grep — run it bare and read the real exit code.',
|
|
60
|
+
'Done requires a receipt: paste the exact verify command and its final output lines in your report.',
|
|
61
|
+
'Before deleting or renaming any function or call site, grep the whole repo for its callers; a caller outside your change means the contract stays intact.',
|
|
62
|
+
'Stuck? Never run the same failing command a third time — write 3 one-line hypotheses, then run the cheapest test that discriminates between them.',
|
|
63
|
+
'Smallest diff that satisfies Done wins; prefer deleting code over adding it.',
|
|
64
|
+
];
|
|
65
|
+
|
|
40
66
|
// The bounded prompt every engine gets. Same contract the manual flight used:
|
|
41
67
|
// isolated worktree, commit never push, MAP first, focused verify, report.
|
|
42
|
-
function buildFleetPrompt(task, { worktreePath } = {}) {
|
|
68
|
+
function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
|
|
43
69
|
const ref = task.display_id || task.id || 'TASK';
|
|
44
70
|
const title = String(task.title || '').trim();
|
|
45
|
-
const { done, check } = parseDoneCheck(title);
|
|
71
|
+
const { done, check: declaredCheck } = parseDoneCheck(title);
|
|
72
|
+
const check = declaredCheck || resolveDefaultVerifier(worktreePath || process.cwd());
|
|
73
|
+
const commitRule = yolo
|
|
74
|
+
? 'Commit on the current branch with a plain-English message, then land it yourself: run atris worktree ship --message "<msg>" --verify "npm run test:fast && node --test <focused files>" --merge and report the PR URL. If ship reports a rebase conflict or the verify fails, stop and report; never resolve conflicts yourself.'
|
|
75
|
+
: 'Commit on the current branch with a clear message. Do not push. Do not create branches.';
|
|
46
76
|
const lines = [
|
|
47
77
|
'First, run `atris worktree guard`; if it fails, stop immediately, report back, and do not edit anything. Do this before any file edit.',
|
|
48
78
|
'',
|
|
@@ -52,13 +82,14 @@ function buildFleetPrompt(task, { worktreePath } = {}) {
|
|
|
52
82
|
'',
|
|
53
83
|
];
|
|
54
84
|
if (done) lines.push(`Done criteria: ${done}`, '');
|
|
55
|
-
|
|
85
|
+
lines.push(`Check: ${check}`, '');
|
|
56
86
|
lines.push(
|
|
57
87
|
'Rules:',
|
|
58
88
|
'- Read atris/MAP.md first to locate the code; never guess file locations.',
|
|
59
89
|
'- Run git status first. Stage ONLY files you changed. Never revert or touch files another agent modified.',
|
|
60
|
-
'- Include or update a focused regression test; run it with node --test before committing.',
|
|
61
|
-
|
|
90
|
+
'- Include or update a focused regression test; run it with npm run test:fast && node --test <focused files> before committing.',
|
|
91
|
+
`- ${commitRule}`,
|
|
92
|
+
...METHOD_KERNEL.map((rule) => `- ${rule}`),
|
|
62
93
|
'',
|
|
63
94
|
'Final report (plain text): files changed, test command + result, commit sha (or say the commit failed and why).'
|
|
64
95
|
);
|
|
@@ -72,6 +103,17 @@ function buildFleetPrompt(task, { worktreePath } = {}) {
|
|
|
72
103
|
// spawn; template engines (cursor/codex/devin) ignore it — their CLIs manage
|
|
73
104
|
// their own permissions.
|
|
74
105
|
const FLEET_ALLOWED_TOOLS = 'Bash,Read,Edit,Write,Grep,Glob';
|
|
106
|
+
const VALIDATOR_ALLOWED_TOOLS = 'Bash,Read,Grep,Glob';
|
|
107
|
+
const DEAD_ENGINE_OUTPUT_PATTERNS = Object.freeze([
|
|
108
|
+
'usage limit',
|
|
109
|
+
'purchase more credits',
|
|
110
|
+
'rate limit',
|
|
111
|
+
]);
|
|
112
|
+
const YOLO_ENGINE_FLAGS = Object.freeze({
|
|
113
|
+
codex: '--dangerously-bypass-approvals-and-sandbox',
|
|
114
|
+
claude: '--dangerously-skip-permissions',
|
|
115
|
+
});
|
|
116
|
+
const DISPATCH_SELF_LAND_TARGET = 'origin/master';
|
|
75
117
|
|
|
76
118
|
function realpathOrResolve(value) {
|
|
77
119
|
const resolved = path.resolve(String(value));
|
|
@@ -95,16 +137,29 @@ function assertIsolatedWorktree(worktreePath, root = process.cwd()) {
|
|
|
95
137
|
return { worktreePath: resolvedWorktree, primaryRoot };
|
|
96
138
|
}
|
|
97
139
|
|
|
98
|
-
function buildEngineCommand(engineName, promptFile) {
|
|
140
|
+
function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = false, allowedTools = FLEET_ALLOWED_TOOLS } = {}) {
|
|
99
141
|
if (!RUNNER_PROFILE_DEFS[engineName]) throw new Error(`unknown engine "${engineName}"`);
|
|
100
142
|
const prev = process.env.ATRIS_RUNNER_PROFILE;
|
|
101
143
|
process.env.ATRIS_RUNNER_PROFILE = engineName;
|
|
102
144
|
try {
|
|
103
|
-
|
|
145
|
+
let cmd = buildRunnerCommand({ promptFile, allowedTools });
|
|
104
146
|
// devin's default is read-only for writes; fleet builds ALWAYS run in an
|
|
105
147
|
// isolated worktree, so the conductor grants write permission here and
|
|
106
148
|
// only here (the profile itself stays safe for non-worktree ticks).
|
|
149
|
+
if (sealed && engineName === 'codex') {
|
|
150
|
+
cmd = cmd.replace(/\bexec\b/, 'exec --sandbox workspace-write --ephemeral --ignore-user-config --ignore-rules');
|
|
151
|
+
}
|
|
152
|
+
if (sealed && engineName === 'claude') {
|
|
153
|
+
cmd = `${cmd} --safe-mode --no-session-persistence --permission-mode acceptEdits --settings '${JSON.stringify({ sandbox: { enabled: true, autoAllowBashIfSandboxed: true } })}'`;
|
|
154
|
+
}
|
|
155
|
+
if (sealed && engineName === 'cursor') cmd = `${cmd} --sandbox enabled`;
|
|
156
|
+
if (sealed && engineName === 'devin') return cmd.replace(/^devin -p /, 'devin -p --sandbox --permission-mode accept-edits ');
|
|
157
|
+
if (sealed && engineName === 'grok') {
|
|
158
|
+
cmd = `${cmd.replace(/\s+--always-approve\b/, '')} --sandbox enabled --permission-mode acceptEdits --no-memory --no-subagents`;
|
|
159
|
+
}
|
|
107
160
|
if (engineName === 'devin') return cmd.replace(/^devin -p /, 'devin -p --permission-mode dangerous ');
|
|
161
|
+
if (yolo && engineName === 'codex') cmd = cmd.replace(/\bexec\b/, `exec ${YOLO_ENGINE_FLAGS.codex}`);
|
|
162
|
+
if (yolo && engineName === 'claude') cmd = `${cmd} ${YOLO_ENGINE_FLAGS.claude}`;
|
|
108
163
|
return cmd;
|
|
109
164
|
} finally {
|
|
110
165
|
if (prev === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
|
|
@@ -112,28 +167,372 @@ function buildEngineCommand(engineName, promptFile) {
|
|
|
112
167
|
}
|
|
113
168
|
}
|
|
114
169
|
|
|
170
|
+
function trackedSandboxPids(stateFile, leaseFile) {
|
|
171
|
+
const pids = new Set();
|
|
172
|
+
let state = {};
|
|
173
|
+
try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch {}
|
|
174
|
+
const pgid = Number(state.pgid);
|
|
175
|
+
if (Number.isInteger(pgid) && pgid > 0) {
|
|
176
|
+
const grouped = spawnSync('/usr/bin/pgrep', ['-g', String(pgid)], { encoding: 'utf8' });
|
|
177
|
+
for (const value of String(grouped.stdout || '').trim().split(/\s+/)) {
|
|
178
|
+
const pid = Number(value);
|
|
179
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const leased = spawnSync('/usr/sbin/lsof', ['-t', leaseFile], { encoding: 'utf8' });
|
|
183
|
+
for (const value of String(leased.stdout || '').trim().split(/\s+/)) {
|
|
184
|
+
const pid = Number(value);
|
|
185
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
186
|
+
}
|
|
187
|
+
if (state.cwd) {
|
|
188
|
+
const rooted = spawnSync('/usr/sbin/lsof', ['-a', '-d', 'cwd', '+D', String(state.cwd), '-t'], { encoding: 'utf8' });
|
|
189
|
+
for (const value of String(rooted.stdout || '').trim().split(/\s+/)) {
|
|
190
|
+
const pid = Number(value);
|
|
191
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
pids.delete(process.pid);
|
|
195
|
+
return { pgid, pids: [...pids] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function terminateTrackedSandbox(stateFile, leaseFile) {
|
|
199
|
+
for (const signal of ['SIGTERM', 'SIGKILL', 'SIGKILL']) {
|
|
200
|
+
const tracked = trackedSandboxPids(stateFile, leaseFile);
|
|
201
|
+
if (tracked.pgid > 0) {
|
|
202
|
+
try { process.kill(-tracked.pgid, signal); } catch {}
|
|
203
|
+
}
|
|
204
|
+
for (const pid of tracked.pids) {
|
|
205
|
+
try { process.kill(pid, signal); } catch {}
|
|
206
|
+
}
|
|
207
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function terminateSameSandboxProfile(executable, args, options) {
|
|
212
|
+
if (executable !== '/usr/bin/sandbox-exec' || args[0] !== '-p' || !args[1]) return;
|
|
213
|
+
spawnSync(executable, ['-p', args[1], '/bin/kill', '-KILL', '-1'], {
|
|
214
|
+
cwd: options.cwd,
|
|
215
|
+
env: options.env,
|
|
216
|
+
encoding: 'utf8',
|
|
217
|
+
stdio: 'ignore',
|
|
218
|
+
timeout: 5000,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function runInReapedProcessGroup(executable, args, options, controlDir, statusFile) {
|
|
223
|
+
if (!controlDir || !statusFile) throw new Error('sealed execution requires isolated control and status paths');
|
|
224
|
+
const supervisor = path.join(controlDir, 'sandbox-supervisor.js');
|
|
225
|
+
const stateFile = path.join(controlDir, 'sandbox-process.json');
|
|
226
|
+
const leaseFile = path.join(controlDir, 'sandbox-process.lease');
|
|
227
|
+
fs.writeFileSync(supervisor, [
|
|
228
|
+
"'use strict';",
|
|
229
|
+
"const fs = require('node:fs');",
|
|
230
|
+
"const { spawn, spawnSync } = require('node:child_process');",
|
|
231
|
+
"const [stateFile, leaseFile, statusFile, executable, ...args] = process.argv.slice(2);",
|
|
232
|
+
"const leaseFd = fs.openSync(leaseFile, 'w', 0o600);",
|
|
233
|
+
"const statusFd = fs.openSync(statusFile, 'w', 0o600);",
|
|
234
|
+
"const child = spawn(executable, args, { cwd: process.cwd(), env: process.env, detached: true, stdio: ['ignore', 'inherit', 'inherit', leaseFd, statusFd] });",
|
|
235
|
+
"fs.closeSync(leaseFd);",
|
|
236
|
+
"fs.closeSync(statusFd);",
|
|
237
|
+
"fs.writeFileSync(stateFile, JSON.stringify({ pgid: child.pid, cwd: process.cwd() }) + '\\n', { mode: 0o600 });",
|
|
238
|
+
"let stopping = false;",
|
|
239
|
+
"const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
|
|
240
|
+
"function trackedPids() {",
|
|
241
|
+
" const pids = new Set();",
|
|
242
|
+
" for (const [bin, argv] of [['/usr/bin/pgrep', ['-g', String(child.pid)]], ['/usr/sbin/lsof', ['-t', leaseFile]], ['/usr/sbin/lsof', ['-a', '-d', 'cwd', '+D', process.cwd(), '-t']]]) {",
|
|
243
|
+
" const found = spawnSync(bin, argv, { encoding: 'utf8' });",
|
|
244
|
+
" for (const value of String(found.stdout || '').trim().split(/\\s+/)) {",
|
|
245
|
+
" const pid = Number(value);",
|
|
246
|
+
" if (Number.isInteger(pid) && pid > 0) pids.add(pid);",
|
|
247
|
+
" }",
|
|
248
|
+
" }",
|
|
249
|
+
" pids.delete(process.pid);",
|
|
250
|
+
" return [...pids];",
|
|
251
|
+
"}",
|
|
252
|
+
"async function stop(code) {",
|
|
253
|
+
" if (stopping) return;",
|
|
254
|
+
" stopping = true;",
|
|
255
|
+
" if (executable === '/usr/bin/sandbox-exec' && args[0] === '-p' && args[1]) {",
|
|
256
|
+
" spawnSync(executable, ['-p', args[1], '/bin/kill', '-KILL', '-1'], { cwd: process.cwd(), env: process.env, stdio: 'ignore', timeout: 5000 });",
|
|
257
|
+
" }",
|
|
258
|
+
" for (const [signal, delay] of [['SIGTERM', 100], ['SIGKILL', 100], ['SIGKILL', 100]]) {",
|
|
259
|
+
" try { process.kill(-child.pid, signal); } catch {}",
|
|
260
|
+
" for (const pid of trackedPids()) { try { process.kill(pid, signal); } catch {} }",
|
|
261
|
+
" await wait(delay);",
|
|
262
|
+
" }",
|
|
263
|
+
" let exitCode = Number.isInteger(code) ? code : 128;",
|
|
264
|
+
" try {",
|
|
265
|
+
" const savedText = fs.readFileSync(statusFile, 'utf8').trim();",
|
|
266
|
+
" const saved = Number(savedText);",
|
|
267
|
+
" if (savedText && Number.isInteger(saved) && saved >= 0 && saved <= 255) exitCode = saved;",
|
|
268
|
+
" } catch {}",
|
|
269
|
+
" process.exit(exitCode);",
|
|
270
|
+
"}",
|
|
271
|
+
"child.once('error', () => { void stop(1); });",
|
|
272
|
+
"child.once('exit', (code) => { void stop(code); });",
|
|
273
|
+
"for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => { void stop(143); });",
|
|
274
|
+
'',
|
|
275
|
+
].join('\n'), { mode: 0o700 });
|
|
276
|
+
let result;
|
|
277
|
+
try {
|
|
278
|
+
result = spawnSync(process.execPath, [supervisor, stateFile, leaseFile, statusFile, executable, ...args], options);
|
|
279
|
+
} finally {
|
|
280
|
+
terminateSameSandboxProfile(executable, args, options);
|
|
281
|
+
terminateTrackedSandbox(stateFile, leaseFile);
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function sandboxLifecycleWrapper(runtimeDir, controlDir) {
|
|
287
|
+
const wrapper = path.join(runtimeDir, 'sandbox-lifecycle.sh');
|
|
288
|
+
const statusFile = path.join(controlDir, 'sandbox-status');
|
|
289
|
+
fs.writeFileSync(wrapper, [
|
|
290
|
+
'#!/bin/sh',
|
|
291
|
+
'"$@" 4>&-',
|
|
292
|
+
'status=$?',
|
|
293
|
+
'printf "%s\\n" "$status" >&4',
|
|
294
|
+
"trap '' EXIT TERM INT HUP",
|
|
295
|
+
'/bin/kill -TERM -1 2>/dev/null || true',
|
|
296
|
+
'/bin/sleep 0.1',
|
|
297
|
+
'/bin/kill -KILL -1 2>/dev/null || true',
|
|
298
|
+
'exit "$status"',
|
|
299
|
+
'',
|
|
300
|
+
].join('\n'), { mode: 0o700 });
|
|
301
|
+
return { wrapper, statusFile };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function dispatchResultOutput(result) {
|
|
305
|
+
if (!result) return '';
|
|
306
|
+
return [
|
|
307
|
+
result.report,
|
|
308
|
+
result.stdout,
|
|
309
|
+
result.stderr,
|
|
310
|
+
].map((value) => String(value || '')).filter(Boolean).join('\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function dispatchResultExitCode(result) {
|
|
314
|
+
if (!result || typeof result !== 'object') return 0;
|
|
315
|
+
if (Object.prototype.hasOwnProperty.call(result, 'exitCode')) return result.exitCode;
|
|
316
|
+
if (Object.prototype.hasOwnProperty.call(result, 'status')) return result.status;
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function dispatchTaskId(task) {
|
|
321
|
+
return task && (task.display_id || task.task_id || task.id) ? String(task.display_id || task.task_id || task.id) : '';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function dispatchBriefAuthor(worktreePath, fallback = 'orb') {
|
|
325
|
+
try {
|
|
326
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(worktreePath, '.atris', 'agent-worktree.json'), 'utf8'));
|
|
327
|
+
return String(sidecar.owner || sidecar.member || sidecar.agent || fallback).trim() || fallback;
|
|
328
|
+
} catch {
|
|
329
|
+
return fallback;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function captureDispatchBrief({ root, task, engine, worktreePath, prompt, missionId = '', author = '', yolo = false }) {
|
|
334
|
+
const promptText = prompt || buildFleetPrompt(task, { worktreePath, yolo });
|
|
335
|
+
const record = appendBriefRecord(root, {
|
|
336
|
+
author: author || dispatchBriefAuthor(worktreePath, 'orb'),
|
|
337
|
+
engine,
|
|
338
|
+
task_id: dispatchTaskId(task),
|
|
339
|
+
mission_id: missionId,
|
|
340
|
+
prompt_text: promptText,
|
|
341
|
+
context: {
|
|
342
|
+
worktree: worktreePath,
|
|
343
|
+
base_ref: worktreeBaseRef(worktreePath, ''),
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
if (path.resolve(root) !== path.resolve(worktreePath || root)) {
|
|
347
|
+
mirrorBriefRecord(worktreePath, record);
|
|
348
|
+
}
|
|
349
|
+
return record;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function stampDispatchBrief(root, briefId, result, note) {
|
|
353
|
+
if (!briefId) return { ok: false, error: 'missing brief id' };
|
|
354
|
+
try {
|
|
355
|
+
return stampBriefOutcome(root, briefId, { result, note });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
return { ok: false, error: err.message };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function detectDeadEngineDispatch(result) {
|
|
362
|
+
const exitCode = dispatchResultExitCode(result);
|
|
363
|
+
if (exitCode === 0) return null;
|
|
364
|
+
const output = dispatchResultOutput(result).toLowerCase();
|
|
365
|
+
const pattern = DEAD_ENGINE_OUTPUT_PATTERNS.find((p) => output.includes(p));
|
|
366
|
+
if (pattern) return { reason: 'usage_limit', pattern };
|
|
367
|
+
return { reason: 'nonzero_exit', exitCode };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function normalizeInstalledEngines(engines) {
|
|
371
|
+
return [...new Set((engines || [])
|
|
372
|
+
.map((entry) => (typeof entry === 'string' ? entry : entry && entry.name))
|
|
373
|
+
.map((name) => String(name || '').trim())
|
|
374
|
+
.filter((name) => FLEET_CAPABLE.includes(name) && RUNNER_PROFILE_DEFS[name]))];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function installedFleetEngines(root) {
|
|
378
|
+
const { roster } = require('../commands/engine');
|
|
379
|
+
return normalizeInstalledEngines(roster(root).filter((e) => e.installed));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function rankFleetEngines(engines, root = process.cwd()) {
|
|
383
|
+
return rankEngines(normalizeInstalledEngines(engines), {
|
|
384
|
+
root,
|
|
385
|
+
taskType: 'executor',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function nextInstalledFleetEngine(current, { root = process.cwd(), installedEngines = null } = {}) {
|
|
390
|
+
const engines = installedEngines ? normalizeInstalledEngines(installedEngines) : installedFleetEngines(root);
|
|
391
|
+
const currentName = String(current || '').trim();
|
|
392
|
+
if (!engines.length) return '';
|
|
393
|
+
const index = engines.indexOf(currentName);
|
|
394
|
+
const ordered = index === -1
|
|
395
|
+
? engines
|
|
396
|
+
: [...engines.slice(index + 1), ...engines.slice(0, index)];
|
|
397
|
+
const candidates = ordered.filter((name) => name && name !== currentName);
|
|
398
|
+
return rankFleetEngines(candidates, root)[0] || '';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizeDispatchResult(result, engineName) {
|
|
402
|
+
const normalized = { ...(result || {}) };
|
|
403
|
+
normalized.engine = normalized.engine || engineName;
|
|
404
|
+
normalized.exitCode = dispatchResultExitCode(normalized);
|
|
405
|
+
return normalized;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function failedDispatchLeg(result, engineName) {
|
|
409
|
+
return {
|
|
410
|
+
engine: String(result && result.engine || engineName || ''),
|
|
411
|
+
exitCode: dispatchResultExitCode(result),
|
|
412
|
+
stderr: String(result && result.stderr || '').slice(-2000),
|
|
413
|
+
report: String(result && result.report || '').slice(-2000),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function dispatchEntryWithRestaff({
|
|
418
|
+
entry,
|
|
419
|
+
engine,
|
|
420
|
+
root,
|
|
421
|
+
dispatch,
|
|
422
|
+
installedEngines = null,
|
|
423
|
+
restaffState,
|
|
424
|
+
}) {
|
|
425
|
+
const runOnce = async (engineName) => {
|
|
426
|
+
const prompt = entry.prompt || buildFleetPrompt(entry.task, { worktreePath: entry.worktreePath, yolo: entry.yolo });
|
|
427
|
+
const brief = captureDispatchBrief({
|
|
428
|
+
root,
|
|
429
|
+
task: entry.task,
|
|
430
|
+
engine: engineName,
|
|
431
|
+
worktreePath: entry.worktreePath,
|
|
432
|
+
prompt,
|
|
433
|
+
missionId: entry.mission_id || entry.missionId || '',
|
|
434
|
+
author: entry.author || '',
|
|
435
|
+
yolo: entry.yolo,
|
|
436
|
+
});
|
|
437
|
+
try {
|
|
438
|
+
const runResult = normalizeDispatchResult(await dispatch({ ...entry, engine: engineName, prompt, brief_id: brief.brief_id, briefId: brief.brief_id, skipBriefCapture: true }), engineName);
|
|
439
|
+
if (!runResult.brief_id) runResult.brief_id = brief.brief_id;
|
|
440
|
+
return runResult;
|
|
441
|
+
} catch (err) {
|
|
442
|
+
return normalizeDispatchResult({
|
|
443
|
+
brief_id: brief.brief_id,
|
|
444
|
+
exitCode: 1,
|
|
445
|
+
report: '',
|
|
446
|
+
stderr: String(err && err.message || err),
|
|
447
|
+
}, engineName);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const first = await runOnce(engine);
|
|
452
|
+
const deadEngine = detectDeadEngineDispatch(first);
|
|
453
|
+
if (!deadEngine) return first;
|
|
454
|
+
stampDispatchBrief(root, first.brief_id, 'fail', `restaffed from ${engine}: ${deadEngine.reason}`);
|
|
455
|
+
|
|
456
|
+
const outage = { from: engine, reason: deadEngine.reason };
|
|
457
|
+
if (restaffState.used) {
|
|
458
|
+
return { ...first, deadEngine: { ...outage, skipped: 'already_restaffed' } };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const fallback = nextInstalledFleetEngine(engine, { root, installedEngines });
|
|
462
|
+
if (!fallback) {
|
|
463
|
+
return { ...first, deadEngine: { ...outage, skipped: 'no_fallback_engine' } };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
restaffState.used = true;
|
|
467
|
+
const fallbackResult = await runOnce(fallback);
|
|
468
|
+
return {
|
|
469
|
+
...fallbackResult,
|
|
470
|
+
restaffed: {
|
|
471
|
+
from: engine,
|
|
472
|
+
to: fallback,
|
|
473
|
+
reason: deadEngine.reason,
|
|
474
|
+
failed_legs: [failedDispatchLeg(first, engine)],
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
115
479
|
// Run one engine on one task in one worktree. Blocking; the conductor runs
|
|
116
480
|
// dispatches in parallel via child processes, not threads. `runner` is
|
|
117
481
|
// injectable for tests. `prompt` is injectable too: a caller-supplied prompt
|
|
118
482
|
// (e.g. `atris engine dispatch --prompt-file`) skips the generated
|
|
119
483
|
// buildFleetPrompt text entirely.
|
|
120
|
-
function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), timeoutMs = 900000, runner = null, prompt: promptOverride = '' }) {
|
|
484
|
+
function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), timeoutMs = 900000, runner = null, prompt: promptOverride = '', yolo = false, sealed = false, briefId = '', skipBriefCapture = false, environment = null, allowedTools = FLEET_ALLOWED_TOOLS }) {
|
|
121
485
|
assertIsolatedWorktree(worktreePath, root);
|
|
122
|
-
const prompt = promptOverride || buildFleetPrompt(task, { worktreePath });
|
|
123
|
-
|
|
486
|
+
const prompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo });
|
|
487
|
+
let capturedBriefId = briefId;
|
|
488
|
+
if (!skipBriefCapture) {
|
|
489
|
+
capturedBriefId = captureDispatchBrief({ root, task, engine, worktreePath, prompt, yolo }).brief_id;
|
|
490
|
+
}
|
|
491
|
+
const runtimeDir = String(environment && environment.ATRIS_ONE_LAP_RUNTIME_DIR || '');
|
|
492
|
+
const promptFile = path.join(sealed && runtimeDir ? runtimeDir : path.join(worktreePath, '.atris'), `fleet-prompt-${task.display_id || 'task'}.md`);
|
|
124
493
|
fs.mkdirSync(path.dirname(promptFile), { recursive: true });
|
|
125
494
|
fs.writeFileSync(promptFile, prompt);
|
|
126
|
-
const command = buildEngineCommand(engine, promptFile);
|
|
127
|
-
const exec = runner || ((cmd) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
495
|
+
const command = buildEngineCommand(engine, promptFile, { yolo, sealed, allowedTools });
|
|
496
|
+
const exec = runner || ((cmd) => {
|
|
497
|
+
const childEnv = sealed && environment
|
|
498
|
+
? { ...environment }
|
|
499
|
+
: (environment ? { ...process.env, ...environment } : { ...process.env });
|
|
500
|
+
const sandboxProfile = String(childEnv.ATRIS_ONE_LAP_SANDBOX_PROFILE || '');
|
|
501
|
+
const cleanupRuntimeDir = String(childEnv.ATRIS_ONE_LAP_RUNTIME_DIR || '');
|
|
502
|
+
const cleanupControlDir = String(childEnv.ATRIS_ONE_LAP_CONTROL_DIR || '');
|
|
503
|
+
const lifecycleWrapper = String(childEnv.ATRIS_ONE_LAP_LIFECYCLE_WRAPPER || '');
|
|
504
|
+
const statusFile = String(childEnv.ATRIS_ONE_LAP_STATUS_FILE || '');
|
|
505
|
+
delete childEnv.ATRIS_ONE_LAP_SANDBOX_PROFILE;
|
|
506
|
+
delete childEnv.ATRIS_ONE_LAP_RUNTIME_DIR;
|
|
507
|
+
delete childEnv.ATRIS_ONE_LAP_CONTROL_DIR;
|
|
508
|
+
delete childEnv.ATRIS_ONE_LAP_LIFECYCLE_WRAPPER;
|
|
509
|
+
delete childEnv.ATRIS_ONE_LAP_STATUS_FILE;
|
|
510
|
+
try {
|
|
511
|
+
const executable = sandboxProfile ? '/usr/bin/sandbox-exec' : 'sh';
|
|
512
|
+
const args = sandboxProfile
|
|
513
|
+
? ['-p', sandboxProfile, lifecycleWrapper, '/bin/sh', '-c', cmd]
|
|
514
|
+
: ['-c', cmd];
|
|
515
|
+
const spawnOptions = {
|
|
516
|
+
cwd: worktreePath,
|
|
517
|
+
env: childEnv,
|
|
518
|
+
encoding: 'utf8',
|
|
519
|
+
timeout: timeoutMs,
|
|
520
|
+
};
|
|
521
|
+
return sandboxProfile
|
|
522
|
+
? runInReapedProcessGroup(executable, args, spawnOptions, cleanupControlDir, statusFile)
|
|
523
|
+
: spawnSync(executable, args, spawnOptions);
|
|
524
|
+
} finally {
|
|
525
|
+
if (cleanupRuntimeDir) fs.rmSync(cleanupRuntimeDir, { recursive: true, force: true });
|
|
526
|
+
if (cleanupControlDir) fs.rmSync(cleanupControlDir, { recursive: true, force: true });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
132
529
|
const result = exec(command);
|
|
133
530
|
return {
|
|
134
531
|
task: task.display_id || task.id,
|
|
135
532
|
engine,
|
|
136
533
|
worktreePath,
|
|
534
|
+
promptFile,
|
|
535
|
+
brief_id: capturedBriefId || null,
|
|
137
536
|
command,
|
|
138
537
|
exitCode: result.status,
|
|
139
538
|
report: String(result.stdout || '').slice(-8000),
|
|
@@ -146,12 +545,13 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
146
545
|
|
|
147
546
|
function taskTags(task) {
|
|
148
547
|
const fromTags = Array.isArray(task.tags) ? task.tags : [];
|
|
548
|
+
const fromTag = task && task.tag ? [task.tag] : [];
|
|
149
549
|
// Tags added after creation live in metadata.tags (`atris task tag`); a
|
|
150
550
|
// fleet that only read task.tags/title hashtags would ignore an owner-hold
|
|
151
551
|
// flag stamped on a live task and keep restaffing it (CLI-879).
|
|
152
552
|
const fromMeta = task && task.metadata && Array.isArray(task.metadata.tags) ? task.metadata.tags : [];
|
|
153
553
|
const fromTitle = (String(task.title || '').match(/#([a-z0-9-]+)/gi) || []).map((t) => t.slice(1));
|
|
154
|
-
return [...fromTags, ...fromMeta, ...fromTitle].map((t) => String(t).toLowerCase());
|
|
554
|
+
return [...fromTag, ...fromTags, ...fromMeta, ...fromTitle].map((t) => String(t).toLowerCase());
|
|
155
555
|
}
|
|
156
556
|
|
|
157
557
|
// A task flagged for a human decision is never fleet-staffable, whatever its
|
|
@@ -250,15 +650,24 @@ function landArrival({ worktreePath, git = null }) {
|
|
|
250
650
|
|
|
251
651
|
module.exports = {
|
|
252
652
|
DENIED_TAGS,
|
|
653
|
+
DEAD_ENGINE_OUTPUT_PATTERNS,
|
|
253
654
|
get FLEET_CAPABLE() { return FLEET_CAPABLE; },
|
|
254
655
|
get runFleetFlight() { return runFleetFlight; },
|
|
255
656
|
get focusedCheck() { return focusedCheck; },
|
|
256
657
|
get dispatchCheck() { return dispatchCheck; },
|
|
257
658
|
get runDispatchFlight() { return runDispatchFlight; },
|
|
659
|
+
YOLO_ENGINE_FLAGS,
|
|
660
|
+
DISPATCH_SELF_LAND_TARGET,
|
|
258
661
|
parseDoneCheck,
|
|
662
|
+
METHOD_KERNEL,
|
|
259
663
|
buildFleetPrompt,
|
|
260
664
|
assertIsolatedWorktree,
|
|
261
665
|
buildEngineCommand,
|
|
666
|
+
isSafeLane,
|
|
667
|
+
taskTags,
|
|
668
|
+
detectDeadEngineDispatch,
|
|
669
|
+
rankFleetEngines,
|
|
670
|
+
nextInstalledFleetEngine,
|
|
262
671
|
dispatchToEngine,
|
|
263
672
|
taskTags,
|
|
264
673
|
isHumanHoldTag,
|
|
@@ -269,6 +678,7 @@ module.exports = {
|
|
|
269
678
|
assignEngines,
|
|
270
679
|
landArrival,
|
|
271
680
|
fleetShipArgs,
|
|
681
|
+
defaultSelfLandCheck,
|
|
272
682
|
};
|
|
273
683
|
|
|
274
684
|
// ---------------------------------------------------------------------------
|
|
@@ -276,10 +686,1101 @@ module.exports = {
|
|
|
276
686
|
|
|
277
687
|
// Engines that can edit a repo headlessly. atris-fast (ax) is a chat lane,
|
|
278
688
|
// not a repo worker — it keeps owning normal mission ticks, not fleet builds.
|
|
279
|
-
const FLEET_CAPABLE = ['claude', 'codex', 'cursor', 'devin'];
|
|
689
|
+
const FLEET_CAPABLE = ['claude', 'codex', 'cursor', 'devin', 'grok'];
|
|
280
690
|
|
|
691
|
+
let receiptSequence = 0;
|
|
281
692
|
function nowStamp() {
|
|
282
|
-
|
|
693
|
+
receiptSequence += 1;
|
|
694
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, '');
|
|
695
|
+
return `${stamp}-p${process.pid}-${receiptSequence}`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function shellSingleQuote(value) {
|
|
699
|
+
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function canonicalPath(value) {
|
|
703
|
+
const resolved = path.resolve(String(value || ''));
|
|
704
|
+
try { return fs.realpathSync(resolved); } catch { return resolved; }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function seatbeltRule(kind, value) {
|
|
708
|
+
return `(${kind} ${JSON.stringify(canonicalPath(value))})`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function seatbeltAncestors(target) {
|
|
712
|
+
const out = [];
|
|
713
|
+
let current = path.dirname(canonicalPath(target));
|
|
714
|
+
while (current && current !== path.dirname(current)) {
|
|
715
|
+
out.push(current);
|
|
716
|
+
current = path.dirname(current);
|
|
717
|
+
}
|
|
718
|
+
out.push('/');
|
|
719
|
+
return out.reverse();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function reviewOnlySandboxProfile({ worktreePath, gitDir, quarantine, runtimeTmp, engine = '', network = false, writable = true }) {
|
|
723
|
+
if (process.platform !== 'darwin' || !fs.existsSync('/usr/bin/sandbox-exec')) {
|
|
724
|
+
throw new Error('review-only execution requires the macOS sandbox-exec isolation backend');
|
|
725
|
+
}
|
|
726
|
+
const home = String(process.env.HOME || '').trim();
|
|
727
|
+
const readSubpaths = [
|
|
728
|
+
'/System', '/usr', '/bin', '/sbin', '/Library', '/opt/homebrew', '/dev',
|
|
729
|
+
'/private/etc', '/private/var/db', '/private/var/run', '/private/var/select',
|
|
730
|
+
worktreePath, gitDir, quarantine, runtimeTmp,
|
|
731
|
+
];
|
|
732
|
+
const writeSubpaths = ['/dev', runtimeTmp];
|
|
733
|
+
if (writable) writeSubpaths.push(worktreePath, gitDir, quarantine);
|
|
734
|
+
const installByEngine = {
|
|
735
|
+
codex: ['.bun'],
|
|
736
|
+
claude: ['.local/bin', '.local/share/claude'],
|
|
737
|
+
cursor: ['.local/bin', '.local/share/cursor-agent'],
|
|
738
|
+
devin: ['.local/bin', '.local/share/devin'],
|
|
739
|
+
grok: ['.local/bin', '.grok/downloads'],
|
|
740
|
+
};
|
|
741
|
+
if (home && engine) {
|
|
742
|
+
for (const relative of installByEngine[engine] || []) readSubpaths.push(path.join(home, relative));
|
|
743
|
+
}
|
|
744
|
+
if (engine && RUNNER_PROFILE_DEFS[engine]) {
|
|
745
|
+
const engineBin = String(RUNNER_PROFILE_DEFS[engine].bin || '').trim();
|
|
746
|
+
const located = engineBin
|
|
747
|
+
? spawnSync('/bin/sh', ['-c', `command -v ${engineBin}`], { encoding: 'utf8' })
|
|
748
|
+
: null;
|
|
749
|
+
const executable = located && located.status === 0 ? String(located.stdout || '').trim() : '';
|
|
750
|
+
if (executable) {
|
|
751
|
+
readSubpaths.push(path.dirname(executable));
|
|
752
|
+
readSubpaths.push(path.dirname(canonicalPath(executable)));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const readLiterals = [];
|
|
756
|
+
const writeLiterals = [];
|
|
757
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
758
|
+
if (!/^ATRIS_(?:ENGINE|VALIDATOR|PUSH|BOUNDARY)_(?:COUNT|PROMPT|RESULT|URL|CONFIG|DUMP)$/.test(key)) continue;
|
|
759
|
+
if (!value || !path.isAbsolute(value)) continue;
|
|
760
|
+
readLiterals.push(value);
|
|
761
|
+
writeLiterals.push(value);
|
|
762
|
+
}
|
|
763
|
+
const ancestorLiterals = [...new Set([
|
|
764
|
+
...readSubpaths,
|
|
765
|
+
...readLiterals,
|
|
766
|
+
...writeLiterals,
|
|
767
|
+
].flatMap(seatbeltAncestors))];
|
|
768
|
+
const readRules = [
|
|
769
|
+
...ancestorLiterals.map((value) => seatbeltRule('literal', value)),
|
|
770
|
+
...[...new Set(readSubpaths)].map((value) => seatbeltRule('subpath', value)),
|
|
771
|
+
...[...new Set(readLiterals)].map((value) => seatbeltRule('literal', value)),
|
|
772
|
+
];
|
|
773
|
+
const writeRules = [
|
|
774
|
+
...[...new Set(writeSubpaths)].map((value) => seatbeltRule('subpath', value)),
|
|
775
|
+
...[...new Set(writeLiterals)].map((value) => seatbeltRule('literal', value)),
|
|
776
|
+
];
|
|
777
|
+
return [
|
|
778
|
+
'(version 1)',
|
|
779
|
+
'(deny default)',
|
|
780
|
+
'(import "system.sb")',
|
|
781
|
+
'(import "com.apple.corefoundation.sb")',
|
|
782
|
+
'(allow process*)',
|
|
783
|
+
'(allow signal (target same-sandbox))',
|
|
784
|
+
...(network ? ['(allow network*)'] : []),
|
|
785
|
+
'(allow sysctl-read)',
|
|
786
|
+
'(allow mach-lookup)',
|
|
787
|
+
'(allow ipc-posix*)',
|
|
788
|
+
`(allow file-read* ${readRules.join(' ')})`,
|
|
789
|
+
`(allow file-write* ${writeRules.join(' ')})`,
|
|
790
|
+
].join('\n');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function prepareEphemeralEngineHome(runtimeTmp, engine) {
|
|
794
|
+
const sourceHome = String(process.env.HOME || '').trim();
|
|
795
|
+
const runtimeHome = path.join(runtimeTmp, `home-${engine || 'verifier'}`);
|
|
796
|
+
fs.mkdirSync(runtimeHome, { recursive: true, mode: 0o700 });
|
|
797
|
+
const copy = (sourceRelative, targetRelative, pickKeys = null) => {
|
|
798
|
+
if (!sourceHome) return;
|
|
799
|
+
const source = path.join(sourceHome, sourceRelative);
|
|
800
|
+
if (!fs.existsSync(source) || !fs.lstatSync(source).isFile()) return;
|
|
801
|
+
const target = path.join(runtimeHome, targetRelative);
|
|
802
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
803
|
+
if (pickKeys) {
|
|
804
|
+
let parsed;
|
|
805
|
+
try { parsed = JSON.parse(fs.readFileSync(source, 'utf8')); } catch { return; }
|
|
806
|
+
const selected = {};
|
|
807
|
+
for (const key of pickKeys) {
|
|
808
|
+
if (Object.prototype.hasOwnProperty.call(parsed, key)) selected[key] = parsed[key];
|
|
809
|
+
}
|
|
810
|
+
fs.writeFileSync(target, `${JSON.stringify(selected, null, 2)}\n`, { mode: 0o600 });
|
|
811
|
+
} else {
|
|
812
|
+
fs.copyFileSync(source, target);
|
|
813
|
+
fs.chmodSync(target, 0o600);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
if (engine === 'codex') copy('.codex/auth.json', '.codex/auth.json');
|
|
817
|
+
if (engine === 'claude') {
|
|
818
|
+
copy('.claude.json', '.claude.json', [
|
|
819
|
+
'oauthAccount', 'userID', 'anonymousId', 'machineID', 'hasCompletedOnboarding',
|
|
820
|
+
'installMethod', 'claudeMaxTier', 'hasAvailableMaxSubscription', 'hasAvailableSubscription',
|
|
821
|
+
]);
|
|
822
|
+
}
|
|
823
|
+
if (engine === 'cursor') {
|
|
824
|
+
copy('.cursor/cli-config.json', '.cursor/cli-config.json', ['authInfo', 'version', 'privacyCache', 'network']);
|
|
825
|
+
}
|
|
826
|
+
if (engine === 'devin') copy('.config/devin/config.json', '.config/devin/config.json');
|
|
827
|
+
if (engine === 'grok') {
|
|
828
|
+
copy('.grok/auth.json', '.grok/auth.json');
|
|
829
|
+
copy('.grok/config.toml', '.grok/config.toml');
|
|
830
|
+
}
|
|
831
|
+
return runtimeHome;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
|
|
835
|
+
const selectedEngine = String(options.engine || '');
|
|
836
|
+
const selectedSecret = (engines, name) => engines.includes(selectedEngine) ? String(process.env[name] || '') : '';
|
|
837
|
+
const runtimeTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-one-lap-runtime-'));
|
|
838
|
+
const guardDir = path.join(runtimeTmp, 'bin');
|
|
839
|
+
fs.mkdirSync(guardDir, { recursive: true });
|
|
840
|
+
const locate = (name) => {
|
|
841
|
+
const result = spawnSync('sh', ['-c', `command -v ${name}`], { encoding: 'utf8' });
|
|
842
|
+
return result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
843
|
+
};
|
|
844
|
+
const gitBin = locate('git');
|
|
845
|
+
if (!gitBin) throw new Error('review-only dispatch requires git');
|
|
846
|
+
fs.writeFileSync(path.join(guardDir, 'git'), [
|
|
847
|
+
'#!/bin/sh',
|
|
848
|
+
'for arg in "$@"; do',
|
|
849
|
+
' case "$arg" in',
|
|
850
|
+
' push|send-pack) echo "one lap blocked git $arg" >&2; exit 2 ;;',
|
|
851
|
+
' esac',
|
|
852
|
+
'done',
|
|
853
|
+
`exec ${shellSingleQuote(gitBin)} "$@"`,
|
|
854
|
+
'',
|
|
855
|
+
].join('\n'), { mode: 0o755 });
|
|
856
|
+
|
|
857
|
+
const npmBin = locate('npm');
|
|
858
|
+
if (npmBin) {
|
|
859
|
+
fs.writeFileSync(path.join(guardDir, 'npm'), [
|
|
860
|
+
'#!/bin/sh',
|
|
861
|
+
'case "${1:-}" in',
|
|
862
|
+
' publish|install|i|add|login|logout|owner|token|deprecate|unpublish|dist-tag|access|team|org) echo "one lap blocked npm $1" >&2; exit 2 ;;',
|
|
863
|
+
'esac',
|
|
864
|
+
`exec ${shellSingleQuote(npmBin)} "$@"`,
|
|
865
|
+
'',
|
|
866
|
+
].join('\n'), { mode: 0o755 });
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
for (const command of ['gh', 'curl', 'wget', 'ssh', 'scp', 'rsync', 'fly', 'flyctl', 'vercel', 'render', 'kubectl', 'aws', 'gcloud', 'az', 'terraform', 'stripe']) {
|
|
870
|
+
fs.writeFileSync(path.join(guardDir, command), `#!/bin/sh\necho "one lap blocked ${command}" >&2\nexit 2\n`, { mode: 0o755 });
|
|
871
|
+
}
|
|
872
|
+
const commonResult = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: worktreePath, encoding: 'utf8' });
|
|
873
|
+
const remoteResult = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: worktreePath, encoding: 'utf8' });
|
|
874
|
+
if (commonResult.status !== 0 || remoteResult.status !== 0) {
|
|
875
|
+
throw new Error('review-only execution could not resolve its sealed Git boundary');
|
|
876
|
+
}
|
|
877
|
+
const gitDir = canonicalPath(path.resolve(worktreePath, String(commonResult.stdout || '').trim()));
|
|
878
|
+
const quarantine = canonicalPath(path.resolve(worktreePath, String(remoteResult.stdout || '').trim()));
|
|
879
|
+
const runtimeHome = prepareEphemeralEngineHome(runtimeTmp, selectedEngine);
|
|
880
|
+
const controlTmp = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-one-lap-control-'));
|
|
881
|
+
const lifecycle = sandboxLifecycleWrapper(runtimeTmp, controlTmp);
|
|
882
|
+
let sandboxProfile;
|
|
883
|
+
try {
|
|
884
|
+
sandboxProfile = reviewOnlySandboxProfile({
|
|
885
|
+
worktreePath,
|
|
886
|
+
gitDir,
|
|
887
|
+
quarantine,
|
|
888
|
+
runtimeTmp,
|
|
889
|
+
engine: String(options.engine || ''),
|
|
890
|
+
network: options.network === true,
|
|
891
|
+
writable: options.writable !== false,
|
|
892
|
+
});
|
|
893
|
+
} catch (error) {
|
|
894
|
+
fs.rmSync(runtimeTmp, { recursive: true, force: true });
|
|
895
|
+
fs.rmSync(controlTmp, { recursive: true, force: true });
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
const environment = {
|
|
899
|
+
PATH: `${guardDir}${path.delimiter}${process.env.PATH || ''}`,
|
|
900
|
+
GIT_CONFIG_COUNT: '2',
|
|
901
|
+
GIT_CONFIG_KEY_0: 'remote.origin.pushurl',
|
|
902
|
+
GIT_CONFIG_VALUE_0: path.join(guardDir, 'blocked-push'),
|
|
903
|
+
GIT_CONFIG_KEY_1: 'remote.origin.receivepack',
|
|
904
|
+
GIT_CONFIG_VALUE_1: 'false',
|
|
905
|
+
GH_TOKEN: '',
|
|
906
|
+
GITHUB_TOKEN: '',
|
|
907
|
+
NPM_TOKEN: '',
|
|
908
|
+
NODE_AUTH_TOKEN: '',
|
|
909
|
+
AWS_ACCESS_KEY_ID: '',
|
|
910
|
+
AWS_SECRET_ACCESS_KEY: '',
|
|
911
|
+
GOOGLE_APPLICATION_CREDENTIALS: '',
|
|
912
|
+
AZURE_CLIENT_SECRET: '',
|
|
913
|
+
STRIPE_SECRET_KEY: '',
|
|
914
|
+
OPENAI_API_KEY: selectedSecret(['codex'], 'OPENAI_API_KEY'),
|
|
915
|
+
CODEX_API_KEY: selectedSecret(['codex'], 'CODEX_API_KEY'),
|
|
916
|
+
ANTHROPIC_API_KEY: selectedSecret(['claude'], 'ANTHROPIC_API_KEY'),
|
|
917
|
+
CLAUDE_CODE_OAUTH_TOKEN: selectedSecret(['claude'], 'CLAUDE_CODE_OAUTH_TOKEN'),
|
|
918
|
+
CURSOR_API_KEY: selectedSecret(['cursor'], 'CURSOR_API_KEY'),
|
|
919
|
+
DEVIN_API_KEY: selectedSecret(['devin'], 'DEVIN_API_KEY'),
|
|
920
|
+
XAI_API_KEY: selectedSecret(['grok'], 'XAI_API_KEY'),
|
|
921
|
+
GROK_API_KEY: selectedSecret(['grok'], 'GROK_API_KEY'),
|
|
922
|
+
SSH_AUTH_SOCK: '',
|
|
923
|
+
TMPDIR: runtimeTmp,
|
|
924
|
+
HOME: runtimeHome,
|
|
925
|
+
XDG_CONFIG_HOME: path.join(runtimeHome, '.config'),
|
|
926
|
+
XDG_CACHE_HOME: path.join(runtimeTmp, 'cache'),
|
|
927
|
+
npm_config_cache: path.join(runtimeTmp, 'npm-cache'),
|
|
928
|
+
GIT_CONFIG_GLOBAL: path.join(runtimeTmp, 'gitconfig'),
|
|
929
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
930
|
+
CODEX_HOME: path.join(runtimeHome, '.codex'),
|
|
931
|
+
CLAUDE_CONFIG_DIR: path.join(runtimeHome, '.claude'),
|
|
932
|
+
ATRIS_ONE_LAP_SANDBOX_PROFILE: sandboxProfile,
|
|
933
|
+
ATRIS_ONE_LAP_RUNTIME_DIR: runtimeTmp,
|
|
934
|
+
ATRIS_ONE_LAP_CONTROL_DIR: controlTmp,
|
|
935
|
+
ATRIS_ONE_LAP_LIFECYCLE_WRAPPER: lifecycle.wrapper,
|
|
936
|
+
ATRIS_ONE_LAP_STATUS_FILE: lifecycle.statusFile,
|
|
937
|
+
};
|
|
938
|
+
for (const key of [
|
|
939
|
+
'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'NO_COLOR', 'USER', 'LOGNAME', 'SHELL',
|
|
940
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
|
|
941
|
+
'NODE_EXTRA_CA_CERTS', 'NODE_NO_WARNINGS', 'DISABLE_AUTOUPDATER',
|
|
942
|
+
'ATRIS_ENGINE_MODE', 'ATRIS_ENGINE_DELAY', 'ATRIS_ENGINE_COUNT', 'ATRIS_ENGINE_PROMPT',
|
|
943
|
+
'ATRIS_VALIDATOR_MODE', 'ATRIS_VALIDATOR_COUNT', 'ATRIS_VALIDATOR_PROMPT',
|
|
944
|
+
'ATRIS_PUSH_RESULT', 'ATRIS_PUSH_URL', 'ATRIS_PUSH_CONFIG', 'ATRIS_BOUNDARY_DUMP',
|
|
945
|
+
'ATRIS_REAL_GIT', 'ATRIS_TASKS_DB',
|
|
946
|
+
]) {
|
|
947
|
+
if (process.env[key] !== undefined) environment[key] = String(process.env[key]);
|
|
948
|
+
}
|
|
949
|
+
return environment;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function conductorGitEnvironment() {
|
|
953
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
954
|
+
for (const key of Object.keys(env)) {
|
|
955
|
+
if (/^GIT_CONFIG_(?:COUNT|KEY_\d+|VALUE_\d+)$/.test(key)) delete env[key];
|
|
956
|
+
}
|
|
957
|
+
for (const key of [
|
|
958
|
+
'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY',
|
|
959
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_REPLACE_REF_BASE', 'GIT_GRAFT_FILE', 'GIT_CONFIG_PARAMETERS',
|
|
960
|
+
]) delete env[key];
|
|
961
|
+
return env;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function remoteMasterOid(worktreePath, remote = 'origin') {
|
|
965
|
+
const result = spawnSync('git', ['ls-remote', '--exit-code', remote, 'refs/heads/master'], {
|
|
966
|
+
cwd: worktreePath,
|
|
967
|
+
encoding: 'utf8',
|
|
968
|
+
timeout: 15000,
|
|
969
|
+
env: conductorGitEnvironment(),
|
|
970
|
+
});
|
|
971
|
+
if (result.status !== 0) return '';
|
|
972
|
+
return String(result.stdout || '').trim().split(/\s+/)[0] || '';
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function remoteRefsDigest(worktreePath, remote = 'origin') {
|
|
976
|
+
const result = spawnSync('git', ['ls-remote', remote], {
|
|
977
|
+
cwd: worktreePath,
|
|
978
|
+
encoding: 'utf8',
|
|
979
|
+
timeout: 30000,
|
|
980
|
+
env: conductorGitEnvironment(),
|
|
981
|
+
});
|
|
982
|
+
if (result.status !== 0) return '';
|
|
983
|
+
return crypto.createHash('sha256').update(String(result.stdout || '')).digest('hex');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function digestFiles(paths) {
|
|
987
|
+
const hash = crypto.createHash('sha256');
|
|
988
|
+
const visit = (target) => {
|
|
989
|
+
hash.update(`\0${target}\0`);
|
|
990
|
+
if (!fs.existsSync(target)) {
|
|
991
|
+
hash.update('missing');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const stat = fs.lstatSync(target);
|
|
995
|
+
if (stat.isSymbolicLink()) {
|
|
996
|
+
hash.update(`link:${fs.readlinkSync(target)}`);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (stat.isDirectory()) {
|
|
1000
|
+
hash.update('directory');
|
|
1001
|
+
for (const name of fs.readdirSync(target).sort()) visit(path.join(target, name));
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
hash.update(`file:${stat.mode}:`);
|
|
1005
|
+
hash.update(fs.readFileSync(target));
|
|
1006
|
+
};
|
|
1007
|
+
for (const target of paths) visit(target);
|
|
1008
|
+
return hash.digest('hex');
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function reviewSandboxMetadataDigest(boundary) {
|
|
1012
|
+
return digestFiles([
|
|
1013
|
+
path.join(boundary.worktreePath, '.git'),
|
|
1014
|
+
path.join(boundary.gitDir, 'config'),
|
|
1015
|
+
path.join(boundary.gitDir, 'hooks'),
|
|
1016
|
+
path.join(boundary.gitDir, 'info'),
|
|
1017
|
+
path.join(boundary.gitDir, 'objects', 'info'),
|
|
1018
|
+
path.join(boundary.gitDir, 'shallow'),
|
|
1019
|
+
path.join(boundary.worktreeGitDir, 'config.worktree'),
|
|
1020
|
+
path.join(boundary.worktreeGitDir, 'commondir'),
|
|
1021
|
+
path.join(boundary.worktreeGitDir, 'gitdir'),
|
|
1022
|
+
path.join(boundary.worktreeGitDir, 'HEAD'),
|
|
1023
|
+
]);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function bareRefsDigest(gitDir) {
|
|
1027
|
+
const result = spawnSync('git', ['--git-dir', gitDir, 'show-ref', '--head'], { encoding: 'utf8', env: trustedGitEnvironment() });
|
|
1028
|
+
if (result.status !== 0) return '';
|
|
1029
|
+
return crypto.createHash('sha256').update(String(result.stdout || '')).digest('hex');
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function prepareReviewSandbox({ root, taskId, engine }) {
|
|
1033
|
+
const run = (args, options = {}) => spawnSync('git', args, {
|
|
1034
|
+
cwd: options.cwd || root,
|
|
1035
|
+
encoding: 'utf8',
|
|
1036
|
+
env: trustedGitEnvironment(),
|
|
1037
|
+
timeout: options.timeout || 30000,
|
|
1038
|
+
});
|
|
1039
|
+
const runConductorGit = (args) => spawnSync('git', args, {
|
|
1040
|
+
cwd: root,
|
|
1041
|
+
encoding: 'utf8',
|
|
1042
|
+
env: conductorGitEnvironment(),
|
|
1043
|
+
timeout: 30000,
|
|
1044
|
+
});
|
|
1045
|
+
const fetched = runConductorGit(['fetch', 'origin', 'master']);
|
|
1046
|
+
const original = runConductorGit(['remote', 'get-url', 'origin']);
|
|
1047
|
+
const base = run(['rev-parse', '--verify', 'origin/master^{commit}']);
|
|
1048
|
+
if (fetched.status !== 0 || original.status !== 0 || base.status !== 0) {
|
|
1049
|
+
return { ok: false, detail: 'review-only dispatch requires a current protected origin/master' };
|
|
1050
|
+
}
|
|
1051
|
+
const originalUrl = String(original.stdout || '').trim();
|
|
1052
|
+
const protectedMaster = remoteMasterOid(root, originalUrl);
|
|
1053
|
+
const protectedRefs = remoteRefsDigest(root, originalUrl);
|
|
1054
|
+
const baseOid = String(base.stdout || '').trim();
|
|
1055
|
+
if (!protectedMaster || !protectedRefs || baseOid !== protectedMaster) {
|
|
1056
|
+
return { ok: false, detail: 'review-only origin/master is not at the protected remote snapshot' };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const token = `${String(taskId || 'task').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'task'}-${process.pid}-${crypto.randomBytes(5).toString('hex')}`;
|
|
1060
|
+
const sandboxRoot = fs.mkdtempSync(path.join(os.tmpdir(), `atris-one-lap-${token}-`));
|
|
1061
|
+
const worktreePath = path.join(sandboxRoot, 'worktree');
|
|
1062
|
+
const gitDir = path.join(sandboxRoot, 'objects.git');
|
|
1063
|
+
const quarantine = path.join(sandboxRoot, 'quarantine.git');
|
|
1064
|
+
const seedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-one-lap-seed-'));
|
|
1065
|
+
const seedBundle = path.join(seedDir, 'seed.bundle');
|
|
1066
|
+
const branch = `one-lap-${token}`;
|
|
1067
|
+
const cleanup = () => {
|
|
1068
|
+
fs.rmSync(seedDir, { recursive: true, force: true });
|
|
1069
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
1070
|
+
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
1071
|
+
fs.rmSync(quarantine, { recursive: true, force: true });
|
|
1072
|
+
fs.rmSync(sandboxRoot, { recursive: true, force: true });
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
try {
|
|
1076
|
+
fs.mkdirSync(sandboxRoot, { recursive: true });
|
|
1077
|
+
let result = run(['bundle', 'create', seedBundle, 'origin/master']);
|
|
1078
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not create seed bundle').trim());
|
|
1079
|
+
result = run(['init', '--bare', '--initial-branch=master', gitDir], { cwd: sandboxRoot });
|
|
1080
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not initialize sandbox object store').trim());
|
|
1081
|
+
result = spawnSync('git', ['--git-dir', gitDir, 'bundle', 'unbundle', seedBundle], { encoding: 'utf8', timeout: 30000 });
|
|
1082
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not seed sandbox object store').trim());
|
|
1083
|
+
result = run(['init', '--bare', '--initial-branch=master', quarantine], { cwd: sandboxRoot });
|
|
1084
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not initialize quarantine remote').trim());
|
|
1085
|
+
result = spawnSync('git', ['--git-dir', quarantine, 'bundle', 'unbundle', seedBundle], { encoding: 'utf8', timeout: 30000 });
|
|
1086
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not seed quarantine remote').trim());
|
|
1087
|
+
for (const [repo, ref, oid] of [
|
|
1088
|
+
[gitDir, `refs/heads/${branch}`, baseOid],
|
|
1089
|
+
[gitDir, 'refs/remotes/origin/master', baseOid],
|
|
1090
|
+
[quarantine, 'refs/heads/master', baseOid],
|
|
1091
|
+
]) {
|
|
1092
|
+
result = spawnSync('git', ['--git-dir', repo, 'update-ref', ref, oid], { encoding: 'utf8' });
|
|
1093
|
+
if (result.status !== 0) throw new Error(String(result.stderr || `could not seed ${ref}`).trim());
|
|
1094
|
+
}
|
|
1095
|
+
const configure = (args) => spawnSync('git', ['--git-dir', gitDir, 'config', ...args], { encoding: 'utf8' });
|
|
1096
|
+
for (const args of [
|
|
1097
|
+
['extensions.worktreeConfig', 'true'],
|
|
1098
|
+
['remote.origin.url', quarantine],
|
|
1099
|
+
['remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*'],
|
|
1100
|
+
['user.name', String(runConductorGit(['config', '--get', 'user.name']).stdout || 'Atris One Lap').trim() || 'Atris One Lap'],
|
|
1101
|
+
['user.email', String(runConductorGit(['config', '--get', 'user.email']).stdout || 'one-lap@localhost').trim() || 'one-lap@localhost'],
|
|
1102
|
+
]) {
|
|
1103
|
+
result = configure(args);
|
|
1104
|
+
if (result.status !== 0) throw new Error(String(result.stderr || 'could not configure sandbox').trim());
|
|
1105
|
+
}
|
|
1106
|
+
result = spawnSync('git', ['--git-dir', gitDir, 'worktree', 'add', '--no-checkout', worktreePath, branch], {
|
|
1107
|
+
encoding: 'utf8',
|
|
1108
|
+
env: trustedGitEnvironment(),
|
|
1109
|
+
timeout: 30000,
|
|
1110
|
+
});
|
|
1111
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not create sandbox worktree').trim());
|
|
1112
|
+
result = spawnSync('git', ['-C', worktreePath, 'config', '--worktree', 'core.bare', 'false'], { encoding: 'utf8' });
|
|
1113
|
+
if (result.status !== 0) throw new Error(String(result.stderr || 'could not activate sandbox worktree').trim());
|
|
1114
|
+
const worktreeGitDirResult = spawnSync('git', ['-C', worktreePath, 'rev-parse', '--absolute-git-dir'], { encoding: 'utf8' });
|
|
1115
|
+
if (worktreeGitDirResult.status !== 0) throw new Error(String(worktreeGitDirResult.stderr || 'could not locate sandbox worktree metadata').trim());
|
|
1116
|
+
const worktreeGitDir = String(worktreeGitDirResult.stdout || '').trim();
|
|
1117
|
+
result = spawnSync('git', ['-C', worktreePath, 'read-tree', 'HEAD'], { encoding: 'utf8', env: trustedGitEnvironment() });
|
|
1118
|
+
if (result.status !== 0) throw new Error(String(result.stderr || 'could not seed sandbox index').trim());
|
|
1119
|
+
materializeVerifiedTree(worktreePath, [], readGitTree(gitDir, baseOid));
|
|
1120
|
+
fs.mkdirSync(path.join(worktreePath, '.atris'), { recursive: true });
|
|
1121
|
+
fs.writeFileSync(path.join(worktreePath, '.atris', 'agent-worktree.json'), `${JSON.stringify({
|
|
1122
|
+
agent: engine || null,
|
|
1123
|
+
owner: engine || 'one-lap',
|
|
1124
|
+
task: String(taskId || ''),
|
|
1125
|
+
branch,
|
|
1126
|
+
base: 'origin/master',
|
|
1127
|
+
sealed_review_sandbox: true,
|
|
1128
|
+
created_at: new Date().toISOString(),
|
|
1129
|
+
}, null, 2)}\n`, 'utf8');
|
|
1130
|
+
fs.rmSync(seedDir, { recursive: true, force: true });
|
|
1131
|
+
|
|
1132
|
+
const boundary = {
|
|
1133
|
+
ok: true,
|
|
1134
|
+
trustedRoot: root,
|
|
1135
|
+
sandboxRoot,
|
|
1136
|
+
worktreePath,
|
|
1137
|
+
gitDir,
|
|
1138
|
+
worktreeGitDir,
|
|
1139
|
+
quarantine,
|
|
1140
|
+
originalUrl,
|
|
1141
|
+
protectedMaster,
|
|
1142
|
+
protectedRefs,
|
|
1143
|
+
quarantineRefs: bareRefsDigest(quarantine),
|
|
1144
|
+
branch,
|
|
1145
|
+
baseOid,
|
|
1146
|
+
};
|
|
1147
|
+
boundary.metadataDigest = reviewSandboxMetadataDigest(boundary);
|
|
1148
|
+
const configText = fs.readFileSync(path.join(gitDir, 'config'), 'utf8');
|
|
1149
|
+
const gitFile = fs.readFileSync(path.join(worktreePath, '.git'), 'utf8');
|
|
1150
|
+
if (!boundary.quarantineRefs || configText.includes(originalUrl) || configText.includes(root) || gitFile.includes(root)) {
|
|
1151
|
+
throw new Error('sealed sandbox leaked a protected repository locator');
|
|
1152
|
+
}
|
|
1153
|
+
return boundary;
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
cleanup();
|
|
1156
|
+
return { ok: false, detail: `review-only sandbox could not be prepared: ${error.message}` };
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function reviewRemoteBoundaryState(worktreePath, boundary) {
|
|
1161
|
+
if (!boundary || !boundary.ok) {
|
|
1162
|
+
return { ok: false, stage: 'remote_quarantine', detail: 'review-only sealed sandbox is not armed' };
|
|
1163
|
+
}
|
|
1164
|
+
let metadataDigest = '';
|
|
1165
|
+
try { metadataDigest = reviewSandboxMetadataDigest(boundary); } catch {}
|
|
1166
|
+
if (!metadataDigest || metadataDigest !== boundary.metadataDigest) {
|
|
1167
|
+
return {
|
|
1168
|
+
ok: false,
|
|
1169
|
+
stage: 'sandbox_metadata_changed',
|
|
1170
|
+
detail: 'the worker changed sealed Git metadata',
|
|
1171
|
+
protected_master: boundary.protectedMaster,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
const protectedMaster = remoteMasterOid(boundary.trustedRoot, boundary.originalUrl);
|
|
1175
|
+
const protectedRefs = remoteRefsDigest(boundary.trustedRoot, boundary.originalUrl);
|
|
1176
|
+
const quarantineMasterResult = spawnSync('git', ['--git-dir', boundary.quarantine, 'rev-parse', '--verify', 'refs/heads/master^{commit}'], { encoding: 'utf8' });
|
|
1177
|
+
const quarantineMaster = quarantineMasterResult.status === 0 ? String(quarantineMasterResult.stdout || '').trim() : '';
|
|
1178
|
+
const quarantineRefs = bareRefsDigest(boundary.quarantine);
|
|
1179
|
+
if (!protectedMaster || !protectedRefs || protectedMaster !== boundary.protectedMaster || protectedRefs !== boundary.protectedRefs) {
|
|
1180
|
+
return {
|
|
1181
|
+
ok: false,
|
|
1182
|
+
stage: 'master_changed',
|
|
1183
|
+
detail: 'the protected remote changed during isolated execution',
|
|
1184
|
+
protected_master: protectedMaster || null,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
if (!quarantineMaster || !quarantineRefs || quarantineMaster !== boundary.protectedMaster || quarantineRefs !== boundary.quarantineRefs) {
|
|
1188
|
+
return {
|
|
1189
|
+
ok: false,
|
|
1190
|
+
stage: 'outbound_attempt',
|
|
1191
|
+
detail: 'the worker attempted to change the quarantined remote',
|
|
1192
|
+
protected_master: protectedMaster,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
return { ok: true, protected_master: protectedMaster };
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function reviewCandidateSnapshot(boundary, expected = null) {
|
|
1199
|
+
const fail = (detail, extra = {}) => ({ ok: false, stage: 'candidate_changed', detail, ...extra });
|
|
1200
|
+
const run = (args) => spawnSync('git', args, {
|
|
1201
|
+
cwd: boundary.worktreePath,
|
|
1202
|
+
encoding: 'utf8',
|
|
1203
|
+
env: trustedGitEnvironment(),
|
|
1204
|
+
timeout: 30000,
|
|
1205
|
+
});
|
|
1206
|
+
const firstHead = run(['rev-parse', '--verify', 'HEAD^{commit}']);
|
|
1207
|
+
const tree = run(['rev-parse', '--verify', 'HEAD^{tree}']);
|
|
1208
|
+
const status = run(['status', '--porcelain=v1', '--untracked-files=all']);
|
|
1209
|
+
const secondHead = run(['rev-parse', '--verify', 'HEAD^{commit}']);
|
|
1210
|
+
const failed = [firstHead, tree, status, secondHead].find((result) => result.status !== 0);
|
|
1211
|
+
if (failed) return fail(String(failed.stderr || failed.stdout || 'could not freeze the executor candidate').trim());
|
|
1212
|
+
const commit = String(firstHead.stdout || '').trim();
|
|
1213
|
+
const repeatedCommit = String(secondHead.stdout || '').trim();
|
|
1214
|
+
const treeOid = String(tree.stdout || '').trim();
|
|
1215
|
+
if (commit !== repeatedCommit) return fail('the executor changed HEAD while its candidate was being frozen');
|
|
1216
|
+
const conductorEntry = /^\?\? \.atris\/(?:agent-worktree\.json|state\/briefs\.jsonl)$/;
|
|
1217
|
+
const dirty = String(status.stdout || '').trim().split(/\r?\n/).filter(Boolean)
|
|
1218
|
+
.filter((line) => !conductorEntry.test(line));
|
|
1219
|
+
if (dirty.length) return fail(`the executor left uncommitted changes: ${dirty.slice(0, 10).join(', ')}`);
|
|
1220
|
+
if (expected && (commit !== expected.commit || treeOid !== expected.tree)) {
|
|
1221
|
+
return fail('the sealed candidate changed after the executor exited', { commit, tree: treeOid });
|
|
1222
|
+
}
|
|
1223
|
+
return { ok: true, stage: 'candidate_frozen', commit, tree: treeOid };
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function trustedGitEnvironment() {
|
|
1227
|
+
const env = {
|
|
1228
|
+
...process.env,
|
|
1229
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
1230
|
+
GIT_CONFIG_SYSTEM: '/dev/null',
|
|
1231
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
1232
|
+
GIT_ATTR_NOSYSTEM: '1',
|
|
1233
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
1234
|
+
};
|
|
1235
|
+
for (const key of Object.keys(env)) {
|
|
1236
|
+
if (/^GIT_CONFIG_(?:COUNT|KEY_\d+|VALUE_\d+)$/.test(key)) delete env[key];
|
|
1237
|
+
}
|
|
1238
|
+
for (const key of [
|
|
1239
|
+
'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY',
|
|
1240
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_REPLACE_REF_BASE', 'GIT_GRAFT_FILE', 'GIT_CONFIG_PARAMETERS',
|
|
1241
|
+
]) delete env[key];
|
|
1242
|
+
return env;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function readGitTree(gitDir, ref) {
|
|
1246
|
+
const env = trustedGitEnvironment();
|
|
1247
|
+
const listed = spawnSync('git', ['--git-dir', gitDir, 'ls-tree', '-rz', '--full-tree', ref], {
|
|
1248
|
+
encoding: 'utf8',
|
|
1249
|
+
env,
|
|
1250
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1251
|
+
});
|
|
1252
|
+
if (listed.status !== 0) throw new Error(String(listed.stderr || 'could not read verified tree').trim());
|
|
1253
|
+
const entries = String(listed.stdout || '').split('\0').filter(Boolean).map((line) => {
|
|
1254
|
+
const match = line.match(/^([0-7]{6}) (blob|commit) ([0-9a-f]{40,64})\t([^]*)$/);
|
|
1255
|
+
if (!match) throw new Error('verified tree contains an unreadable entry');
|
|
1256
|
+
const [, mode, type, oid, relative] = match;
|
|
1257
|
+
if (type !== 'blob' || !['100644', '100755', '120000'].includes(mode)) {
|
|
1258
|
+
throw new Error(`verified tree entry is not a regular file or symlink: ${relative}`);
|
|
1259
|
+
}
|
|
1260
|
+
if (path.isAbsolute(relative) || relative.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
1261
|
+
throw new Error(`verified tree contains an unsafe path: ${relative}`);
|
|
1262
|
+
}
|
|
1263
|
+
if (!/^[\x20-\x7e]+$/.test(relative)) {
|
|
1264
|
+
throw new Error(`verified tree contains a non-ASCII path that cannot be imported safely: ${relative}`);
|
|
1265
|
+
}
|
|
1266
|
+
const folded = relative.toLowerCase();
|
|
1267
|
+
if (folded === '.atris' || folded === '.atris/agent-worktree.json' || folded === '.git' || folded.startsWith('.git/')) {
|
|
1268
|
+
throw new Error('verified tree conflicts with Atris worktree metadata');
|
|
1269
|
+
}
|
|
1270
|
+
return { mode, oid, relative };
|
|
1271
|
+
});
|
|
1272
|
+
const uniqueOids = [...new Set(entries.map((entry) => entry.oid))];
|
|
1273
|
+
const batch = spawnSync('git', ['--git-dir', gitDir, 'cat-file', '--batch'], {
|
|
1274
|
+
encoding: null,
|
|
1275
|
+
env,
|
|
1276
|
+
input: `${uniqueOids.join('\n')}\n`,
|
|
1277
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
1278
|
+
});
|
|
1279
|
+
if (batch.status !== 0) throw new Error(String(batch.stderr || 'could not read verified blobs').trim());
|
|
1280
|
+
const blobs = new Map();
|
|
1281
|
+
let offset = 0;
|
|
1282
|
+
for (const requestedOid of uniqueOids) {
|
|
1283
|
+
const newline = batch.stdout.indexOf(0x0a, offset);
|
|
1284
|
+
if (newline === -1) throw new Error('verified blob batch ended before its header');
|
|
1285
|
+
const header = batch.stdout.subarray(offset, newline).toString('utf8').split(/\s+/);
|
|
1286
|
+
const size = Number(header[2]);
|
|
1287
|
+
if (header[0] !== requestedOid || header[1] !== 'blob' || !Number.isSafeInteger(size) || size < 0) {
|
|
1288
|
+
throw new Error('verified blob batch returned an invalid object');
|
|
1289
|
+
}
|
|
1290
|
+
const start = newline + 1;
|
|
1291
|
+
const end = start + size;
|
|
1292
|
+
if (end >= batch.stdout.length || batch.stdout[end] !== 0x0a) throw new Error('verified blob batch returned a truncated object');
|
|
1293
|
+
blobs.set(requestedOid, Buffer.from(batch.stdout.subarray(start, end)));
|
|
1294
|
+
offset = end + 1;
|
|
1295
|
+
}
|
|
1296
|
+
return entries.map((entry) => ({ ...entry, data: blobs.get(entry.oid) }));
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function materializeVerifiedTree(landingPath, baseEntries, sourceEntries) {
|
|
1300
|
+
const within = (relative) => {
|
|
1301
|
+
const absolute = path.resolve(landingPath, relative);
|
|
1302
|
+
if (absolute !== landingPath && !absolute.startsWith(`${path.resolve(landingPath)}${path.sep}`)) {
|
|
1303
|
+
throw new Error(`verified tree path escapes the landing worktree: ${relative}`);
|
|
1304
|
+
}
|
|
1305
|
+
return absolute;
|
|
1306
|
+
};
|
|
1307
|
+
for (const entry of [...baseEntries].sort((a, b) => b.relative.length - a.relative.length)) {
|
|
1308
|
+
fs.rmSync(within(entry.relative), { recursive: true, force: true });
|
|
1309
|
+
}
|
|
1310
|
+
for (const entry of sourceEntries) {
|
|
1311
|
+
const target = within(entry.relative);
|
|
1312
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1313
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
1314
|
+
if (entry.mode === '120000') {
|
|
1315
|
+
fs.symlinkSync(entry.data.toString('utf8'), target);
|
|
1316
|
+
} else {
|
|
1317
|
+
fs.writeFileSync(target, entry.data, { mode: entry.mode === '100755' ? 0o755 : 0o644 });
|
|
1318
|
+
fs.chmodSync(target, entry.mode === '100755' ? 0o755 : 0o644);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const sourceByPath = new Map(sourceEntries.map((entry) => [entry.relative, entry]));
|
|
1322
|
+
for (const entry of sourceEntries) {
|
|
1323
|
+
const target = within(entry.relative);
|
|
1324
|
+
const stat = fs.lstatSync(target);
|
|
1325
|
+
if (entry.mode === '120000') {
|
|
1326
|
+
if (!stat.isSymbolicLink() || fs.readlinkSync(target) !== entry.data.toString('utf8')) {
|
|
1327
|
+
throw new Error(`materialized symlink does not match verified tree: ${entry.relative}`);
|
|
1328
|
+
}
|
|
1329
|
+
} else if (!stat.isFile() || !fs.readFileSync(target).equals(entry.data) || Boolean(stat.mode & 0o111) !== (entry.mode === '100755')) {
|
|
1330
|
+
throw new Error(`materialized file does not match verified tree: ${entry.relative}`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
for (const entry of baseEntries) {
|
|
1334
|
+
if (!sourceByPath.has(entry.relative) && fs.existsSync(within(entry.relative))) {
|
|
1335
|
+
throw new Error(`deleted verified path remains in landing worktree: ${entry.relative}`);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function persistVerifiedCommit(boundary, landingPath, headOid) {
|
|
1341
|
+
const transferDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-one-lap-proof-'));
|
|
1342
|
+
const bundle = path.join(transferDir, 'verified.bundle');
|
|
1343
|
+
const proofRef = `refs/atris/one-lap/${headOid}`;
|
|
1344
|
+
const env = trustedGitEnvironment();
|
|
1345
|
+
try {
|
|
1346
|
+
let result = spawnSync('git', ['bundle', 'create', bundle, 'HEAD', `^${boundary.baseOid}`], {
|
|
1347
|
+
cwd: boundary.worktreePath,
|
|
1348
|
+
encoding: 'utf8',
|
|
1349
|
+
env,
|
|
1350
|
+
timeout: 60000,
|
|
1351
|
+
});
|
|
1352
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not export proof objects').trim());
|
|
1353
|
+
result = spawnSync('git', ['bundle', 'unbundle', bundle], {
|
|
1354
|
+
cwd: landingPath,
|
|
1355
|
+
encoding: 'utf8',
|
|
1356
|
+
env,
|
|
1357
|
+
timeout: 60000,
|
|
1358
|
+
});
|
|
1359
|
+
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || 'could not import proof objects').trim());
|
|
1360
|
+
result = spawnSync('git', ['update-ref', proofRef, headOid], { cwd: landingPath, encoding: 'utf8', env });
|
|
1361
|
+
if (result.status !== 0) throw new Error(String(result.stderr || 'could not retain proof ref').trim());
|
|
1362
|
+
result = spawnSync('git', ['cat-file', '-e', `${headOid}^{commit}`], { cwd: landingPath, encoding: 'utf8', env });
|
|
1363
|
+
if (result.status !== 0) throw new Error('the retained proof commit is not readable from the review worktree');
|
|
1364
|
+
return proofRef;
|
|
1365
|
+
} finally {
|
|
1366
|
+
fs.rmSync(transferDir, { recursive: true, force: true });
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function bindOneLapProof(worktreePath, proofRef, sourceCommit) {
|
|
1371
|
+
const env = trustedGitEnvironment();
|
|
1372
|
+
const branchResult = spawnSync('git', ['branch', '--show-current'], { cwd: worktreePath, encoding: 'utf8', env });
|
|
1373
|
+
const treeResult = spawnSync('git', ['rev-parse', '--verify', `${proofRef}^{tree}`], { cwd: worktreePath, encoding: 'utf8', env });
|
|
1374
|
+
const branch = String(branchResult.stdout || '').trim();
|
|
1375
|
+
const sourceTree = String(treeResult.stdout || '').trim();
|
|
1376
|
+
if (branchResult.status !== 0 || !branch || treeResult.status !== 0 || !/^[0-9a-f]{40,64}$/.test(sourceTree)) {
|
|
1377
|
+
throw new Error('could not bind the review worktree to its verified proof tree');
|
|
1378
|
+
}
|
|
1379
|
+
const sidecarPath = path.join(worktreePath, '.atris', 'agent-worktree.json');
|
|
1380
|
+
let sidecar;
|
|
1381
|
+
try {
|
|
1382
|
+
sidecar = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
1383
|
+
} catch {
|
|
1384
|
+
throw new Error('could not read review worktree metadata for proof binding');
|
|
1385
|
+
}
|
|
1386
|
+
sidecar.one_lap_proof = {
|
|
1387
|
+
schema: 'atris.one_lap_proof.v1',
|
|
1388
|
+
ref: proofRef,
|
|
1389
|
+
commit: sourceCommit,
|
|
1390
|
+
tree: sourceTree,
|
|
1391
|
+
};
|
|
1392
|
+
fs.writeFileSync(sidecarPath, `${JSON.stringify(sidecar, null, 2)}\n`, 'utf8');
|
|
1393
|
+
const configured = spawnSync('git', ['config', `branch.${branch}.atris-proof-ref`, proofRef], {
|
|
1394
|
+
cwd: worktreePath,
|
|
1395
|
+
encoding: 'utf8',
|
|
1396
|
+
env,
|
|
1397
|
+
});
|
|
1398
|
+
if (configured.status !== 0) throw new Error(String(configured.stderr || 'could not persist one-lap proof binding').trim());
|
|
1399
|
+
return sourceTree;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function importReviewCommit(boundary, { cli, taskId, engine, expectedCommit = '', expectedTree = '', startBaseArgs = [] } = {}) {
|
|
1403
|
+
const fail = (stage, detail, extra = {}) => ({ ok: false, stage, detail: String(detail || '').trim().slice(-500), ...extra });
|
|
1404
|
+
const runSandbox = (args) => spawnSync('git', args, {
|
|
1405
|
+
cwd: boundary.worktreePath,
|
|
1406
|
+
encoding: 'utf8',
|
|
1407
|
+
env: trustedGitEnvironment(),
|
|
1408
|
+
timeout: 30000,
|
|
1409
|
+
});
|
|
1410
|
+
const boundaryState = reviewRemoteBoundaryState(boundary.worktreePath, boundary);
|
|
1411
|
+
if (!boundaryState.ok) return boundaryState;
|
|
1412
|
+
if (!expectedCommit || !expectedTree) return fail('candidate_changed', 'the verified candidate identity was not frozen before import');
|
|
1413
|
+
const candidate = reviewCandidateSnapshot(boundary, { commit: expectedCommit, tree: expectedTree });
|
|
1414
|
+
if (!candidate.ok) return candidate;
|
|
1415
|
+
const status = runSandbox(['status', '--porcelain=v1', '--untracked-files=all']);
|
|
1416
|
+
if (status.status !== 0) return fail('review_import', status.stderr || 'could not inspect the sealed sandbox');
|
|
1417
|
+
const dirtyEntries = String(status.stdout || '').trim().split(/\r?\n/).filter(Boolean);
|
|
1418
|
+
const conductorEntries = /^\?\? \.atris\/(?:agent-worktree\.json|fleet-prompt-[^/]+\.md|runtime-tmp(?:\/.*)?|state\/briefs\.jsonl)$/;
|
|
1419
|
+
const uncommitted = dirtyEntries.filter((line) => !conductorEntries.test(line));
|
|
1420
|
+
if (uncommitted.length) return fail('uncommitted_change', `the sealed executor must commit every requested change before Review: ${uncommitted.join(', ')}`);
|
|
1421
|
+
const headOid = expectedCommit;
|
|
1422
|
+
if (headOid === boundary.baseOid) return fail('no_change', 'the executor produced no committed change');
|
|
1423
|
+
if (runSandbox(['merge-base', '--is-ancestor', boundary.baseOid, headOid]).status !== 0) {
|
|
1424
|
+
return fail('review_import', 'the verified commit is not descended from the protected snapshot');
|
|
1425
|
+
}
|
|
1426
|
+
const refs = spawnSync('git', ['--git-dir', boundary.gitDir, 'show-ref'], { encoding: 'utf8', env: trustedGitEnvironment() });
|
|
1427
|
+
if (refs.status !== 0) return fail('review_import', refs.stderr || 'could not inspect sealed refs');
|
|
1428
|
+
const allowedRefs = new Map([
|
|
1429
|
+
[`refs/heads/${boundary.branch}`, headOid],
|
|
1430
|
+
['refs/remotes/origin/HEAD', boundary.baseOid],
|
|
1431
|
+
['refs/remotes/origin/master', boundary.baseOid],
|
|
1432
|
+
]);
|
|
1433
|
+
const refLines = String(refs.stdout || '').trim().split(/\r?\n/).filter(Boolean);
|
|
1434
|
+
const seenRefs = new Set();
|
|
1435
|
+
for (const line of refLines) {
|
|
1436
|
+
const [oid, ref] = line.split(/\s+/, 2);
|
|
1437
|
+
if (!allowedRefs.has(ref) || allowedRefs.get(ref) !== oid) return fail('sandbox_metadata_changed', `the worker created an unexpected Git ref (${ref || 'unknown'})`);
|
|
1438
|
+
seenRefs.add(ref);
|
|
1439
|
+
}
|
|
1440
|
+
for (const ref of [`refs/heads/${boundary.branch}`, 'refs/remotes/origin/master']) {
|
|
1441
|
+
if (!seenRefs.has(ref)) return fail('sandbox_metadata_changed', `the sealed sandbox is missing a required Git ref (${ref})`);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
let baseEntries;
|
|
1445
|
+
let sourceEntries;
|
|
1446
|
+
try {
|
|
1447
|
+
baseEntries = readGitTree(boundary.gitDir, boundary.baseOid);
|
|
1448
|
+
sourceEntries = readGitTree(boundary.gitDir, headOid);
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
return fail('review_import', error.message);
|
|
1451
|
+
}
|
|
1452
|
+
const baseByPath = new Map(baseEntries.map((entry) => [entry.relative, entry]));
|
|
1453
|
+
const sourceByPath = new Map(sourceEntries.map((entry) => [entry.relative, entry]));
|
|
1454
|
+
const changedEntries = [...new Set([...baseByPath.keys(), ...sourceByPath.keys()])]
|
|
1455
|
+
.filter((relative) => {
|
|
1456
|
+
const before = baseByPath.get(relative);
|
|
1457
|
+
const after = sourceByPath.get(relative);
|
|
1458
|
+
return !before || !after || before.oid !== after.oid || before.mode !== after.mode;
|
|
1459
|
+
})
|
|
1460
|
+
.sort()
|
|
1461
|
+
.map((relative) => `${baseByPath.has(relative) ? (sourceByPath.has(relative) ? 'M' : 'D') : 'A'} ${relative}`);
|
|
1462
|
+
if (!changedEntries.length) return fail('no_change', 'the verified commit has no tree change');
|
|
1463
|
+
|
|
1464
|
+
const started = cli(['worktree', 'start', '--agent', engine, '--task', `dispatch-${String(taskId).toLowerCase()}`, ...startBaseArgs]);
|
|
1465
|
+
const landingPath = (String(started && started.stdout || '').match(/next: cd (.+)/) || [])[1];
|
|
1466
|
+
if (!landingPath) return fail('worktree_start', String(started && (started.stderr || started.stdout) || 'could not create review worktree'));
|
|
1467
|
+
const worktreePath = landingPath.trim();
|
|
1468
|
+
const landingHead = spawnSync('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
|
|
1469
|
+
cwd: worktreePath,
|
|
1470
|
+
encoding: 'utf8',
|
|
1471
|
+
env: trustedGitEnvironment(),
|
|
1472
|
+
});
|
|
1473
|
+
if (landingHead.status !== 0 || String(landingHead.stdout || '').trim() !== boundary.baseOid) {
|
|
1474
|
+
return fail('review_import', 'the review worktree was not cut from the verified base', { worktreePath });
|
|
1475
|
+
}
|
|
1476
|
+
let proofRef;
|
|
1477
|
+
let proofTree;
|
|
1478
|
+
try {
|
|
1479
|
+
proofRef = persistVerifiedCommit(boundary, worktreePath, headOid);
|
|
1480
|
+
proofTree = bindOneLapProof(worktreePath, proofRef, headOid);
|
|
1481
|
+
materializeVerifiedTree(worktreePath, baseEntries, sourceEntries);
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
return fail('review_import', error.message, { worktreePath });
|
|
1484
|
+
}
|
|
1485
|
+
return {
|
|
1486
|
+
ok: true,
|
|
1487
|
+
stage: 'verified_for_review',
|
|
1488
|
+
worktreePath,
|
|
1489
|
+
sourceWorktreePath: boundary.worktreePath,
|
|
1490
|
+
head: headOid,
|
|
1491
|
+
proof_ref: proofRef,
|
|
1492
|
+
proof_tree: proofTree,
|
|
1493
|
+
protected_master: boundary.protectedMaster,
|
|
1494
|
+
change: {
|
|
1495
|
+
has_change: true,
|
|
1496
|
+
base: boundary.baseOid,
|
|
1497
|
+
head: boundary.baseOid,
|
|
1498
|
+
source_commit: headOid,
|
|
1499
|
+
proof_ref: proofRef,
|
|
1500
|
+
proof_tree: proofTree,
|
|
1501
|
+
commit: headOid,
|
|
1502
|
+
dirty: true,
|
|
1503
|
+
changed_entries: changedEntries.slice(0, 100),
|
|
1504
|
+
},
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function disposeReviewSandbox(boundary) {
|
|
1509
|
+
spawnSync('git', ['--git-dir', boundary.gitDir, 'worktree', 'remove', '--force', boundary.worktreePath], { encoding: 'utf8' });
|
|
1510
|
+
fs.rmSync(boundary.worktreePath, { recursive: true, force: true });
|
|
1511
|
+
fs.rmSync(boundary.gitDir, { recursive: true, force: true });
|
|
1512
|
+
fs.rmSync(boundary.quarantine, { recursive: true, force: true });
|
|
1513
|
+
fs.rmSync(boundary.sandboxRoot, { recursive: true, force: true });
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function inspectReviewChange(worktreePath, baseRef = 'origin/master') {
|
|
1517
|
+
const run = (args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8', env: trustedGitEnvironment() });
|
|
1518
|
+
const base = run(['rev-parse', '--verify', `${baseRef}^{commit}`]);
|
|
1519
|
+
const head = run(['rev-parse', '--verify', 'HEAD^{commit}']);
|
|
1520
|
+
if (base.status !== 0 || head.status !== 0) {
|
|
1521
|
+
return {
|
|
1522
|
+
has_change: false,
|
|
1523
|
+
detail: String(base.stderr || head.stderr || 'could not resolve the review change boundary').trim(),
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
const baseOid = String(base.stdout || '').trim();
|
|
1527
|
+
const headOid = String(head.stdout || '').trim();
|
|
1528
|
+
const committed = run(['diff', '--quiet', baseOid, headOid, '--']);
|
|
1529
|
+
if (committed.status !== 0 && committed.status !== 1) {
|
|
1530
|
+
return {
|
|
1531
|
+
has_change: false,
|
|
1532
|
+
base: baseOid,
|
|
1533
|
+
head: headOid,
|
|
1534
|
+
detail: String(committed.stderr || 'could not inspect committed changes').trim(),
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
const status = run(['status', '--porcelain=v1', '--untracked-files=all']);
|
|
1538
|
+
if (status.status !== 0) {
|
|
1539
|
+
return {
|
|
1540
|
+
has_change: false,
|
|
1541
|
+
base: baseOid,
|
|
1542
|
+
head: headOid,
|
|
1543
|
+
detail: String(status.stderr || 'could not inspect worktree changes').trim(),
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
const conductorEntry = /^\?\? \.atris\/(?:agent-worktree\.json|state\/briefs\.jsonl)$/;
|
|
1547
|
+
const dirty = String(status.stdout || '').trim().split('\n').filter(Boolean)
|
|
1548
|
+
.filter((line) => !conductorEntry.test(line));
|
|
1549
|
+
const hasCommittedDiff = committed.status === 1;
|
|
1550
|
+
return {
|
|
1551
|
+
has_change: hasCommittedDiff || dirty.length > 0,
|
|
1552
|
+
base: baseOid,
|
|
1553
|
+
head: headOid,
|
|
1554
|
+
commit: hasCommittedDiff ? headOid : null,
|
|
1555
|
+
dirty: dirty.length > 0,
|
|
1556
|
+
changed_entries: dirty.slice(0, 100),
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function reviewWorktreeSnapshot(worktreePath) {
|
|
1561
|
+
const run = (args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8', env: trustedGitEnvironment() });
|
|
1562
|
+
const head = run(['rev-parse', '--verify', 'HEAD^{commit}']);
|
|
1563
|
+
const status = run(['status', '--porcelain=v1', '-z', '--untracked-files=all']);
|
|
1564
|
+
const diff = run(['diff', '--binary', 'HEAD', '--']);
|
|
1565
|
+
const untracked = run(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
1566
|
+
const localConfig = run(['config', '--local', '--null', '--list', '--show-origin']);
|
|
1567
|
+
const worktreeConfig = run(['config', '--worktree', '--null', '--list', '--show-origin']);
|
|
1568
|
+
const refs = run(['show-ref', '--head']);
|
|
1569
|
+
const failed = [head, status, diff, untracked, localConfig, worktreeConfig, refs]
|
|
1570
|
+
.find((result) => result.status !== 0);
|
|
1571
|
+
if (failed) {
|
|
1572
|
+
return {
|
|
1573
|
+
ok: false,
|
|
1574
|
+
detail: String(failed.stderr || failed.stdout || 'could not snapshot validator worktree').trim(),
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
const hash = crypto.createHash('sha256');
|
|
1578
|
+
hash.update(String(head.stdout || ''));
|
|
1579
|
+
hash.update('\0status\0');
|
|
1580
|
+
hash.update(String(status.stdout || ''));
|
|
1581
|
+
hash.update('\0diff\0');
|
|
1582
|
+
hash.update(String(diff.stdout || ''));
|
|
1583
|
+
hash.update('\0local-config\0');
|
|
1584
|
+
hash.update(String(localConfig.stdout || ''));
|
|
1585
|
+
hash.update('\0worktree-config\0');
|
|
1586
|
+
hash.update(String(worktreeConfig.stdout || ''));
|
|
1587
|
+
hash.update('\0refs\0');
|
|
1588
|
+
hash.update(String(refs.stdout || ''));
|
|
1589
|
+
for (const relative of String(untracked.stdout || '').split('\0').filter(Boolean).sort()) {
|
|
1590
|
+
hash.update('\0untracked\0');
|
|
1591
|
+
hash.update(relative);
|
|
1592
|
+
const absolute = path.resolve(worktreePath, relative);
|
|
1593
|
+
try {
|
|
1594
|
+
const info = fs.lstatSync(absolute);
|
|
1595
|
+
if (info.isSymbolicLink()) hash.update(`symlink:${fs.readlinkSync(absolute)}`);
|
|
1596
|
+
else if (info.isFile()) hash.update(fs.readFileSync(absolute));
|
|
1597
|
+
else hash.update(`mode:${info.mode}:size:${info.size}`);
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
return { ok: false, detail: `could not hash validator worktree entry ${relative}: ${error.message}` };
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return {
|
|
1603
|
+
ok: true,
|
|
1604
|
+
head: String(head.stdout || '').trim(),
|
|
1605
|
+
status: String(status.stdout || ''),
|
|
1606
|
+
digest: hash.digest('hex'),
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
async function runIndependentValidator({
|
|
1611
|
+
root,
|
|
1612
|
+
task,
|
|
1613
|
+
worktreePath,
|
|
1614
|
+
executorEngine,
|
|
1615
|
+
verifierCommand,
|
|
1616
|
+
validatorEngines,
|
|
1617
|
+
validatorDispatcher = null,
|
|
1618
|
+
stateInspector = reviewWorktreeSnapshot,
|
|
1619
|
+
}) {
|
|
1620
|
+
const candidates = [...new Set((validatorEngines || []).map((value) => String(value || '').trim()).filter(Boolean))]
|
|
1621
|
+
.filter((name) => RUNNER_PROFILE_DEFS[name] && name !== executorEngine);
|
|
1622
|
+
if (!candidates.length) {
|
|
1623
|
+
return {
|
|
1624
|
+
ok: false,
|
|
1625
|
+
stage: 'validator_unavailable',
|
|
1626
|
+
validator_result: {
|
|
1627
|
+
engine: null,
|
|
1628
|
+
executor_engine: executorEngine,
|
|
1629
|
+
independent: false,
|
|
1630
|
+
passed: false,
|
|
1631
|
+
verdict: 'unavailable',
|
|
1632
|
+
reason: 'no distinct ready validator is available',
|
|
1633
|
+
exit_code: null,
|
|
1634
|
+
output: '',
|
|
1635
|
+
brief_id: null,
|
|
1636
|
+
worktree_unchanged: null,
|
|
1637
|
+
},
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
const dispatch = validatorDispatcher || ((entry) => Promise.resolve(dispatchToEngine({
|
|
1641
|
+
task,
|
|
1642
|
+
engine: entry.engine,
|
|
1643
|
+
worktreePath,
|
|
1644
|
+
root,
|
|
1645
|
+
prompt: entry.prompt,
|
|
1646
|
+
environment: reviewOnlyEngineEnvironment(worktreePath, {
|
|
1647
|
+
engine: entry.engine,
|
|
1648
|
+
network: true,
|
|
1649
|
+
writable: false,
|
|
1650
|
+
}),
|
|
1651
|
+
sealed: true,
|
|
1652
|
+
allowedTools: VALIDATOR_ALLOWED_TOOLS,
|
|
1653
|
+
skipBriefCapture: true,
|
|
1654
|
+
})));
|
|
1655
|
+
|
|
1656
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
1657
|
+
const validatorEngine = candidates[index];
|
|
1658
|
+
const prompt = buildOneLapValidatorPrompt(task, { verifierCommand, executorEngine });
|
|
1659
|
+
const before = stateInspector(worktreePath);
|
|
1660
|
+
if (!before || before.ok !== true) {
|
|
1661
|
+
return {
|
|
1662
|
+
ok: false,
|
|
1663
|
+
stage: 'validator_snapshot_failed',
|
|
1664
|
+
validator_result: {
|
|
1665
|
+
engine: validatorEngine,
|
|
1666
|
+
executor_engine: executorEngine,
|
|
1667
|
+
independent: true,
|
|
1668
|
+
passed: false,
|
|
1669
|
+
verdict: 'invalid',
|
|
1670
|
+
reason: String(before && before.detail || 'could not snapshot worktree before validation'),
|
|
1671
|
+
exit_code: null,
|
|
1672
|
+
output: '',
|
|
1673
|
+
brief_id: null,
|
|
1674
|
+
worktree_unchanged: null,
|
|
1675
|
+
},
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
let dispatched;
|
|
1679
|
+
try {
|
|
1680
|
+
dispatched = normalizeDispatchResult(await dispatch({
|
|
1681
|
+
task,
|
|
1682
|
+
engine: validatorEngine,
|
|
1683
|
+
worktreePath,
|
|
1684
|
+
prompt,
|
|
1685
|
+
}), validatorEngine);
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
dispatched = normalizeDispatchResult({ exitCode: 1, stderr: error.message || String(error) }, validatorEngine);
|
|
1688
|
+
}
|
|
1689
|
+
const after = stateInspector(worktreePath);
|
|
1690
|
+
const output = dispatchResultOutput(dispatched).slice(-8000);
|
|
1691
|
+
const verdictOutput = [dispatched.report, dispatched.stdout]
|
|
1692
|
+
.map((value) => String(value || ''))
|
|
1693
|
+
.filter(Boolean)
|
|
1694
|
+
.join('\n');
|
|
1695
|
+
const verdict = parseOneLapValidatorVerdict(verdictOutput);
|
|
1696
|
+
const unchanged = Boolean(after && after.ok === true && before.digest === after.digest);
|
|
1697
|
+
const exitCode = dispatchResultExitCode(dispatched);
|
|
1698
|
+
const validatorResult = {
|
|
1699
|
+
engine: validatorEngine,
|
|
1700
|
+
executor_engine: executorEngine,
|
|
1701
|
+
independent: validatorEngine !== executorEngine,
|
|
1702
|
+
passed: exitCode === 0 && verdict.passed === true && unchanged,
|
|
1703
|
+
verdict: verdict.verdict,
|
|
1704
|
+
reason: unchanged ? verdict.reason : String(after && after.detail || 'validator changed the worktree'),
|
|
1705
|
+
exit_code: exitCode,
|
|
1706
|
+
output,
|
|
1707
|
+
brief_id: dispatched.brief_id || null,
|
|
1708
|
+
worktree_unchanged: unchanged,
|
|
1709
|
+
};
|
|
1710
|
+
if (!unchanged) return { ok: false, stage: 'validator_mutated_worktree', validator_result: validatorResult };
|
|
1711
|
+
if (exitCode !== 0) {
|
|
1712
|
+
const outage = detectDeadEngineDispatch(dispatched);
|
|
1713
|
+
if (outage && outage.reason === 'usage_limit' && index + 1 < candidates.length) continue;
|
|
1714
|
+
validatorResult.reason = String(dispatched.stderr || verdict.reason || `validator exited ${exitCode}`).trim().slice(-500);
|
|
1715
|
+
return { ok: false, stage: 'validator_failed', validator_result: validatorResult };
|
|
1716
|
+
}
|
|
1717
|
+
if (verdict.verdict === 'reject') return { ok: false, stage: 'validation_rejected', validator_result: validatorResult };
|
|
1718
|
+
if (!verdict.passed) return { ok: false, stage: 'validator_failed', validator_result: validatorResult };
|
|
1719
|
+
return { ok: true, stage: 'validator_signed_off', validator_result: validatorResult };
|
|
1720
|
+
}
|
|
1721
|
+
return { ok: false, stage: 'validator_failed', validator_result: null };
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function dispatchReceiptResult(flight, { ids, reviewOnly, enforceRemoteBoundary }) {
|
|
1725
|
+
const validatorResult = flight.validator_result || flight.result?.validator_result;
|
|
1726
|
+
const successfulRows = reviewOnly ? flight.ready : flight.landed;
|
|
1727
|
+
const successfulVerifierRows = successfulRows
|
|
1728
|
+
.map((row) => row.verifier_result)
|
|
1729
|
+
.filter((row) => row && typeof row === 'object');
|
|
1730
|
+
const verifierRows = [
|
|
1731
|
+
...successfulVerifierRows,
|
|
1732
|
+
...flight.paused.map((row) => row.verifier_result).filter((row) => row && typeof row === 'object'),
|
|
1733
|
+
];
|
|
1734
|
+
const basePassed = flight.paused.length === 0
|
|
1735
|
+
&& successfulRows.length === ids.length
|
|
1736
|
+
&& successfulVerifierRows.length === ids.length
|
|
1737
|
+
&& successfulVerifierRows.every((row) => row.passed === true);
|
|
1738
|
+
const oneLapReview = reviewOnly && flight.context && flight.context.source === 'one_lap';
|
|
1739
|
+
const validatorPassed = validatorResult
|
|
1740
|
+
&& validatorResult.passed === true
|
|
1741
|
+
&& validatorResult.independent === true
|
|
1742
|
+
&& validatorResult.worktree_unchanged === true
|
|
1743
|
+
&& validatorResult.engine
|
|
1744
|
+
&& validatorResult.executor_engine
|
|
1745
|
+
&& validatorResult.engine !== validatorResult.executor_engine;
|
|
1746
|
+
const passed = basePassed && (!oneLapReview || validatorPassed === true);
|
|
1747
|
+
const result = {
|
|
1748
|
+
kind: reviewOnly ? 'dispatch_review_ready' : 'dispatch_landed',
|
|
1749
|
+
passed,
|
|
1750
|
+
verifier_result: verifierRows.length === 1
|
|
1751
|
+
? verifierRows[0]
|
|
1752
|
+
: { passed, checks: verifierRows },
|
|
1753
|
+
};
|
|
1754
|
+
if (reviewOnly) {
|
|
1755
|
+
result.master_boundary_enforced = enforceRemoteBoundary;
|
|
1756
|
+
result.master_unchanged = enforceRemoteBoundary
|
|
1757
|
+
? flight.ready.length === ids.length && flight.ready.every((row) => row.master_before && row.master_before === row.master_after)
|
|
1758
|
+
: null;
|
|
1759
|
+
}
|
|
1760
|
+
if (validatorResult && typeof validatorResult === 'object') {
|
|
1761
|
+
result.validator_result = validatorResult;
|
|
1762
|
+
}
|
|
1763
|
+
return result;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function writeDispatchReceipt(flight, receiptPath, resultOptions) {
|
|
1767
|
+
flight.result = dispatchReceiptResult(flight, resultOptions);
|
|
1768
|
+
flight.receipt = receiptPath;
|
|
1769
|
+
fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
|
|
1770
|
+
const tempPath = `${receiptPath}.tmp-${process.pid}`;
|
|
1771
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(flight, null, 2)}\n`);
|
|
1772
|
+
fs.renameSync(tempPath, receiptPath);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function stampDispatchResultVerification(flight, taskId, engine, startedAtMs, verifierResult) {
|
|
1776
|
+
if (!verifierResult || typeof verifierResult.passed !== 'boolean') return;
|
|
1777
|
+
const row = flight.results.find((result) => result.task === taskId);
|
|
1778
|
+
if (!row) return;
|
|
1779
|
+
const completedAtMs = Date.now();
|
|
1780
|
+
row.engine = engine;
|
|
1781
|
+
row.verified_passed = verifierResult.passed;
|
|
1782
|
+
row.duration_ms = Math.max(0, completedAtMs - startedAtMs);
|
|
1783
|
+
row.at = new Date(completedAtMs).toISOString();
|
|
283
1784
|
}
|
|
284
1785
|
|
|
285
1786
|
function readProjectionTasks(root) {
|
|
@@ -346,6 +1847,79 @@ function defaultVerifyRunner(command, cwd) {
|
|
|
346
1847
|
return { status: result.status, stdout: String(result.stdout || ''), stderr: String(result.stderr || '') };
|
|
347
1848
|
}
|
|
348
1849
|
|
|
1850
|
+
function defaultTrustedVerifyRunner(command, cwd) {
|
|
1851
|
+
const parsed = require('./auto-accept-certified').parseVerifyCommand(command);
|
|
1852
|
+
if (!parsed.ok) return { status: 2, stdout: '', stderr: parsed.reason };
|
|
1853
|
+
const worktreeRoot = path.resolve(cwd);
|
|
1854
|
+
const insideWorktree = (target) => target === worktreeRoot || target.startsWith(`${worktreeRoot}${path.sep}`);
|
|
1855
|
+
const commandCwd = parsed.cwd ? path.resolve(worktreeRoot, parsed.cwd) : worktreeRoot;
|
|
1856
|
+
if (!insideWorktree(commandCwd)) {
|
|
1857
|
+
return { status: 2, stdout: '', stderr: 'verify_workdir_outside_worktree' };
|
|
1858
|
+
}
|
|
1859
|
+
if (parsed.argv[0] === 'git' && parsed.argv[1] === '-C') {
|
|
1860
|
+
const gitCwd = path.resolve(commandCwd, parsed.argv[2]);
|
|
1861
|
+
if (!insideWorktree(gitCwd)) {
|
|
1862
|
+
return { status: 2, stdout: '', stderr: 'verify_git_path_outside_worktree' };
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
let boundaryEnv;
|
|
1866
|
+
try {
|
|
1867
|
+
boundaryEnv = reviewOnlyEngineEnvironment(worktreeRoot);
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
return { status: 2, stdout: '', stderr: error.message || String(error) };
|
|
1870
|
+
}
|
|
1871
|
+
const sandboxProfile = String(boundaryEnv.ATRIS_ONE_LAP_SANDBOX_PROFILE || '');
|
|
1872
|
+
const runtimeDir = String(boundaryEnv.ATRIS_ONE_LAP_RUNTIME_DIR || '');
|
|
1873
|
+
const controlDir = String(boundaryEnv.ATRIS_ONE_LAP_CONTROL_DIR || '');
|
|
1874
|
+
const lifecycleWrapper = String(boundaryEnv.ATRIS_ONE_LAP_LIFECYCLE_WRAPPER || '');
|
|
1875
|
+
const statusFile = String(boundaryEnv.ATRIS_ONE_LAP_STATUS_FILE || '');
|
|
1876
|
+
delete boundaryEnv.ATRIS_ONE_LAP_SANDBOX_PROFILE;
|
|
1877
|
+
delete boundaryEnv.ATRIS_ONE_LAP_RUNTIME_DIR;
|
|
1878
|
+
delete boundaryEnv.ATRIS_ONE_LAP_CONTROL_DIR;
|
|
1879
|
+
delete boundaryEnv.ATRIS_ONE_LAP_LIFECYCLE_WRAPPER;
|
|
1880
|
+
delete boundaryEnv.ATRIS_ONE_LAP_STATUS_FILE;
|
|
1881
|
+
const executable = sandboxProfile ? '/usr/bin/sandbox-exec' : parsed.argv[0];
|
|
1882
|
+
const args = sandboxProfile
|
|
1883
|
+
? ['-p', sandboxProfile, lifecycleWrapper, parsed.argv[0], ...parsed.argv.slice(1)]
|
|
1884
|
+
: parsed.argv.slice(1);
|
|
1885
|
+
let result;
|
|
1886
|
+
try {
|
|
1887
|
+
const spawnOptions = {
|
|
1888
|
+
cwd: commandCwd,
|
|
1889
|
+
env: { ...(parsed.env || {}), ...boundaryEnv },
|
|
1890
|
+
encoding: 'utf8',
|
|
1891
|
+
shell: false,
|
|
1892
|
+
timeout: 120000,
|
|
1893
|
+
};
|
|
1894
|
+
result = sandboxProfile
|
|
1895
|
+
? runInReapedProcessGroup(executable, args, spawnOptions, controlDir, statusFile)
|
|
1896
|
+
: spawnSync(executable, args, spawnOptions);
|
|
1897
|
+
} finally {
|
|
1898
|
+
if (runtimeDir) fs.rmSync(runtimeDir, { recursive: true, force: true });
|
|
1899
|
+
if (controlDir) fs.rmSync(controlDir, { recursive: true, force: true });
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
status: Number.isInteger(result.status) ? result.status : 1,
|
|
1903
|
+
stdout: String(result.stdout || ''),
|
|
1904
|
+
stderr: String(result.stderr || result.error && result.error.message || ''),
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
function defaultSelfLandCheck({ worktreePath, targetRef = DISPATCH_SELF_LAND_TARGET, git = null } = {}) {
|
|
1909
|
+
const run = git || ((args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8' }));
|
|
1910
|
+
const branch = String(targetRef || '').startsWith('origin/') ? String(targetRef).slice('origin/'.length) : '';
|
|
1911
|
+
const fetch = run(branch ? ['fetch', 'origin', `${branch}:refs/remotes/origin/${branch}`] : ['fetch', 'origin']);
|
|
1912
|
+
if (fetch.status !== 0) {
|
|
1913
|
+
return { ok: false, stage: 'self_land_check', target: targetRef, detail: String(fetch.stderr || fetch.stdout || '').trim() };
|
|
1914
|
+
}
|
|
1915
|
+
const ancestor = run(['merge-base', '--is-ancestor', 'HEAD', targetRef]);
|
|
1916
|
+
if (ancestor.status === 0) return { ok: true, stage: 'self_landed', target: targetRef };
|
|
1917
|
+
if (ancestor.status === 1) {
|
|
1918
|
+
return { ok: false, stage: 'self_land_missing', target: targetRef, detail: `HEAD is not an ancestor of ${targetRef}` };
|
|
1919
|
+
}
|
|
1920
|
+
return { ok: false, stage: 'self_land_check', target: targetRef, detail: String(ancestor.stderr || ancestor.stdout || '').trim() };
|
|
1921
|
+
}
|
|
1922
|
+
|
|
349
1923
|
// One flight. Staff -> dispatch in parallel -> land serially -> receipt.
|
|
350
1924
|
// No fleet state file: progress is narrated via `log`, durability lives in
|
|
351
1925
|
// worktrees, task claims, and the receipt written at the end.
|
|
@@ -363,7 +1937,10 @@ async function runFleetFlight({
|
|
|
363
1937
|
const cli = ownCli || defaultOwnCli(root);
|
|
364
1938
|
const roster = engines || (() => {
|
|
365
1939
|
const { roster: fullRoster } = require('../commands/engine');
|
|
366
|
-
|
|
1940
|
+
const installed = fullRoster(root)
|
|
1941
|
+
.filter((e) => e.installed && FLEET_CAPABLE.includes(e.name))
|
|
1942
|
+
.map((e) => e.name);
|
|
1943
|
+
return rankFleetEngines(installed, root);
|
|
367
1944
|
})();
|
|
368
1945
|
|
|
369
1946
|
const staffed = assignEngines(staffFlight(readProjectionTasks(root), { slots }), roster);
|
|
@@ -393,8 +1970,17 @@ async function runFleetFlight({
|
|
|
393
1970
|
|
|
394
1971
|
// Claim + cut a worktree per assignment, then dispatch all in parallel.
|
|
395
1972
|
const dispatch = dispatcher || ((entry) => new Promise((resolve) => {
|
|
396
|
-
resolve(dispatchToEngine({
|
|
1973
|
+
resolve(dispatchToEngine({
|
|
1974
|
+
task: entry.task,
|
|
1975
|
+
engine: entry.engine,
|
|
1976
|
+
worktreePath: entry.worktreePath,
|
|
1977
|
+
root,
|
|
1978
|
+
prompt: entry.prompt,
|
|
1979
|
+
briefId: entry.brief_id,
|
|
1980
|
+
skipBriefCapture: true,
|
|
1981
|
+
}));
|
|
397
1982
|
}));
|
|
1983
|
+
const restaffState = { used: false };
|
|
398
1984
|
|
|
399
1985
|
// Cut every build worktree from origin/master by default, not the launcher's
|
|
400
1986
|
// HEAD. A flight launched from a long-lived feature-branch checkout would
|
|
@@ -417,9 +2003,22 @@ async function runFleetFlight({
|
|
|
417
2003
|
}
|
|
418
2004
|
|
|
419
2005
|
const results = await Promise.all(prepared.map((entry) =>
|
|
420
|
-
|
|
2006
|
+
dispatchEntryWithRestaff({
|
|
2007
|
+
entry,
|
|
2008
|
+
engine: entry.engine,
|
|
2009
|
+
root,
|
|
2010
|
+
dispatch,
|
|
2011
|
+
installedEngines: roster,
|
|
2012
|
+
restaffState,
|
|
2013
|
+
}).then((result) => ({ entry, result }))
|
|
421
2014
|
));
|
|
422
|
-
flight.results = results.map(({ entry, result }) =>
|
|
2015
|
+
flight.results = results.map(({ entry, result }) => {
|
|
2016
|
+
const row = { task: entry.task.display_id, engine: result.engine || entry.engine, exitCode: result.exitCode };
|
|
2017
|
+
if (result.brief_id) row.brief_id = result.brief_id;
|
|
2018
|
+
if (result.restaffed) row.restaffed = result.restaffed;
|
|
2019
|
+
if (result.deadEngine) row.deadEngine = result.deadEngine;
|
|
2020
|
+
return row;
|
|
2021
|
+
});
|
|
423
2022
|
|
|
424
2023
|
// Land serially: rebase-before-ship, conflict pauses (never auto-resolve).
|
|
425
2024
|
const land = lander || (({ entry }) => {
|
|
@@ -434,20 +2033,38 @@ async function runFleetFlight({
|
|
|
434
2033
|
});
|
|
435
2034
|
|
|
436
2035
|
for (const { entry, result } of results) {
|
|
2036
|
+
const activeEngine = result.engine || entry.engine;
|
|
2037
|
+
const landingEntry = { ...entry, engine: activeEngine };
|
|
2038
|
+
if (result.restaffed) log(` restaffed ${entry.task.display_id}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.reason})`);
|
|
437
2039
|
if (result.exitCode !== 0) {
|
|
438
|
-
|
|
439
|
-
|
|
2040
|
+
const paused = { task: entry.task.display_id, engine: activeEngine, stage: 'build', detail: (result.stderr || '').slice(0, 200) };
|
|
2041
|
+
if (result.restaffed) paused.restaffed = result.restaffed;
|
|
2042
|
+
if (result.deadEngine) paused.deadEngine = result.deadEngine;
|
|
2043
|
+
flight.paused.push(paused);
|
|
2044
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `build failed for ${entry.task.display_id}`);
|
|
2045
|
+
log(` ${activeEngine.padEnd(8)} ✗ build failed ${entry.task.display_id} — worktree kept for takeover`);
|
|
440
2046
|
continue;
|
|
441
2047
|
}
|
|
442
|
-
log(` ${
|
|
443
|
-
const landed = land({ entry, result });
|
|
2048
|
+
log(` ${activeEngine.padEnd(8)} landing ${entry.task.display_id}...`);
|
|
2049
|
+
const landed = land({ entry: landingEntry, result });
|
|
444
2050
|
if (landed.ok) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
2051
|
+
const landedRow = { task: entry.task.display_id, engine: activeEngine };
|
|
2052
|
+
if (result.brief_id) landedRow.brief_id = result.brief_id;
|
|
2053
|
+
if (result.restaffed) landedRow.restaffed = result.restaffed;
|
|
2054
|
+
flight.landed.push(landedRow);
|
|
2055
|
+
stampDispatchBrief(root, result.brief_id, 'pass', `landed ${entry.task.display_id} via fleet ship`);
|
|
2056
|
+
const restaffProof = result.restaffed ? ` Restaffed from ${result.restaffed.from} to ${result.restaffed.to} (${result.restaffed.reason}).` : '';
|
|
2057
|
+
cli([
|
|
2058
|
+
'task', 'ready', String(entry.task.display_id),
|
|
2059
|
+
'--proof', `Built by ${activeEngine} engine in fleet flight.${restaffProof} Landed via worktree ship gate (rebase-before-ship, verify re-run). Receipt saved at ${path.relative(root, receiptPath)}. Report tail: ${String(result.report || '').slice(-300).replace(/\n/g, ' ')}`,
|
|
2060
|
+
'--result', 'Operators can now review fleet-shipped work faster because the worktree was verified before landing.',
|
|
2061
|
+
'--as', `fleet-${activeEngine}`,
|
|
2062
|
+
]);
|
|
2063
|
+
log(` ${activeEngine.padEnd(8)} ✓ landed ${entry.task.display_id}`);
|
|
448
2064
|
} else {
|
|
449
|
-
flight.paused.push({ task: entry.task.display_id, engine:
|
|
450
|
-
|
|
2065
|
+
flight.paused.push({ task: entry.task.display_id, engine: activeEngine, ...(result.restaffed ? { restaffed: result.restaffed } : {}), ...landed });
|
|
2066
|
+
stampDispatchBrief(root, result.brief_id, 'partial', `paused ${entry.task.display_id} at ${landed.stage || 'landing'}`);
|
|
2067
|
+
log(` ${activeEngine.padEnd(8)} ⏸ paused ${entry.task.display_id} at ${landed.stage}${landed.conflicts ? ` (${landed.conflicts.join(', ')})` : ''} — worktree kept`);
|
|
451
2068
|
}
|
|
452
2069
|
}
|
|
453
2070
|
|
|
@@ -483,6 +2100,17 @@ async function runDispatchFlight({
|
|
|
483
2100
|
verifier = null,
|
|
484
2101
|
rebase = null,
|
|
485
2102
|
checkoutBase = 'origin/master',
|
|
2103
|
+
installedEngines = null,
|
|
2104
|
+
selfLandCheck = null,
|
|
2105
|
+
yolo = false,
|
|
2106
|
+
reviewOnly = false,
|
|
2107
|
+
verifierCommand = '',
|
|
2108
|
+
receiptContext = null,
|
|
2109
|
+
actor = '',
|
|
2110
|
+
changeInspector = null,
|
|
2111
|
+
validatorEngines = null,
|
|
2112
|
+
validatorDispatcher = null,
|
|
2113
|
+
validatorStateInspector = null,
|
|
486
2114
|
} = {}) {
|
|
487
2115
|
if (!engine) throw new Error('runDispatchFlight: engine is required');
|
|
488
2116
|
if (!FLEET_CAPABLE.includes(engine)) {
|
|
@@ -493,11 +2121,40 @@ async function runDispatchFlight({
|
|
|
493
2121
|
if (promptOverride && ids.length > 1) {
|
|
494
2122
|
throw new Error('runDispatchFlight: --prompt-file only supports a single task id');
|
|
495
2123
|
}
|
|
2124
|
+
const trustedVerifier = String(verifierCommand || '').trim();
|
|
2125
|
+
if (trustedVerifier) {
|
|
2126
|
+
const parsedVerifier = require('./auto-accept-certified').parseVerifyCommand(trustedVerifier);
|
|
2127
|
+
if (!parsedVerifier.ok) {
|
|
2128
|
+
throw new Error(`runDispatchFlight: verifier command is not allowed (${parsedVerifier.reason})`);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if (reviewOnly && !trustedVerifier) {
|
|
2132
|
+
throw new Error('runDispatchFlight: review-only dispatch requires an explicit verifier command');
|
|
2133
|
+
}
|
|
496
2134
|
|
|
497
2135
|
const cli = ownCli || defaultOwnCli(root);
|
|
498
|
-
const verify = verifier || defaultVerifyRunner;
|
|
2136
|
+
const verify = verifier || (trustedVerifier ? defaultTrustedVerifyRunner : defaultVerifyRunner);
|
|
2137
|
+
const inspectChange = changeInspector || inspectReviewChange;
|
|
2138
|
+
const enforceRemoteBoundary = reviewOnly && !dispatcher;
|
|
2139
|
+
const explicitActor = String(actor || '').trim();
|
|
2140
|
+
const taskActor = explicitActor || `fleet-${engine}`;
|
|
499
2141
|
const receiptPath = path.join(root, 'atris', 'runs', `dispatch-${nowStamp()}.json`);
|
|
500
|
-
const flight = {
|
|
2142
|
+
const flight = {
|
|
2143
|
+
schema: 'atris.dispatch_receipt.v1',
|
|
2144
|
+
at: new Date().toISOString(),
|
|
2145
|
+
root,
|
|
2146
|
+
engine,
|
|
2147
|
+
tasks: ids,
|
|
2148
|
+
results: [],
|
|
2149
|
+
landed: [],
|
|
2150
|
+
ready: [],
|
|
2151
|
+
paused: [],
|
|
2152
|
+
};
|
|
2153
|
+
if (yolo) flight.yolo = true;
|
|
2154
|
+
if (reviewOnly) flight.review_only = true;
|
|
2155
|
+
flight.actor = taskActor;
|
|
2156
|
+
if (receiptContext && typeof receiptContext === 'object') flight.context = receiptContext;
|
|
2157
|
+
const requiresIndependentValidator = reviewOnly && flight.context && flight.context.source === 'one_lap';
|
|
501
2158
|
|
|
502
2159
|
log('');
|
|
503
2160
|
log(` dispatch — ${ids.length} task${ids.length === 1 ? '' : 's'} -> ${engine}`);
|
|
@@ -514,84 +2171,579 @@ async function runDispatchFlight({
|
|
|
514
2171
|
log(` ✗ ${taskId} not found`);
|
|
515
2172
|
continue;
|
|
516
2173
|
}
|
|
517
|
-
cli(['task', 'claim', taskId, '--as',
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
log(` ✗ ${taskId} worktree start failed`);
|
|
2174
|
+
const claimed = cli(['task', 'claim', taskId, '--as', taskActor]);
|
|
2175
|
+
if (!claimed || claimed.status !== 0) {
|
|
2176
|
+
const detail = String(claimed && (claimed.stderr || claimed.stdout) || 'claim failed').trim().slice(0, 300);
|
|
2177
|
+
flight.paused.push({ task: taskId, stage: 'claim', detail });
|
|
2178
|
+
log(` x ${taskId} claim failed`);
|
|
523
2179
|
continue;
|
|
524
2180
|
}
|
|
525
|
-
|
|
526
|
-
|
|
2181
|
+
let landingWorktreePath = '';
|
|
2182
|
+
let remoteBoundary = null;
|
|
2183
|
+
if (enforceRemoteBoundary) {
|
|
2184
|
+
remoteBoundary = prepareReviewSandbox({ root, taskId, engine });
|
|
2185
|
+
} else {
|
|
2186
|
+
const started = cli(['worktree', 'start', '--agent', engine, '--task', `dispatch-${taskId.toLowerCase()}`, ...startBaseArgs]);
|
|
2187
|
+
const wt = (started.stdout.match(/next: cd (.+)/) || [])[1];
|
|
2188
|
+
if (!wt) {
|
|
2189
|
+
flight.paused.push({ task: taskId, stage: 'worktree_start', detail: String(started.stderr || '').slice(0, 200) });
|
|
2190
|
+
log(` ✗ ${taskId} worktree start failed`);
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
landingWorktreePath = wt.trim();
|
|
2194
|
+
}
|
|
2195
|
+
if (enforceRemoteBoundary && (!remoteBoundary || remoteBoundary.ok !== true)) {
|
|
2196
|
+
flight.paused.push({
|
|
2197
|
+
task: taskId,
|
|
2198
|
+
stage: 'remote_quarantine',
|
|
2199
|
+
detail: String(remoteBoundary && remoteBoundary.detail || 'could not prepare a sealed review sandbox').slice(-500),
|
|
2200
|
+
worktree: null,
|
|
2201
|
+
});
|
|
2202
|
+
log(` paused ${taskId} because its sealed review sandbox could not be prepared`);
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
const worktreePath = enforceRemoteBoundary ? remoteBoundary.worktreePath : landingWorktreePath;
|
|
2206
|
+
const basePrompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo });
|
|
2207
|
+
const safetyPrompt = reviewOnly
|
|
2208
|
+
? `${basePrompt}\n\nSafety boundary: edit and test this checkout only. Do not push, merge, deploy, publish, send messages, change cloud state, install dependencies, or use credentials.`
|
|
2209
|
+
: basePrompt;
|
|
2210
|
+
const trustedPrompt = trustedVerifier
|
|
2211
|
+
? `${safetyPrompt}\n\nTrusted verifier (run it bare before reporting done): ${trustedVerifier}`
|
|
2212
|
+
: promptOverride;
|
|
2213
|
+
prepared.push({
|
|
2214
|
+
task,
|
|
2215
|
+
taskId,
|
|
2216
|
+
worktreePath,
|
|
2217
|
+
landingWorktreePath,
|
|
2218
|
+
engine,
|
|
2219
|
+
remoteBoundary,
|
|
2220
|
+
remoteMasterBefore: remoteBoundary ? remoteBoundary.protectedMaster : '',
|
|
2221
|
+
...(trustedPrompt ? { prompt: trustedPrompt } : {}),
|
|
2222
|
+
});
|
|
2223
|
+
log(` building ${taskId} in ${path.basename(worktreePath)}`);
|
|
527
2224
|
}
|
|
528
2225
|
|
|
529
2226
|
const dispatch = dispatcher || ((entry) => new Promise((resolve) => {
|
|
530
2227
|
resolve(dispatchToEngine({
|
|
531
2228
|
task: entry.task,
|
|
532
|
-
engine,
|
|
2229
|
+
engine: entry.engine,
|
|
533
2230
|
worktreePath: entry.worktreePath,
|
|
534
2231
|
root,
|
|
535
|
-
prompt: promptOverride || undefined,
|
|
2232
|
+
prompt: entry.prompt || promptOverride || undefined,
|
|
2233
|
+
environment: enforceRemoteBoundary ? reviewOnlyEngineEnvironment(entry.worktreePath, {
|
|
2234
|
+
engine: entry.engine,
|
|
2235
|
+
network: true,
|
|
2236
|
+
writable: true,
|
|
2237
|
+
}) : null,
|
|
2238
|
+
sealed: enforceRemoteBoundary,
|
|
2239
|
+
yolo,
|
|
2240
|
+
briefId: entry.brief_id,
|
|
2241
|
+
skipBriefCapture: true,
|
|
536
2242
|
}));
|
|
537
2243
|
}));
|
|
2244
|
+
const restaffState = { used: false };
|
|
538
2245
|
|
|
539
|
-
const results = await Promise.all(prepared.map((entry) =>
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
2246
|
+
const results = await Promise.all(prepared.map((entry) => {
|
|
2247
|
+
const startedAtMs = Date.now();
|
|
2248
|
+
return dispatchEntryWithRestaff({
|
|
2249
|
+
entry,
|
|
2250
|
+
engine: entry.engine,
|
|
2251
|
+
root,
|
|
2252
|
+
dispatch,
|
|
2253
|
+
installedEngines,
|
|
2254
|
+
restaffState,
|
|
2255
|
+
}).then((result) => {
|
|
2256
|
+
const completedAtMs = Date.now();
|
|
2257
|
+
return {
|
|
2258
|
+
entry,
|
|
2259
|
+
result,
|
|
2260
|
+
startedAtMs,
|
|
2261
|
+
completedAtMs,
|
|
2262
|
+
candidate: enforceRemoteBoundary && result.exitCode === 0
|
|
2263
|
+
? reviewCandidateSnapshot(entry.remoteBoundary)
|
|
2264
|
+
: null,
|
|
2265
|
+
};
|
|
2266
|
+
});
|
|
2267
|
+
}));
|
|
2268
|
+
flight.results = results.map(({ entry, result, candidate, startedAtMs, completedAtMs }) => {
|
|
2269
|
+
const row = {
|
|
2270
|
+
task: entry.taskId,
|
|
2271
|
+
engine: result.engine || entry.engine,
|
|
2272
|
+
task_type: 'executor',
|
|
2273
|
+
verified_passed: null,
|
|
2274
|
+
duration_ms: Math.max(0, completedAtMs - startedAtMs),
|
|
2275
|
+
at: new Date(completedAtMs).toISOString(),
|
|
2276
|
+
exitCode: result.exitCode,
|
|
2277
|
+
};
|
|
2278
|
+
if (result.brief_id) row.brief_id = result.brief_id;
|
|
2279
|
+
if (result.restaffed) row.restaffed = result.restaffed;
|
|
2280
|
+
if (result.deadEngine) row.deadEngine = result.deadEngine;
|
|
2281
|
+
if (candidate && candidate.ok) {
|
|
2282
|
+
row.candidate_commit = candidate.commit;
|
|
2283
|
+
row.candidate_tree = candidate.tree;
|
|
2284
|
+
}
|
|
2285
|
+
return row;
|
|
2286
|
+
});
|
|
543
2287
|
|
|
544
|
-
// Land serially: rebase, re-run
|
|
545
|
-
//
|
|
2288
|
+
// Land serially: rebase, re-run the trusted verifier for real, then either
|
|
2289
|
+
// stop proof-ready in Review or ship. Conflict/verify failure always pauses.
|
|
546
2290
|
const rebaseArrival = rebase || landArrival;
|
|
2291
|
+
const checkSelfLand = selfLandCheck || defaultSelfLandCheck;
|
|
547
2292
|
const land = lander || (({ entry }) => {
|
|
548
|
-
const rebased =
|
|
2293
|
+
const rebased = enforceRemoteBoundary
|
|
2294
|
+
? { ok: true, stage: 'frozen_base' }
|
|
2295
|
+
: rebaseArrival({ worktreePath: entry.worktreePath });
|
|
549
2296
|
if (!rebased.ok) return rebased;
|
|
550
|
-
|
|
2297
|
+
if (enforceRemoteBoundary) {
|
|
2298
|
+
const candidate = reviewCandidateSnapshot(entry.remoteBoundary, entry.candidate);
|
|
2299
|
+
if (!candidate.ok) return candidate;
|
|
2300
|
+
}
|
|
2301
|
+
const change = reviewOnly ? inspectChange(entry.worktreePath, checkoutBase || 'origin/master') : null;
|
|
2302
|
+
if (reviewOnly && (!change || change.has_change !== true)) {
|
|
2303
|
+
return {
|
|
2304
|
+
ok: false,
|
|
2305
|
+
stage: 'no_change',
|
|
2306
|
+
detail: String(change && change.detail || 'the engine produced no committed or worktree diff').slice(-500),
|
|
2307
|
+
change: change || null,
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
if (enforceRemoteBoundary && (change.head !== entry.candidate.commit || change.dirty)) {
|
|
2311
|
+
return {
|
|
2312
|
+
ok: false,
|
|
2313
|
+
stage: 'candidate_changed',
|
|
2314
|
+
detail: 'the change selected for verification differs from the frozen executor commit',
|
|
2315
|
+
change,
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
const check = trustedVerifier || dispatchCheck(entry.task) || 'git log -1 --oneline';
|
|
551
2319
|
const verified = verify(check, entry.worktreePath);
|
|
2320
|
+
const verifierResult = {
|
|
2321
|
+
command: check,
|
|
2322
|
+
passed: verified.status === 0,
|
|
2323
|
+
status: verified.status,
|
|
2324
|
+
output: `${verified.stdout || ''}${verified.stderr || ''}`.slice(-4000),
|
|
2325
|
+
...(entry.candidate && entry.candidate.ok ? {
|
|
2326
|
+
candidate_commit: entry.candidate.commit,
|
|
2327
|
+
candidate_tree: entry.candidate.tree,
|
|
2328
|
+
} : {}),
|
|
2329
|
+
};
|
|
552
2330
|
if (verified.status !== 0) {
|
|
553
2331
|
return {
|
|
554
2332
|
ok: false,
|
|
555
2333
|
stage: 'verify_failed',
|
|
556
2334
|
detail: `${verified.stdout}${verified.stderr}`.slice(-500),
|
|
557
2335
|
verifyOutput: `${verified.stdout}${verified.stderr}`,
|
|
2336
|
+
check,
|
|
2337
|
+
verifier_result: verifierResult,
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
if (enforceRemoteBoundary) {
|
|
2341
|
+
const candidate = reviewCandidateSnapshot(entry.remoteBoundary, entry.candidate);
|
|
2342
|
+
if (!candidate.ok) return { ...candidate, check, verifier_result: verifierResult };
|
|
2343
|
+
}
|
|
2344
|
+
if (reviewOnly) {
|
|
2345
|
+
return {
|
|
2346
|
+
ok: true,
|
|
2347
|
+
stage: 'verified_for_review',
|
|
2348
|
+
check,
|
|
2349
|
+
verifyOutput: `${verified.stdout}${verified.stderr}`,
|
|
2350
|
+
verifier_result: verifierResult,
|
|
2351
|
+
change,
|
|
558
2352
|
};
|
|
559
2353
|
}
|
|
560
|
-
const shipped = cli(fleetShipArgs({ task: entry.task, engine }, check), entry.worktreePath);
|
|
2354
|
+
const shipped = cli(fleetShipArgs({ task: entry.task, engine: entry.engine || engine }, check), entry.worktreePath);
|
|
561
2355
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
562
2356
|
return { ok: false, stage: 'ship', detail: (shipped.stderr || shipped.stdout).slice(-500) };
|
|
563
2357
|
}
|
|
564
|
-
return {
|
|
2358
|
+
return {
|
|
2359
|
+
ok: true,
|
|
2360
|
+
stage: 'shipped',
|
|
2361
|
+
check,
|
|
2362
|
+
verifyOutput: `${verified.stdout}${verified.stderr}`,
|
|
2363
|
+
verifier_result: verifierResult,
|
|
2364
|
+
};
|
|
565
2365
|
});
|
|
566
2366
|
|
|
567
|
-
for (const { entry, result } of results) {
|
|
2367
|
+
for (const { entry, result, candidate, startedAtMs } of results) {
|
|
2368
|
+
const activeEngine = result.engine || entry.engine || engine;
|
|
2369
|
+
const readyActor = explicitActor || `fleet-${activeEngine}`;
|
|
2370
|
+
const landingEntry = { ...entry, engine: activeEngine, candidate };
|
|
2371
|
+
if (result.restaffed) log(` restaffed ${entry.taskId}: ${result.restaffed.from} -> ${result.restaffed.to} (${result.restaffed.reason})`);
|
|
568
2372
|
if (result.exitCode !== 0) {
|
|
569
|
-
|
|
2373
|
+
const paused = {
|
|
2374
|
+
task: entry.taskId,
|
|
2375
|
+
engine: activeEngine,
|
|
2376
|
+
stage: 'build',
|
|
2377
|
+
detail: String(result.stderr || '').slice(0, 300),
|
|
2378
|
+
worktree: entry.worktreePath,
|
|
2379
|
+
};
|
|
2380
|
+
if (result.restaffed) paused.restaffed = result.restaffed;
|
|
2381
|
+
if (result.deadEngine) paused.deadEngine = result.deadEngine;
|
|
2382
|
+
flight.paused.push(paused);
|
|
2383
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `build failed for ${entry.taskId}`);
|
|
570
2384
|
log(` ✗ build failed ${entry.taskId} — worktree kept for takeover`);
|
|
571
2385
|
continue;
|
|
572
2386
|
}
|
|
573
|
-
|
|
574
|
-
|
|
2387
|
+
if (enforceRemoteBoundary && (!candidate || candidate.ok !== true)) {
|
|
2388
|
+
const paused = {
|
|
2389
|
+
task: entry.taskId,
|
|
2390
|
+
engine: activeEngine,
|
|
2391
|
+
stage: candidate && candidate.stage || 'candidate_changed',
|
|
2392
|
+
detail: String(candidate && candidate.detail || 'the executor candidate could not be frozen').slice(-500),
|
|
2393
|
+
worktree: entry.worktreePath,
|
|
2394
|
+
};
|
|
2395
|
+
flight.paused.push(paused);
|
|
2396
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `blocked ${entry.taskId}: candidate could not be frozen`);
|
|
2397
|
+
log(` paused ${entry.taskId} at ${paused.stage} - worktree kept`);
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
if (enforceRemoteBoundary) {
|
|
2401
|
+
const boundaryAfterBuild = reviewRemoteBoundaryState(entry.worktreePath, entry.remoteBoundary);
|
|
2402
|
+
if (!boundaryAfterBuild.ok) {
|
|
2403
|
+
flight.paused.push({
|
|
2404
|
+
task: entry.taskId,
|
|
2405
|
+
engine: activeEngine,
|
|
2406
|
+
stage: boundaryAfterBuild.stage,
|
|
2407
|
+
detail: boundaryAfterBuild.detail,
|
|
2408
|
+
worktree: entry.worktreePath,
|
|
2409
|
+
master_before: entry.remoteMasterBefore || null,
|
|
2410
|
+
master_after: boundaryAfterBuild.protected_master || null,
|
|
2411
|
+
});
|
|
2412
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `blocked ${entry.taskId}: ${boundaryAfterBuild.stage} during build`);
|
|
2413
|
+
log(` paused ${entry.taskId} at ${boundaryAfterBuild.stage} - worktree kept`);
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
if (yolo) {
|
|
2418
|
+
log(` checking self-land ${entry.taskId}...`);
|
|
2419
|
+
const selfLanded = checkSelfLand({ entry, result, worktreePath: entry.worktreePath, targetRef: DISPATCH_SELF_LAND_TARGET });
|
|
2420
|
+
if (selfLanded.ok) {
|
|
2421
|
+
const target = selfLanded.target || DISPATCH_SELF_LAND_TARGET;
|
|
2422
|
+
flight.landed.push({
|
|
2423
|
+
task: entry.taskId,
|
|
2424
|
+
engine: activeEngine,
|
|
2425
|
+
landing: 'self',
|
|
2426
|
+
target,
|
|
2427
|
+
verifier_result: {
|
|
2428
|
+
command: `git merge-base --is-ancestor HEAD ${target}`,
|
|
2429
|
+
passed: true,
|
|
2430
|
+
status: 0,
|
|
2431
|
+
output: `HEAD is an ancestor of ${target}`,
|
|
2432
|
+
},
|
|
2433
|
+
...(result.brief_id ? { brief_id: result.brief_id } : {}),
|
|
2434
|
+
});
|
|
2435
|
+
stampDispatchBrief(root, result.brief_id, 'pass', `self-landed ${entry.taskId}`);
|
|
2436
|
+
log(` ✓ self-landed ${entry.taskId}`);
|
|
2437
|
+
} else {
|
|
2438
|
+
const stage = selfLanded.stage || 'self_land_missing';
|
|
2439
|
+
flight.paused.push({
|
|
2440
|
+
task: entry.taskId,
|
|
2441
|
+
engine,
|
|
2442
|
+
stage,
|
|
2443
|
+
target: selfLanded.target || DISPATCH_SELF_LAND_TARGET,
|
|
2444
|
+
detail: selfLanded.detail || '',
|
|
2445
|
+
});
|
|
2446
|
+
stampDispatchBrief(root, result.brief_id, 'partial', `self-land check paused ${entry.taskId} at ${stage}`);
|
|
2447
|
+
log(` ⏸ paused ${entry.taskId} at ${stage}`);
|
|
2448
|
+
}
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
log(` ${reviewOnly ? 'checking' : 'landing'} ${entry.taskId}...`);
|
|
2452
|
+
const landed = land({ entry: landingEntry, result });
|
|
2453
|
+
stampDispatchResultVerification(
|
|
2454
|
+
flight,
|
|
2455
|
+
entry.taskId,
|
|
2456
|
+
activeEngine,
|
|
2457
|
+
startedAtMs,
|
|
2458
|
+
landed.verifier_result,
|
|
2459
|
+
);
|
|
575
2460
|
if (landed.ok) {
|
|
576
|
-
flight.landed.push({ task: entry.taskId, engine, check: landed.check });
|
|
577
2461
|
const verifyTail = String(landed.verifyOutput || '').trim().slice(-1200).replace(/\n/g, ' ');
|
|
2462
|
+
const restaffProof = result.restaffed ? ` Restaffed from ${result.restaffed.from} to ${result.restaffed.to} (${result.restaffed.reason}).` : '';
|
|
2463
|
+
if (reviewOnly) {
|
|
2464
|
+
let finalMaster = entry.remoteMasterBefore;
|
|
2465
|
+
const boundaryBeforeReview = enforceRemoteBoundary
|
|
2466
|
+
? reviewRemoteBoundaryState(entry.worktreePath, entry.remoteBoundary)
|
|
2467
|
+
: { ok: true, protected_master: '' };
|
|
2468
|
+
if (!boundaryBeforeReview.ok) {
|
|
2469
|
+
flight.paused.push({
|
|
2470
|
+
task: entry.taskId,
|
|
2471
|
+
engine: activeEngine,
|
|
2472
|
+
stage: boundaryBeforeReview.stage,
|
|
2473
|
+
detail: boundaryBeforeReview.detail,
|
|
2474
|
+
worktree: entry.worktreePath,
|
|
2475
|
+
master_before: entry.remoteMasterBefore,
|
|
2476
|
+
master_after: boundaryBeforeReview.protected_master || null,
|
|
2477
|
+
});
|
|
2478
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `blocked ${entry.taskId}: ${boundaryBeforeReview.stage} before Review`);
|
|
2479
|
+
log(` paused ${entry.taskId} at ${boundaryBeforeReview.stage} - worktree kept`);
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
finalMaster = boundaryBeforeReview.protected_master || finalMaster;
|
|
2483
|
+
const change = landed.change || inspectChange(entry.worktreePath, checkoutBase || 'origin/master');
|
|
2484
|
+
if (!change || change.has_change !== true) {
|
|
2485
|
+
flight.paused.push({
|
|
2486
|
+
task: entry.taskId,
|
|
2487
|
+
engine: activeEngine,
|
|
2488
|
+
stage: 'no_change',
|
|
2489
|
+
detail: String(change && change.detail || 'the engine produced no committed or worktree diff').slice(-500),
|
|
2490
|
+
check: landed.check,
|
|
2491
|
+
verifier_result: landed.verifier_result,
|
|
2492
|
+
worktree: entry.worktreePath,
|
|
2493
|
+
change: change || null,
|
|
2494
|
+
});
|
|
2495
|
+
stampDispatchBrief(root, result.brief_id, 'partial', `verified ${entry.taskId}, but no code change was found`);
|
|
2496
|
+
log(` paused ${entry.taskId} at no_change - worktree kept`);
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
let validatorResult = null;
|
|
2500
|
+
if (requiresIndependentValidator) {
|
|
2501
|
+
log(` validating ${entry.taskId} in a fresh context...`);
|
|
2502
|
+
const validation = await runIndependentValidator({
|
|
2503
|
+
root,
|
|
2504
|
+
task: entry.task,
|
|
2505
|
+
worktreePath: entry.worktreePath,
|
|
2506
|
+
executorEngine: activeEngine,
|
|
2507
|
+
verifierCommand: landed.check,
|
|
2508
|
+
validatorEngines,
|
|
2509
|
+
validatorDispatcher,
|
|
2510
|
+
stateInspector: validatorStateInspector || reviewWorktreeSnapshot,
|
|
2511
|
+
});
|
|
2512
|
+
validatorResult = validation.validator_result;
|
|
2513
|
+
flight.validator_result = validatorResult;
|
|
2514
|
+
if (!validation.ok) {
|
|
2515
|
+
flight.paused.push({
|
|
2516
|
+
task: entry.taskId,
|
|
2517
|
+
engine: activeEngine,
|
|
2518
|
+
stage: validation.stage,
|
|
2519
|
+
detail: String(validatorResult && validatorResult.reason || 'independent validator failed').slice(-500),
|
|
2520
|
+
check: landed.check,
|
|
2521
|
+
verifier_result: landed.verifier_result,
|
|
2522
|
+
validator_result: validatorResult,
|
|
2523
|
+
worktree: entry.worktreePath,
|
|
2524
|
+
change,
|
|
2525
|
+
});
|
|
2526
|
+
stampDispatchBrief(root, result.brief_id, 'partial', `validator paused ${entry.taskId} at ${validation.stage}`);
|
|
2527
|
+
log(` paused ${entry.taskId} at ${validation.stage} - worktree kept`);
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
if (enforceRemoteBoundary) {
|
|
2531
|
+
const candidateAfterValidator = reviewCandidateSnapshot(entry.remoteBoundary, candidate);
|
|
2532
|
+
if (!candidateAfterValidator.ok) {
|
|
2533
|
+
validatorResult.passed = false;
|
|
2534
|
+
validatorResult.reason = candidateAfterValidator.detail;
|
|
2535
|
+
flight.paused.push({
|
|
2536
|
+
task: entry.taskId,
|
|
2537
|
+
engine: activeEngine,
|
|
2538
|
+
stage: candidateAfterValidator.stage,
|
|
2539
|
+
detail: candidateAfterValidator.detail,
|
|
2540
|
+
check: landed.check,
|
|
2541
|
+
verifier_result: landed.verifier_result,
|
|
2542
|
+
validator_result: validatorResult,
|
|
2543
|
+
worktree: entry.worktreePath,
|
|
2544
|
+
change,
|
|
2545
|
+
});
|
|
2546
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `blocked ${entry.taskId}: candidate changed during validation`);
|
|
2547
|
+
log(` paused ${entry.taskId} at ${candidateAfterValidator.stage} - worktree kept`);
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
if (enforceRemoteBoundary) {
|
|
2552
|
+
const boundaryAfterValidator = reviewRemoteBoundaryState(entry.worktreePath, entry.remoteBoundary);
|
|
2553
|
+
if (!boundaryAfterValidator.ok) {
|
|
2554
|
+
validatorResult.passed = false;
|
|
2555
|
+
validatorResult.reason = boundaryAfterValidator.detail;
|
|
2556
|
+
flight.paused.push({
|
|
2557
|
+
task: entry.taskId,
|
|
2558
|
+
engine: activeEngine,
|
|
2559
|
+
stage: boundaryAfterValidator.stage,
|
|
2560
|
+
detail: validatorResult.reason,
|
|
2561
|
+
check: landed.check,
|
|
2562
|
+
verifier_result: landed.verifier_result,
|
|
2563
|
+
validator_result: validatorResult,
|
|
2564
|
+
worktree: entry.worktreePath,
|
|
2565
|
+
master_before: entry.remoteMasterBefore,
|
|
2566
|
+
master_after: boundaryAfterValidator.protected_master || null,
|
|
2567
|
+
change,
|
|
2568
|
+
});
|
|
2569
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `blocked ${entry.taskId}: ${boundaryAfterValidator.stage} during validation`);
|
|
2570
|
+
log(` paused ${entry.taskId} at ${boundaryAfterValidator.stage} - worktree kept`);
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
finalMaster = boundaryAfterValidator.protected_master;
|
|
2574
|
+
}
|
|
2575
|
+
log(` validator ${validatorResult.engine} signed off ${entry.taskId}`);
|
|
2576
|
+
}
|
|
2577
|
+
let reviewWorktreePath = entry.worktreePath;
|
|
2578
|
+
let reviewChange = change;
|
|
2579
|
+
if (enforceRemoteBoundary) {
|
|
2580
|
+
const imported = importReviewCommit(entry.remoteBoundary, {
|
|
2581
|
+
cli,
|
|
2582
|
+
taskId: entry.taskId,
|
|
2583
|
+
engine: activeEngine,
|
|
2584
|
+
expectedCommit: candidate.commit,
|
|
2585
|
+
expectedTree: candidate.tree,
|
|
2586
|
+
startBaseArgs,
|
|
2587
|
+
});
|
|
2588
|
+
if (!imported.ok) {
|
|
2589
|
+
flight.paused.push({
|
|
2590
|
+
task: entry.taskId,
|
|
2591
|
+
engine: activeEngine,
|
|
2592
|
+
stage: imported.stage || 'review_import',
|
|
2593
|
+
detail: imported.detail || 'the verified commit could not be imported for Review',
|
|
2594
|
+
check: landed.check,
|
|
2595
|
+
verifier_result: landed.verifier_result,
|
|
2596
|
+
...(validatorResult ? { validator_result: validatorResult } : {}),
|
|
2597
|
+
worktree: imported.worktreePath || entry.worktreePath,
|
|
2598
|
+
master_before: entry.remoteMasterBefore,
|
|
2599
|
+
master_after: imported.protected_master || finalMaster || null,
|
|
2600
|
+
change,
|
|
2601
|
+
});
|
|
2602
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `blocked ${entry.taskId}: ${imported.stage || 'review_import'}`);
|
|
2603
|
+
log(` paused ${entry.taskId} at ${imported.stage || 'review_import'} - sandbox kept`);
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
const boundaryAfterImport = reviewRemoteBoundaryState(entry.worktreePath, entry.remoteBoundary);
|
|
2607
|
+
if (!boundaryAfterImport.ok) {
|
|
2608
|
+
flight.paused.push({
|
|
2609
|
+
task: entry.taskId,
|
|
2610
|
+
engine: activeEngine,
|
|
2611
|
+
stage: boundaryAfterImport.stage,
|
|
2612
|
+
detail: boundaryAfterImport.detail,
|
|
2613
|
+
check: landed.check,
|
|
2614
|
+
verifier_result: landed.verifier_result,
|
|
2615
|
+
...(validatorResult ? { validator_result: validatorResult } : {}),
|
|
2616
|
+
worktree: imported.worktreePath,
|
|
2617
|
+
master_before: entry.remoteMasterBefore,
|
|
2618
|
+
master_after: boundaryAfterImport.protected_master || null,
|
|
2619
|
+
change,
|
|
2620
|
+
});
|
|
2621
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `blocked ${entry.taskId}: ${boundaryAfterImport.stage} after import`);
|
|
2622
|
+
log(` paused ${entry.taskId} at ${boundaryAfterImport.stage} after import`);
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
reviewWorktreePath = imported.worktreePath;
|
|
2626
|
+
reviewChange = imported.change;
|
|
2627
|
+
if (!reviewChange || reviewChange.has_change !== true
|
|
2628
|
+
|| reviewChange.source_commit !== candidate.commit
|
|
2629
|
+
|| reviewChange.proof_tree !== candidate.tree
|
|
2630
|
+
|| imported.head !== candidate.commit) {
|
|
2631
|
+
flight.paused.push({
|
|
2632
|
+
task: entry.taskId,
|
|
2633
|
+
engine: activeEngine,
|
|
2634
|
+
stage: 'review_import',
|
|
2635
|
+
detail: 'the imported review worktree did not preserve the verified change',
|
|
2636
|
+
check: landed.check,
|
|
2637
|
+
verifier_result: landed.verifier_result,
|
|
2638
|
+
...(validatorResult ? { validator_result: validatorResult } : {}),
|
|
2639
|
+
worktree: reviewWorktreePath,
|
|
2640
|
+
master_before: entry.remoteMasterBefore,
|
|
2641
|
+
master_after: boundaryAfterImport.protected_master || null,
|
|
2642
|
+
change: reviewChange || null,
|
|
2643
|
+
});
|
|
2644
|
+
stampDispatchBrief(root, result.brief_id, 'fail', `blocked ${entry.taskId}: imported review mismatch`);
|
|
2645
|
+
log(` paused ${entry.taskId} at review_import - review worktree kept`);
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
finalMaster = boundaryAfterImport.protected_master || finalMaster;
|
|
2649
|
+
disposeReviewSandbox(entry.remoteBoundary);
|
|
2650
|
+
}
|
|
2651
|
+
const shipArgs = fleetShipArgs({ task: entry.task, engine: activeEngine }, landed.check);
|
|
2652
|
+
const shellQuote = (value) => /^[A-Za-z0-9_./:-]+$/.test(String(value))
|
|
2653
|
+
? String(value)
|
|
2654
|
+
: `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
2655
|
+
const nextCommand = `cd ${shellQuote(reviewWorktreePath)} && atris ${shipArgs.map(shellQuote).join(' ')}`;
|
|
2656
|
+
const readyRow = {
|
|
2657
|
+
task: entry.taskId,
|
|
2658
|
+
engine: activeEngine,
|
|
2659
|
+
check: landed.check,
|
|
2660
|
+
verifier_result: landed.verifier_result,
|
|
2661
|
+
worktree: reviewWorktreePath,
|
|
2662
|
+
next_action: nextCommand,
|
|
2663
|
+
change: reviewChange,
|
|
2664
|
+
...(validatorResult ? { validator_result: validatorResult } : {}),
|
|
2665
|
+
review_recorded: false,
|
|
2666
|
+
...(enforceRemoteBoundary ? {
|
|
2667
|
+
master_before: entry.remoteMasterBefore,
|
|
2668
|
+
master_after: finalMaster,
|
|
2669
|
+
} : {}),
|
|
2670
|
+
};
|
|
2671
|
+
if (result.brief_id) readyRow.brief_id = result.brief_id;
|
|
2672
|
+
if (result.restaffed) readyRow.restaffed = result.restaffed;
|
|
2673
|
+
|
|
2674
|
+
flight.ready.push(readyRow);
|
|
2675
|
+
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
|
2676
|
+
const verificationCommit = String(reviewChange.source_commit || reviewChange.commit || reviewChange.head || '').trim();
|
|
2677
|
+
const verificationCitation = verificationCommit
|
|
2678
|
+
? ` Verification snapshot: commit ${verificationCommit}${reviewChange.dirty ? ' with a worktree diff' : ''}; ${landed.check} passed (exit 0).`
|
|
2679
|
+
: ` Verification snapshot: the worktree diff was present; ${landed.check} passed (exit 0).`;
|
|
2680
|
+
const validatorCitation = validatorResult
|
|
2681
|
+
? ` Independent validator ${validatorResult.engine} signed off: ${validatorResult.reason}.`
|
|
2682
|
+
: '';
|
|
2683
|
+
const readyResult = cli([
|
|
2684
|
+
'task', 'ready', entry.taskId,
|
|
2685
|
+
'--proof', `Built by ${activeEngine} engine via one-lap dispatch.${restaffProof} Preserved in isolated worktree ${reviewWorktreePath}. Check re-run: ${landed.check}.${verificationCitation}${validatorCitation} Verify output: ${verifyTail || '(command produced no output, exit 0)'}. Receipt saved at ${path.relative(root, receiptPath)}.`,
|
|
2686
|
+
'--result', 'Operators can review a completed, verified change before it lands, reducing the risk of unreviewed changes reaching users.',
|
|
2687
|
+
'--landing', 'Operators can review the verified change before it reaches users, reducing the risk of an unreviewed release.',
|
|
2688
|
+
'--checked', `${landed.check} passed in the isolated worktree`,
|
|
2689
|
+
'--tested', 'The requested behavior passed its declared verifier.',
|
|
2690
|
+
'--as', readyActor,
|
|
2691
|
+
]);
|
|
2692
|
+
if (!readyResult || readyResult.status !== 0) {
|
|
2693
|
+
flight.ready.splice(flight.ready.indexOf(readyRow), 1);
|
|
2694
|
+
flight.paused.push({
|
|
2695
|
+
task: entry.taskId,
|
|
2696
|
+
engine: activeEngine,
|
|
2697
|
+
stage: 'task_ready',
|
|
2698
|
+
detail: String(readyResult && (readyResult.stderr || readyResult.stdout) || 'task ready failed').slice(-500),
|
|
2699
|
+
check: landed.check,
|
|
2700
|
+
verifier_result: landed.verifier_result,
|
|
2701
|
+
worktree: reviewWorktreePath,
|
|
2702
|
+
change: reviewChange,
|
|
2703
|
+
});
|
|
2704
|
+
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
|
2705
|
+
stampDispatchBrief(root, result.brief_id, 'partial', `verified ${entry.taskId}, but task ready failed`);
|
|
2706
|
+
log(` paused ${entry.taskId} at task_ready - worktree kept`);
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
readyRow.review_recorded = true;
|
|
2710
|
+
stampDispatchBrief(root, result.brief_id, 'pass', `verified ${entry.taskId} for Review`);
|
|
2711
|
+
log(` proof ready ${entry.taskId}`);
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
const landedRow = { task: entry.taskId, engine: activeEngine, check: landed.check };
|
|
2715
|
+
landedRow.verifier_result = landed.verifier_result;
|
|
2716
|
+
if (result.brief_id) landedRow.brief_id = result.brief_id;
|
|
2717
|
+
if (result.restaffed) landedRow.restaffed = result.restaffed;
|
|
2718
|
+
flight.landed.push(landedRow);
|
|
2719
|
+
stampDispatchBrief(root, result.brief_id, 'pass', `landed ${entry.taskId} via engine dispatch`);
|
|
578
2720
|
cli([
|
|
579
2721
|
'task', 'ready', entry.taskId,
|
|
580
|
-
'--proof', `Built by ${
|
|
581
|
-
'--
|
|
2722
|
+
'--proof', `Built by ${activeEngine} engine via atris engine dispatch.${restaffProof} Landed via worktree ship gate (rebase-before-ship, verify re-run). Check re-run: ${landed.check}. Verify output: ${verifyTail || '(command produced no output, exit 0)'}. Receipt saved at ${path.relative(root, receiptPath)}.`,
|
|
2723
|
+
'--result', 'Operators can now review engine-built work faster because dispatch reran the verifier before landing.',
|
|
2724
|
+
'--landing', 'The verified change is on master and ready for review.',
|
|
2725
|
+
'--checked', `${landed.check} passed before landing`,
|
|
2726
|
+
'--tested', 'The requested behavior passed its declared verifier.',
|
|
2727
|
+
'--as', readyActor,
|
|
582
2728
|
]);
|
|
583
2729
|
log(` ✓ landed ${entry.taskId}`);
|
|
584
2730
|
} else {
|
|
585
|
-
flight.paused.push({
|
|
2731
|
+
flight.paused.push({
|
|
2732
|
+
task: entry.taskId,
|
|
2733
|
+
engine: activeEngine,
|
|
2734
|
+
worktree: entry.worktreePath,
|
|
2735
|
+
...(result.restaffed ? { restaffed: result.restaffed } : {}),
|
|
2736
|
+
...landed,
|
|
2737
|
+
});
|
|
2738
|
+
stampDispatchBrief(root, result.brief_id, 'partial', `paused ${entry.taskId} at ${landed.stage || 'landing'}`);
|
|
586
2739
|
log(` ⏸ paused ${entry.taskId} at ${landed.stage}${landed.conflicts ? ` (${landed.conflicts.join(', ')})` : ''} — worktree kept`);
|
|
587
2740
|
}
|
|
588
2741
|
}
|
|
589
2742
|
|
|
590
|
-
|
|
591
|
-
flight.receipt = receiptPath;
|
|
592
|
-
fs.writeFileSync(flight.receipt, `${JSON.stringify(flight, null, 2)}\n`);
|
|
2743
|
+
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
|
593
2744
|
log('');
|
|
594
|
-
|
|
2745
|
+
const completedLabel = reviewOnly ? `${flight.ready.length} proof ready` : `${flight.landed.length} landed`;
|
|
2746
|
+
log(` dispatch over: ${completedLabel}, ${flight.paused.length} paused - receipt: ${path.relative(root, flight.receipt)}`);
|
|
595
2747
|
log('');
|
|
596
2748
|
return flight;
|
|
597
2749
|
}
|