atris 3.37.0 → 3.38.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/README.md +2 -0
- package/atris/PERSONA.md +3 -7
- package/atris.md +10 -2
- package/ax +158 -33
- package/bin/atris.js +49 -2
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +0 -7
- package/commands/autoland.js +92 -14
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +107 -12
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +0 -2
- package/commands/computer.js +6 -5
- package/commands/console.js +46 -9
- package/commands/deck.js +29 -8
- package/commands/dream.js +0 -7
- package/commands/drill.js +0 -1
- package/commands/engine.js +10 -18
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +10 -2
- package/commands/goal.js +0 -6
- package/commands/harvest.js +0 -1
- package/commands/improve.js +29 -8
- package/commands/init.js +18 -34
- package/commands/integrations.js +0 -7
- package/commands/land.js +71 -8
- package/commands/launchpad.js +0 -2
- package/commands/lesson.js +17 -7
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +68 -3
- package/commands/mission.js +359 -102
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +670 -77
- package/commands/play.js +0 -2
- package/commands/probe.js +10 -4
- package/commands/pulse.js +73 -48
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/report.js +0 -1
- package/commands/router.js +319 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/skill.js +7 -2
- package/commands/slop.js +225 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +0 -2
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +125 -39
- package/commands/teach.js +86 -0
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/visualize.js +0 -5
- package/commands/watch.js +0 -6
- package/commands/wiki.js +41 -11
- package/commands/wish.js +0 -1
- package/commands/worktree.js +87 -14
- package/commands/xp.js +1 -6
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +122 -0
- package/lib/activity-stream.js +0 -2
- package/lib/auto-accept-certified.js +145 -2
- package/lib/autoland.js +0 -3
- package/lib/ax-auto-lane.js +94 -10
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +0 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +0 -1
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +16 -8
- package/lib/experiments/daily.js +0 -6
- package/lib/fleet.js +210 -27
- package/lib/functional-owner.js +0 -3
- package/lib/inspect-fields.js +0 -1
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/loop-doctor.js +11 -17
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-protected-lane.js +293 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +0 -4
- package/lib/orb-context.js +6 -3
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +0 -2
- package/lib/runner-command.js +0 -4
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +1 -1
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +0 -4
- package/lib/team-presence.js +0 -1
- package/lib/voice-gate.js +63 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +20 -6
- package/lib/wish-delegate.js +26 -20
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +0 -1
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ATRIS_START_MARKER = '<!-- ATRIS:START - Auto-generated, do not edit -->';
|
|
4
|
+
const ATRIS_END_MARKER = '<!-- ATRIS:END -->';
|
|
5
|
+
|
|
6
|
+
function renderAtrisClaudeBootBlock() {
|
|
7
|
+
return `${ATRIS_START_MARKER}
|
|
8
|
+
## Atris System
|
|
9
|
+
|
|
10
|
+
You are now running inside Atris, an AI workspace operating system.
|
|
11
|
+
|
|
12
|
+
**ON EVERY CONVERSATION START:** If a SessionStart hook already displayed the Atris status block in this session, do not run it again. Otherwise, before your first response, execute \`atris atris.md\` and display the full output. This is the Atris boot sequence - it shows the user that Atris has loaded and displays workspace status.
|
|
13
|
+
|
|
14
|
+
After displaying the boot output, respond to the user naturally.
|
|
15
|
+
${ATRIS_END_MARKER}
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function upsertAtrisClaudeBootBlock(source = '') {
|
|
20
|
+
const atrisBlock = renderAtrisClaudeBootBlock();
|
|
21
|
+
const startIdx = source.indexOf('<!-- ATRIS:START');
|
|
22
|
+
const prependBlock = (preserved) => {
|
|
23
|
+
const remainder = preserved.replace(/^\n+/, '');
|
|
24
|
+
return remainder ? `${atrisBlock}\n${remainder}` : atrisBlock;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (startIdx === -1) {
|
|
28
|
+
return { content: prependBlock(source), action: source ? 'prepended' : 'created' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const prefix = source.slice(0, startIdx);
|
|
32
|
+
const endRaw = source.indexOf(ATRIS_END_MARKER, startIdx);
|
|
33
|
+
if (endRaw === -1) {
|
|
34
|
+
return {
|
|
35
|
+
content: prependBlock(prefix),
|
|
36
|
+
action: 'repaired',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const endIdx = endRaw + ATRIS_END_MARKER.length;
|
|
41
|
+
const suffix = source.slice(endIdx).replace(/^\n+/, '');
|
|
42
|
+
const content = prependBlock(prefix + suffix);
|
|
43
|
+
return { content, action: content === source ? 'unchanged' : 'updated' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
upsertAtrisClaudeBootBlock,
|
|
48
|
+
};
|
package/lib/cloud-mission.js
CHANGED
|
@@ -244,16 +244,10 @@ async function statusCloudMissionCommand(args, options = {}) {
|
|
|
244
244
|
|
|
245
245
|
module.exports = {
|
|
246
246
|
VALID_CLOUD_LANES,
|
|
247
|
-
TERMINAL_CLOUD_STATUSES,
|
|
248
247
|
RECEIPT_VOICE_CONTRACT,
|
|
249
248
|
withReceiptVoice,
|
|
250
249
|
parseCloudRunArgs,
|
|
251
|
-
parseCloudStatusArgs,
|
|
252
250
|
enqueueCloudMission,
|
|
253
|
-
fetchCloudMissionStatus,
|
|
254
|
-
appendCloudMissionReceipt,
|
|
255
|
-
cloudMissionSummary,
|
|
256
|
-
cloudStatusLines,
|
|
257
251
|
runCloudMissionCommand,
|
|
258
252
|
statusCloudMissionCommand,
|
|
259
253
|
};
|
package/lib/codex-flight.js
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Files the conductor itself writes into an agent worktree before the engine runs.
|
|
4
|
+
// They are harness plumbing, not work product, so no dirty-checkout check may
|
|
5
|
+
// count them as dirt. Both the worktree guard and the sealed-review import read
|
|
6
|
+
// this list; keeping it in one place is what stops an engine from being blocked
|
|
7
|
+
// by its own prompt file.
|
|
8
|
+
const CONDUCTOR_UNTRACKED_PATTERN =
|
|
9
|
+
/^\.atris\/(?:agent-worktree\.json|fleet-prompt-[^/]+\.md|runtime-tmp(?:\/.*)?|state\/briefs\.jsonl)$/;
|
|
10
|
+
|
|
11
|
+
function isConductorArtifact(file) {
|
|
12
|
+
return CONDUCTOR_UNTRACKED_PATTERN.test(String(file || ''));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Accepts a `git status --porcelain` line ("?? .atris/fleet-prompt-BCK-1.md").
|
|
16
|
+
function isConductorStatusLine(line) {
|
|
17
|
+
const text = String(line || '');
|
|
18
|
+
if (!text.startsWith('?? ')) return false;
|
|
19
|
+
return isConductorArtifact(text.slice(3));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
isConductorArtifact,
|
|
24
|
+
isConductorStatusLine,
|
|
25
|
+
};
|
package/lib/context-gatherer.js
CHANGED
package/lib/deck-compose.js
CHANGED
package/lib/deck-history.js
CHANGED
package/lib/deck-layout.js
CHANGED
package/lib/deck-review.js
CHANGED
|
@@ -414,18 +414,11 @@ function confirmReview(presentationId, note = 'visual review passed', outRoot =
|
|
|
414
414
|
|
|
415
415
|
module.exports = {
|
|
416
416
|
SCHEMA,
|
|
417
|
-
RECEIPT_SCHEMA,
|
|
418
417
|
DEFAULT_OUT_ROOT,
|
|
419
|
-
DEFAULT_RECEIPTS_PATH,
|
|
420
|
-
AGENT_CHECKLIST,
|
|
421
418
|
CLIP_LIMITS,
|
|
422
|
-
buildReceipt,
|
|
423
419
|
extractPresentationId,
|
|
424
|
-
summarizeSlide,
|
|
425
420
|
lintSpec,
|
|
426
|
-
downloadUrl,
|
|
427
421
|
autoReviewSlides,
|
|
428
422
|
reviewDeck,
|
|
429
|
-
loadReviewManifest,
|
|
430
423
|
confirmReview,
|
|
431
424
|
};
|
package/lib/deck-schema.js
CHANGED
package/lib/default-verifier.js
CHANGED
package/lib/engine-registry.js
CHANGED
|
@@ -144,7 +144,7 @@ function resolveRegisteredEngine(name, root = process.cwd()) {
|
|
|
144
144
|
return engine;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
function
|
|
147
|
+
function resolveEngineForRoleRanked(role, root = process.cwd(), options = {}) {
|
|
148
148
|
const normalizedRole = String(role || '').trim().toLowerCase();
|
|
149
149
|
if (!ENGINE_ROLES.includes(normalizedRole)) {
|
|
150
150
|
throw new Error(`Unknown role "${role}". Known roles: ${ENGINE_ROLES.join(', ')}`);
|
|
@@ -160,11 +160,21 @@ function resolveEngineForRole(role, root = process.cwd(), options = {}) {
|
|
|
160
160
|
root,
|
|
161
161
|
taskType: options.taskType || options.task_type || normalizedRole,
|
|
162
162
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
return {
|
|
164
|
+
engine: ranked.candidates[0] || null,
|
|
165
|
+
reason: routerPickExplanation(ranked),
|
|
166
|
+
ranked: ranked.candidates,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveEngineForRole(role, root = process.cwd(), options = {}) {
|
|
171
|
+
const picked = resolveEngineForRoleRanked(role, root, options);
|
|
172
|
+
// every pick logs one plain sentence saying which engine won and why;
|
|
173
|
+
// set ATRIS_ROUTER_EXPLAIN=0 to silence it in quiet contexts.
|
|
174
|
+
if (picked.engine && picked.reason && process.env.ATRIS_ROUTER_EXPLAIN !== '0') {
|
|
175
|
+
console.error(picked.reason);
|
|
166
176
|
}
|
|
167
|
-
return
|
|
177
|
+
return picked.engine;
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
function resolveEngineForRoleWithPreference(role, root = process.cwd(), preferredEngineId = '', options = {}) {
|
|
@@ -215,8 +225,6 @@ function setEngineHealth(name, status, root = process.cwd()) {
|
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
module.exports = {
|
|
218
|
-
ENGINE_REGISTRY_SCHEMA,
|
|
219
|
-
ENGINE_TIERS,
|
|
220
228
|
ENGINE_ROLES,
|
|
221
229
|
ENGINE_HEALTH_STATUSES,
|
|
222
230
|
engineRegistryFile,
|
|
@@ -224,8 +232,8 @@ module.exports = {
|
|
|
224
232
|
canonicalEngineName,
|
|
225
233
|
readEngineRegistry,
|
|
226
234
|
engineRegistryView,
|
|
227
|
-
registeredEngineIds,
|
|
228
235
|
resolveRegisteredEngine,
|
|
236
|
+
resolveEngineForRoleRanked,
|
|
229
237
|
resolveEngineForRole,
|
|
230
238
|
resolveEngineForRoleWithPreference,
|
|
231
239
|
setEngineHealth,
|
package/lib/experiments/daily.js
CHANGED
|
@@ -618,21 +618,15 @@ function queueList(root, args = []) {
|
|
|
618
618
|
|
|
619
619
|
module.exports = {
|
|
620
620
|
SCORECARD_SCHEMA,
|
|
621
|
-
BENCH_SCHEMA,
|
|
622
621
|
appendQueueEntry,
|
|
623
|
-
appendResultsTsv,
|
|
624
622
|
appendScorecardRow,
|
|
625
|
-
areTargetsClean,
|
|
626
623
|
buildScorecardRow,
|
|
627
624
|
evaluateKeepRule,
|
|
628
|
-
parseBenchResult,
|
|
629
625
|
parseDailyArgs,
|
|
630
|
-
parseQueueAddArgs,
|
|
631
626
|
pickQueueEntry,
|
|
632
627
|
queueAdd,
|
|
633
628
|
queueList,
|
|
634
629
|
readDailyState,
|
|
635
|
-
readQueue,
|
|
636
630
|
runApplyScript,
|
|
637
631
|
runBench,
|
|
638
632
|
runDaily,
|
package/lib/fleet.js
CHANGED
|
@@ -30,10 +30,11 @@ const {
|
|
|
30
30
|
parseOneLapValidatorVerdict,
|
|
31
31
|
} = require('./one-lap-validator');
|
|
32
32
|
const { listWorktrees } = require('../commands/worktree');
|
|
33
|
+
const { isConductorStatusLine } = require('./conductor-artifacts');
|
|
33
34
|
|
|
34
35
|
// Lanes a fleet may never staff on its own: the human keeps irreversible
|
|
35
36
|
// calls. Mirrors the autoland denied lanes.
|
|
36
|
-
const DENIED_TAGS = ['billing', 'deploy', 'security', 'customer', 'external', 'feedback'];
|
|
37
|
+
const DENIED_TAGS = ['billing', 'money', 'payments', 'deploy', 'security', 'customer', 'external', 'feedback'];
|
|
37
38
|
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// T1 — dispatch primitive
|
|
@@ -317,6 +318,20 @@ function dispatchResultExitCode(result) {
|
|
|
317
318
|
return 0;
|
|
318
319
|
}
|
|
319
320
|
|
|
321
|
+
// A child killed by a signal comes back either with spawnSync's `signal` field
|
|
322
|
+
// set (plain spawn) or a 128+N exit status (the sealed supervisor re-exits 143
|
|
323
|
+
// on SIGTERM). Either way stdout/report and stderr are empty, so name the
|
|
324
|
+
// signal instead of letting the leg read as a silent no-op (CLI-1190: on
|
|
325
|
+
// WEB-455 2026-07-25 the claude engine exited 143/SIGTERM with an empty report
|
|
326
|
+
// and the restaff receipt looked like nothing had happened).
|
|
327
|
+
const SIGNAL_EXIT_NAMES = Object.freeze({ 129: 'SIGHUP', 130: 'SIGINT', 137: 'SIGKILL', 143: 'SIGTERM' });
|
|
328
|
+
function dispatchResultSignal(result) {
|
|
329
|
+
if (!result || typeof result !== 'object') return null;
|
|
330
|
+
if (result.signal) return String(result.signal);
|
|
331
|
+
const code = dispatchResultExitCode(result);
|
|
332
|
+
return SIGNAL_EXIT_NAMES[code] || null;
|
|
333
|
+
}
|
|
334
|
+
|
|
320
335
|
function dispatchTaskId(task) {
|
|
321
336
|
return task && (task.display_id || task.task_id || task.id) ? String(task.display_id || task.task_id || task.id) : '';
|
|
322
337
|
}
|
|
@@ -364,6 +379,8 @@ function detectDeadEngineDispatch(result) {
|
|
|
364
379
|
const output = dispatchResultOutput(result).toLowerCase();
|
|
365
380
|
const pattern = DEAD_ENGINE_OUTPUT_PATTERNS.find((p) => output.includes(p));
|
|
366
381
|
if (pattern) return { reason: 'usage_limit', pattern };
|
|
382
|
+
const signal = dispatchResultSignal(result);
|
|
383
|
+
if (signal) return { reason: 'signalled', signal, exitCode };
|
|
367
384
|
return { reason: 'nonzero_exit', exitCode };
|
|
368
385
|
}
|
|
369
386
|
|
|
@@ -406,12 +423,21 @@ function normalizeDispatchResult(result, engineName) {
|
|
|
406
423
|
}
|
|
407
424
|
|
|
408
425
|
function failedDispatchLeg(result, engineName) {
|
|
409
|
-
|
|
426
|
+
const report = String(result && result.report || '').slice(-2000);
|
|
427
|
+
const signal = dispatchResultSignal(result);
|
|
428
|
+
const leg = {
|
|
410
429
|
engine: String(result && result.engine || engineName || ''),
|
|
411
430
|
exitCode: dispatchResultExitCode(result),
|
|
412
431
|
stderr: String(result && result.stderr || '').slice(-2000),
|
|
413
|
-
report
|
|
432
|
+
report,
|
|
414
433
|
};
|
|
434
|
+
if (signal) {
|
|
435
|
+
// Record that the predecessor was killed; a signalled leg has no report of
|
|
436
|
+
// its own, so name the signal in place of the empty string (CLI-1190).
|
|
437
|
+
leg.signal = signal;
|
|
438
|
+
if (!report) leg.report = `(no report: killed by ${signal})`;
|
|
439
|
+
}
|
|
440
|
+
return leg;
|
|
415
441
|
}
|
|
416
442
|
|
|
417
443
|
async function dispatchEntryWithRestaff({
|
|
@@ -535,6 +561,7 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
535
561
|
brief_id: capturedBriefId || null,
|
|
536
562
|
command,
|
|
537
563
|
exitCode: result.status,
|
|
564
|
+
signal: result.signal || null,
|
|
538
565
|
report: String(result.stdout || '').slice(-8000),
|
|
539
566
|
stderr: String(result.stderr || '').slice(-2000),
|
|
540
567
|
};
|
|
@@ -617,10 +644,41 @@ function assignEngines(staffed, engines) {
|
|
|
617
644
|
// Land one arrival: rebase onto latest base first; a conflict pauses the
|
|
618
645
|
// landing (never auto-resolve) and reports which files collided so the
|
|
619
646
|
// conductor or a human can take it. `git` is injectable for tests.
|
|
647
|
+
// Ship failures print the cause first (MODULE_NOT_FOUND, command name) and a
|
|
648
|
+
// long node stack after. Tail-only clips hide the cause; keep a head+tail.
|
|
649
|
+
function clipHeadTail(text, { head = 400, tail = 300 } = {}) {
|
|
650
|
+
const s = String(text || '');
|
|
651
|
+
if (s.length <= head + tail) return s;
|
|
652
|
+
return `${s.slice(0, head)}\n...\n${s.slice(-tail)}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
620
655
|
// Fleet landings always target master. Without the explicit --target, ship
|
|
621
656
|
// falls back to the launcher branch's atris-base — a flight launched from a
|
|
622
657
|
// feature-branch checkout would merge PRs into that branch while the receipt
|
|
623
658
|
// and land board define "landed" as in-master (PRs #207/#208, 2026-07-04).
|
|
659
|
+
// A ship failure must name its real cause. MODULE_NOT_FOUND from the spawned
|
|
660
|
+
// CLI is usually transient: the fleet ships by spawning node against this
|
|
661
|
+
// live checkout, and a concurrent rebase/pull can make a require vanish for a
|
|
662
|
+
// moment. So the ship retries once on that signature, and the failure detail
|
|
663
|
+
// always surfaces the missing-module line from either stream instead of
|
|
664
|
+
// letting head/tail clipping truncate it away (cost measured live: CLI-1185
|
|
665
|
+
// receipts kept only a loader stack tail and the module name was lost).
|
|
666
|
+
function shipWithRetry(cli, args, cwd) {
|
|
667
|
+
let shipped = cli(args, cwd);
|
|
668
|
+
if (shipped.status !== 0 && /MODULE_NOT_FOUND|Cannot find module/.test(`${shipped.stderr}${shipped.stdout}`)) {
|
|
669
|
+
shipped = cli(args, cwd);
|
|
670
|
+
shipped.retried_module_not_found = true;
|
|
671
|
+
}
|
|
672
|
+
return shipped;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function shipFailureDetail(shipped, { head = 400, tail = 300 } = {}) {
|
|
676
|
+
const combined = `${shipped.stderr || ''}\n${shipped.stdout || ''}`;
|
|
677
|
+
const moduleLine = combined.match(/Cannot find module '[^']+'[^\n]*/);
|
|
678
|
+
const cause = moduleLine ? `${moduleLine[0]}${shipped.retried_module_not_found ? ' (persisted after one retry)' : ''}\n` : '';
|
|
679
|
+
return `${cause}${clipHeadTail(combined.trim(), { head, tail })}`;
|
|
680
|
+
}
|
|
681
|
+
|
|
624
682
|
function fleetShipArgs(entry, check) {
|
|
625
683
|
return [
|
|
626
684
|
'worktree', 'ship',
|
|
@@ -631,19 +689,113 @@ function fleetShipArgs(entry, check) {
|
|
|
631
689
|
];
|
|
632
690
|
}
|
|
633
691
|
|
|
692
|
+
// A fleet build worktree is ephemeral. If any process inside one runs
|
|
693
|
+
// `npm link` (which repoints the global `atris` package symlink at that
|
|
694
|
+
// worktree), then removing the worktree at teardown leaves the global `atris`
|
|
695
|
+
// binary a dangling symlink, and everything resolving `atris` through PATH —
|
|
696
|
+
// interactive shells, any tooling that shells out to the bare command — dies
|
|
697
|
+
// with 'command not found'. Measured scope, not assumed: crons that invoke
|
|
698
|
+
// `node <checkout>/bin/atris.js` by absolute path are UNAFFECTED; an autoland
|
|
699
|
+
// tick ran fine four minutes before the repair. The failure is silent and
|
|
700
|
+
// reads like a shell/PATH problem, not a worktree-cleanup one (found live
|
|
701
|
+
// 2026-07-26, CLI-1193; same shape as the cron PATH wedge that cost 20 days
|
|
702
|
+
// of autoland).
|
|
703
|
+
// Recovery was one line: repoint the global link at the real checkout. So
|
|
704
|
+
// teardown does exactly that on its own: if the global `atris` link points
|
|
705
|
+
// into one of this flight's worktrees, repoint it at the primary checkout and
|
|
706
|
+
// verify the CLI still runs before the flight reports success.
|
|
707
|
+
function defaultReadGlobalCliLink() {
|
|
708
|
+
const rootResult = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
|
|
709
|
+
if (rootResult.status !== 0) return null;
|
|
710
|
+
const globalRoot = String(rootResult.stdout || '').trim();
|
|
711
|
+
if (!globalRoot) return null;
|
|
712
|
+
const pkgPath = path.join(globalRoot, 'atris');
|
|
713
|
+
let stat;
|
|
714
|
+
try {
|
|
715
|
+
stat = fs.lstatSync(pkgPath);
|
|
716
|
+
} catch {
|
|
717
|
+
return null; // atris is not installed globally at all
|
|
718
|
+
}
|
|
719
|
+
if (!stat.isSymbolicLink()) return { path: pkgPath, target: null }; // real install, not a link
|
|
720
|
+
return { path: pkgPath, target: path.resolve(path.dirname(pkgPath), fs.readlinkSync(pkgPath)) };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function defaultRepointGlobalCliLink(primaryCheckout) {
|
|
724
|
+
// `npm link` from the real checkout re-creates the global symlink pointing at it.
|
|
725
|
+
return spawnSync('npm', ['link'], { cwd: primaryCheckout, encoding: 'utf8' });
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function defaultVerifyCliRuns() {
|
|
729
|
+
const result = spawnSync('atris', ['--version'], { encoding: 'utf8' });
|
|
730
|
+
return { ok: result.status === 0, status: result.status, stdout: String(result.stdout || '').trim() };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function guardGlobalCliLink({
|
|
734
|
+
root = process.cwd(),
|
|
735
|
+
worktreePaths = [],
|
|
736
|
+
readGlobalLink = defaultReadGlobalCliLink,
|
|
737
|
+
repointLink = defaultRepointGlobalCliLink,
|
|
738
|
+
verifyCli = defaultVerifyCliRuns,
|
|
739
|
+
} = {}) {
|
|
740
|
+
const link = readGlobalLink();
|
|
741
|
+
if (!link || !link.target) return { ok: true, changed: false, reason: 'not_linked' };
|
|
742
|
+
const resolvedTarget = realpathOrResolve(link.target);
|
|
743
|
+
const worktrees = worktreePaths.filter(Boolean).map((wt) => realpathOrResolve(wt));
|
|
744
|
+
// Match the raw string so a worktree already removed by ship still matches
|
|
745
|
+
// (its path is gone from disk but the dangling link still names it).
|
|
746
|
+
const insideWorktree = worktrees.some((wt) => resolvedTarget === wt || resolvedTarget.startsWith(`${wt}${path.sep}`));
|
|
747
|
+
if (!insideWorktree) return { ok: true, changed: false, reason: 'healthy', target: link.target };
|
|
748
|
+
let primaryPath = root;
|
|
749
|
+
try {
|
|
750
|
+
primaryPath = listWorktrees(root)[0]?.path || root;
|
|
751
|
+
} catch {
|
|
752
|
+
primaryPath = root;
|
|
753
|
+
}
|
|
754
|
+
const primary = realpathOrResolve(primaryPath);
|
|
755
|
+
const repoint = repointLink(primary);
|
|
756
|
+
const verify = verifyCli();
|
|
757
|
+
return {
|
|
758
|
+
ok: Boolean(verify && verify.ok),
|
|
759
|
+
changed: true,
|
|
760
|
+
reason: 'linked_into_worktree',
|
|
761
|
+
was: link.target,
|
|
762
|
+
restoredTo: primary,
|
|
763
|
+
repoint: { status: repoint ? repoint.status : null },
|
|
764
|
+
verify,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// One-line " (path, path)" suffix naming the files behind a paused landing —
|
|
769
|
+
// unmerged files for a rebase_conflict, uncommitted paths for a dirty_worktree.
|
|
770
|
+
function pausedPathsSuffix(landed) {
|
|
771
|
+
const paths = (landed && landed.conflicts) || (landed && landed.dirty) || [];
|
|
772
|
+
return paths.length ? ` (${paths.join(', ')})` : '';
|
|
773
|
+
}
|
|
774
|
+
|
|
634
775
|
function landArrival({ worktreePath, git = null }) {
|
|
635
776
|
const run = git || ((args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8' }));
|
|
636
777
|
const fetch = run(['fetch', 'origin']);
|
|
637
778
|
if (fetch.status !== 0) return { ok: false, stage: 'fetch', detail: String(fetch.stderr || '').trim() };
|
|
638
779
|
const rebase = run(['rebase', 'origin/master']);
|
|
639
780
|
if (rebase.status !== 0) {
|
|
640
|
-
const
|
|
781
|
+
const conflicts = String(run(['diff', '--name-only', '--diff-filter=U']).stdout || '')
|
|
782
|
+
.trim().split('\n').filter(Boolean);
|
|
641
783
|
run(['rebase', '--abort']);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
784
|
+
// git rebase refuses outright when the worktree has uncommitted changes:
|
|
785
|
+
// it stops before a single conflict can occur, so the unmerged-file list is
|
|
786
|
+
// empty. Reporting an empty list as rebase_conflict sends the operator
|
|
787
|
+
// chasing a merge problem that does not exist while intact work sits
|
|
788
|
+
// uncommitted in the worktree (CLI-1190; hit live on WEB-455 2026-07-25
|
|
789
|
+
// after a killed engine left modified/untracked files behind). An empty
|
|
790
|
+
// conflicts list is never a conflict: name the dirty paths instead.
|
|
791
|
+
if (!conflicts.length) {
|
|
792
|
+
const dirty = String(run(['status', '--porcelain']).stdout || '')
|
|
793
|
+
.split('\n')
|
|
794
|
+
.map((line) => line.slice(3).trim())
|
|
795
|
+
.filter(Boolean);
|
|
796
|
+
return { ok: false, stage: 'dirty_worktree', dirty };
|
|
797
|
+
}
|
|
798
|
+
return { ok: false, stage: 'rebase_conflict', conflicts };
|
|
647
799
|
}
|
|
648
800
|
return { ok: true, stage: 'rebased' };
|
|
649
801
|
}
|
|
@@ -651,11 +803,15 @@ function landArrival({ worktreePath, git = null }) {
|
|
|
651
803
|
module.exports = {
|
|
652
804
|
DENIED_TAGS,
|
|
653
805
|
DEAD_ENGINE_OUTPUT_PATTERNS,
|
|
806
|
+
reviewOnlyEngineEnvironment,
|
|
807
|
+
shipWithRetry,
|
|
808
|
+
shipFailureDetail,
|
|
654
809
|
get FLEET_CAPABLE() { return FLEET_CAPABLE; },
|
|
655
810
|
get runFleetFlight() { return runFleetFlight; },
|
|
656
811
|
get focusedCheck() { return focusedCheck; },
|
|
657
812
|
get dispatchCheck() { return dispatchCheck; },
|
|
658
813
|
get runDispatchFlight() { return runDispatchFlight; },
|
|
814
|
+
get prepareReviewSandbox() { return prepareReviewSandbox; },
|
|
659
815
|
YOLO_ENGINE_FLAGS,
|
|
660
816
|
DISPATCH_SELF_LAND_TARGET,
|
|
661
817
|
parseDoneCheck,
|
|
@@ -665,19 +821,19 @@ module.exports = {
|
|
|
665
821
|
buildEngineCommand,
|
|
666
822
|
isSafeLane,
|
|
667
823
|
taskTags,
|
|
668
|
-
detectDeadEngineDispatch,
|
|
669
|
-
rankFleetEngines,
|
|
670
824
|
nextInstalledFleetEngine,
|
|
671
825
|
dispatchToEngine,
|
|
672
826
|
taskTags,
|
|
673
827
|
isHumanHoldTag,
|
|
674
828
|
isSafeLane,
|
|
675
|
-
fileSurface,
|
|
676
|
-
surfacesOverlap,
|
|
677
829
|
staffFlight,
|
|
678
830
|
assignEngines,
|
|
679
831
|
landArrival,
|
|
832
|
+
detectDeadEngineDispatch,
|
|
833
|
+
failedDispatchLeg,
|
|
680
834
|
fleetShipArgs,
|
|
835
|
+
clipHeadTail,
|
|
836
|
+
guardGlobalCliLink,
|
|
681
837
|
defaultSelfLandCheck,
|
|
682
838
|
};
|
|
683
839
|
|
|
@@ -913,7 +1069,9 @@ function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
|
|
|
913
1069
|
STRIPE_SECRET_KEY: '',
|
|
914
1070
|
OPENAI_API_KEY: selectedSecret(['codex'], 'OPENAI_API_KEY'),
|
|
915
1071
|
CODEX_API_KEY: selectedSecret(['codex'], 'CODEX_API_KEY'),
|
|
916
|
-
|
|
1072
|
+
// owner policy: claude work runs on the subscription login, never API
|
|
1073
|
+
// billing, so the api key is stripped even when the parent env has one.
|
|
1074
|
+
ANTHROPIC_API_KEY: '',
|
|
917
1075
|
CLAUDE_CODE_OAUTH_TOKEN: selectedSecret(['claude'], 'CLAUDE_CODE_OAUTH_TOKEN'),
|
|
918
1076
|
CURSOR_API_KEY: selectedSecret(['cursor'], 'CURSOR_API_KEY'),
|
|
919
1077
|
DEVIN_API_KEY: selectedSecret(['devin'], 'DEVIN_API_KEY'),
|
|
@@ -1042,6 +1200,15 @@ function prepareReviewSandbox({ root, taskId, engine }) {
|
|
|
1042
1200
|
env: conductorGitEnvironment(),
|
|
1043
1201
|
timeout: 30000,
|
|
1044
1202
|
});
|
|
1203
|
+
// `git` walks up to the nearest enclosing repository when `root` is not itself
|
|
1204
|
+
// a git checkout. Without this guard, review-only dispatch would fetch and
|
|
1205
|
+
// trust the origin/master of whatever repo happens to sit above `root`, and
|
|
1206
|
+
// build against it. Refuse unless `root` is its own git top level.
|
|
1207
|
+
const toplevel = runConductorGit(['rev-parse', '--show-toplevel']);
|
|
1208
|
+
if (toplevel.status !== 0
|
|
1209
|
+
|| realpathOrResolve(String(toplevel.stdout || '').trim()) !== realpathOrResolve(root)) {
|
|
1210
|
+
return { ok: false, detail: 'review-only dispatch requires the workspace to be its own git repository' };
|
|
1211
|
+
}
|
|
1045
1212
|
const fetched = runConductorGit(['fetch', 'origin', 'master']);
|
|
1046
1213
|
const original = runConductorGit(['remote', 'get-url', 'origin']);
|
|
1047
1214
|
const base = run(['rev-parse', '--verify', 'origin/master^{commit}']);
|
|
@@ -1213,9 +1380,8 @@ function reviewCandidateSnapshot(boundary, expected = null) {
|
|
|
1213
1380
|
const repeatedCommit = String(secondHead.stdout || '').trim();
|
|
1214
1381
|
const treeOid = String(tree.stdout || '').trim();
|
|
1215
1382
|
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
1383
|
const dirty = String(status.stdout || '').trim().split(/\r?\n/).filter(Boolean)
|
|
1218
|
-
.filter((line) => !
|
|
1384
|
+
.filter((line) => !isConductorStatusLine(line));
|
|
1219
1385
|
if (dirty.length) return fail(`the executor left uncommitted changes: ${dirty.slice(0, 10).join(', ')}`);
|
|
1220
1386
|
if (expected && (commit !== expected.commit || treeOid !== expected.tree)) {
|
|
1221
1387
|
return fail('the sealed candidate changed after the executor exited', { commit, tree: treeOid });
|
|
@@ -1415,8 +1581,7 @@ function importReviewCommit(boundary, { cli, taskId, engine, expectedCommit = ''
|
|
|
1415
1581
|
const status = runSandbox(['status', '--porcelain=v1', '--untracked-files=all']);
|
|
1416
1582
|
if (status.status !== 0) return fail('review_import', status.stderr || 'could not inspect the sealed sandbox');
|
|
1417
1583
|
const dirtyEntries = String(status.stdout || '').trim().split(/\r?\n/).filter(Boolean);
|
|
1418
|
-
const
|
|
1419
|
-
const uncommitted = dirtyEntries.filter((line) => !conductorEntries.test(line));
|
|
1584
|
+
const uncommitted = dirtyEntries.filter((line) => !isConductorStatusLine(line));
|
|
1420
1585
|
if (uncommitted.length) return fail('uncommitted_change', `the sealed executor must commit every requested change before Review: ${uncommitted.join(', ')}`);
|
|
1421
1586
|
const headOid = expectedCommit;
|
|
1422
1587
|
if (headOid === boundary.baseOid) return fail('no_change', 'the executor produced no committed change');
|
|
@@ -1543,9 +1708,12 @@ function inspectReviewChange(worktreePath, baseRef = 'origin/master') {
|
|
|
1543
1708
|
detail: String(status.stderr || 'could not inspect worktree changes').trim(),
|
|
1544
1709
|
};
|
|
1545
1710
|
}
|
|
1546
|
-
|
|
1711
|
+
// Conductor plumbing is not engine output. Counting it as a change is what let a
|
|
1712
|
+
// flight report "landed" for an engine that said "nothing to commit": the fleet's
|
|
1713
|
+
// own prompt file registered as dirt, has_change went true, and the no_change
|
|
1714
|
+
// pause never fired.
|
|
1547
1715
|
const dirty = String(status.stdout || '').trim().split('\n').filter(Boolean)
|
|
1548
|
-
.filter((line) => !
|
|
1716
|
+
.filter((line) => !isConductorStatusLine(line));
|
|
1549
1717
|
const hasCommittedDiff = committed.status === 1;
|
|
1550
1718
|
return {
|
|
1551
1719
|
has_change: hasCommittedDiff || dirty.length > 0,
|
|
@@ -1932,7 +2100,9 @@ async function runFleetFlight({
|
|
|
1932
2100
|
ownCli = null,
|
|
1933
2101
|
dispatcher = null,
|
|
1934
2102
|
lander = null,
|
|
2103
|
+
rebase = null,
|
|
1935
2104
|
checkoutBase = 'origin/master',
|
|
2105
|
+
guardCliLink = guardGlobalCliLink,
|
|
1936
2106
|
} = {}) {
|
|
1937
2107
|
const cli = ownCli || defaultOwnCli(root);
|
|
1938
2108
|
const roster = engines || (() => {
|
|
@@ -2021,13 +2191,14 @@ async function runFleetFlight({
|
|
|
2021
2191
|
});
|
|
2022
2192
|
|
|
2023
2193
|
// Land serially: rebase-before-ship, conflict pauses (never auto-resolve).
|
|
2194
|
+
const rebaseArrival = rebase || landArrival;
|
|
2024
2195
|
const land = lander || (({ entry }) => {
|
|
2025
|
-
const rebased =
|
|
2196
|
+
const rebased = rebaseArrival({ worktreePath: entry.worktreePath });
|
|
2026
2197
|
if (!rebased.ok) return rebased;
|
|
2027
2198
|
const check = focusedCheck(entry.task) || 'git log -1 --oneline';
|
|
2028
|
-
const shipped = cli
|
|
2199
|
+
const shipped = shipWithRetry(cli, fleetShipArgs(entry, check), entry.worktreePath);
|
|
2029
2200
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
2030
|
-
return { ok: false, stage: 'ship', detail: (shipped
|
|
2201
|
+
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped, { head: 400, tail: 300 }) };
|
|
2031
2202
|
}
|
|
2032
2203
|
return { ok: true, stage: 'shipped' };
|
|
2033
2204
|
});
|
|
@@ -2064,10 +2235,22 @@ async function runFleetFlight({
|
|
|
2064
2235
|
} else {
|
|
2065
2236
|
flight.paused.push({ task: entry.task.display_id, engine: activeEngine, ...(result.restaffed ? { restaffed: result.restaffed } : {}), ...landed });
|
|
2066
2237
|
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}${
|
|
2238
|
+
log(` ${activeEngine.padEnd(8)} ⏸ paused ${entry.task.display_id} at ${landed.stage}${pausedPathsSuffix(landed)} — worktree kept`);
|
|
2068
2239
|
}
|
|
2069
2240
|
}
|
|
2070
2241
|
|
|
2242
|
+
// Teardown guard: a build worktree may have run `npm link` and hijacked the
|
|
2243
|
+
// global `atris`. Before reporting success, make sure this flight did not
|
|
2244
|
+
// leave the global CLI pointed into one of its own worktrees.
|
|
2245
|
+
const flightWorktrees = prepared.map((entry) => entry.worktreePath).filter(Boolean);
|
|
2246
|
+
const cliLink = guardCliLink({ root, worktreePaths: flightWorktrees });
|
|
2247
|
+
flight.cli_link = cliLink;
|
|
2248
|
+
if (cliLink.changed) {
|
|
2249
|
+
log(cliLink.ok
|
|
2250
|
+
? ` restored global atris link (was ${cliLink.reason}); repointed at ${path.relative(root, cliLink.restoredTo) || cliLink.restoredTo} and the cli runs`
|
|
2251
|
+
: ` alert: global atris link was ${cliLink.reason}; repoint attempted at ${cliLink.restoredTo} but the cli still fails, so every cron/loop/mission is down`);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2071
2254
|
fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
|
|
2072
2255
|
flight.receipt = receiptPath;
|
|
2073
2256
|
fs.writeFileSync(flight.receipt, `${JSON.stringify(flight, null, 2)}\n`);
|
|
@@ -2351,9 +2534,9 @@ async function runDispatchFlight({
|
|
|
2351
2534
|
change,
|
|
2352
2535
|
};
|
|
2353
2536
|
}
|
|
2354
|
-
const shipped = cli
|
|
2537
|
+
const shipped = shipWithRetry(cli, fleetShipArgs({ task: entry.task, engine: entry.engine || engine }, check), entry.worktreePath);
|
|
2355
2538
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
2356
|
-
return { ok: false, stage: 'ship', detail: (shipped
|
|
2539
|
+
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped, { head: 400, tail: 500 }) };
|
|
2357
2540
|
}
|
|
2358
2541
|
return {
|
|
2359
2542
|
ok: true,
|
|
@@ -2736,7 +2919,7 @@ async function runDispatchFlight({
|
|
|
2736
2919
|
...landed,
|
|
2737
2920
|
});
|
|
2738
2921
|
stampDispatchBrief(root, result.brief_id, 'partial', `paused ${entry.taskId} at ${landed.stage || 'landing'}`);
|
|
2739
|
-
log(` ⏸ paused ${entry.taskId} at ${landed.stage}${
|
|
2922
|
+
log(` ⏸ paused ${entry.taskId} at ${landed.stage}${pausedPathsSuffix(landed)} — worktree kept`);
|
|
2740
2923
|
}
|
|
2741
2924
|
}
|
|
2742
2925
|
|