atris 3.57.3 → 3.58.5

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.
Files changed (175) hide show
  1. package/README.md +12 -1
  2. package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
  3. package/atris/skills/create-member/SKILL.md +16 -13
  4. package/atris/skills/design/SKILL.md +27 -4
  5. package/atris/skills/engines/SKILL.md +57 -13
  6. package/atris/skills/x-search/SKILL.md +13 -5
  7. package/atris/skills/youtube/SKILL.md +39 -8
  8. package/atris/team/brainstormer/MEMBER.md +2 -2
  9. package/atris/team/executor/MEMBER.md +3 -3
  10. package/atris/team/launcher/MEMBER.md +4 -4
  11. package/atris/team/navigator/MEMBER.md +3 -3
  12. package/atris/team/researcher/MEMBER.md +2 -2
  13. package/atris/team/validator/MEMBER.md +1 -1
  14. package/atris.md +14 -0
  15. package/ax +157 -24
  16. package/bin/atris.js +79 -31
  17. package/commands/aeo.js +4 -4
  18. package/commands/agents.js +1 -1
  19. package/commands/align.js +12 -12
  20. package/commands/analytics.js +3 -3
  21. package/commands/app.js +5 -5
  22. package/commands/ask.js +175 -0
  23. package/commands/auth.js +7 -7
  24. package/commands/autoland.js +29 -5
  25. package/commands/autopilot-front.js +6 -6
  26. package/commands/autopilot.js +90 -90
  27. package/commands/avail.js +5 -5
  28. package/commands/bench.js +63 -1
  29. package/commands/browse.js +2 -2
  30. package/commands/business.js +12 -12
  31. package/commands/card.js +2 -2
  32. package/commands/clean.js +13 -13
  33. package/commands/close.js +297 -15
  34. package/commands/compile.js +13 -13
  35. package/commands/computer.js +94 -27
  36. package/commands/console.js +5 -5
  37. package/commands/decide.js +1 -1
  38. package/commands/deck.js +9 -9
  39. package/commands/drive.js +4 -4
  40. package/commands/engine.js +173 -19
  41. package/commands/errors.js +3 -3
  42. package/commands/experiments.js +18 -1
  43. package/commands/feed.js +2 -2
  44. package/commands/feedback.js +4 -4
  45. package/commands/fleet.js +12 -12
  46. package/commands/game.js +1 -1
  47. package/commands/guide.js +49 -0
  48. package/commands/improve.js +19 -17
  49. package/commands/init.js +67 -19
  50. package/commands/interview.js +7 -7
  51. package/commands/land.js +40 -15
  52. package/commands/learn.js +12 -12
  53. package/commands/lesson.js +2 -2
  54. package/commands/log.js +1 -1
  55. package/commands/loops.js +4 -4
  56. package/commands/member.js +75 -50
  57. package/commands/mission.js +97 -98
  58. package/commands/one-lap.js +1 -1
  59. package/commands/pack.js +74 -8
  60. package/commands/plugin.js +6 -6
  61. package/commands/probe.js +9 -9
  62. package/commands/pull.js +35 -35
  63. package/commands/pulse.js +1 -1
  64. package/commands/push.js +21 -21
  65. package/commands/rainmaker.js +1 -1
  66. package/commands/recap.js +7 -7
  67. package/commands/reel.js +2 -2
  68. package/commands/release.js +3 -3
  69. package/commands/report.js +1 -1
  70. package/commands/review.js +5 -5
  71. package/commands/revisions.js +1 -1
  72. package/commands/run.js +1 -1
  73. package/commands/scout.js +3 -3
  74. package/commands/security-review.js +7 -7
  75. package/commands/serve.js +9 -9
  76. package/commands/setup.js +1 -1
  77. package/commands/sign.js +4 -4
  78. package/commands/signup.js +4 -4
  79. package/commands/site.js +8 -3
  80. package/commands/skill.js +6 -6
  81. package/commands/slop.js +18 -18
  82. package/commands/social.js +3 -3
  83. package/commands/soul.js +14 -14
  84. package/commands/spaceship.js +1 -1
  85. package/commands/status.js +2 -2
  86. package/commands/strings.js +13 -13
  87. package/commands/study.js +4 -4
  88. package/commands/sync.js +8 -8
  89. package/commands/task.js +50 -25
  90. package/commands/teach.js +2 -2
  91. package/commands/team.js +1 -1
  92. package/commands/theme.js +2 -2
  93. package/commands/tree.js +34 -0
  94. package/commands/truth.js +7 -7
  95. package/commands/verify.js +12 -12
  96. package/commands/visualize.js +2 -2
  97. package/commands/workflow.js +97 -40
  98. package/commands/worktree.js +57 -1
  99. package/commands/write.js +9 -9
  100. package/commands/x-search.js +304 -15
  101. package/commands/xp.js +6 -3
  102. package/commands/youtube.js +916 -65
  103. package/lib/accept-verify-gate.js +3 -3
  104. package/lib/activity-stream.js +1 -1
  105. package/lib/apply-gate.js +13 -0
  106. package/lib/auto-accept-certified.js +28 -12
  107. package/lib/autoland.js +6 -6
  108. package/lib/bench/engines.js +59 -8
  109. package/lib/bench/report.js +1 -1
  110. package/lib/bench/runner.js +204 -5
  111. package/lib/bench/tree-render.js +76 -0
  112. package/lib/card.js +1 -1
  113. package/lib/chat-log-scan.js +1 -1
  114. package/lib/claude-boot-block.js +2 -0
  115. package/lib/deck-compose.js +1 -1
  116. package/lib/deck-history.js +2 -2
  117. package/lib/deck-layout.js +1 -1
  118. package/lib/deck-review.js +5 -5
  119. package/lib/deck-schema.js +1 -1
  120. package/lib/default-verifier.js +1 -1
  121. package/lib/developer-api.js +3 -0
  122. package/lib/engine-ask.js +66 -9
  123. package/lib/engine-registry.js +15 -2
  124. package/lib/file-ops.js +1 -1
  125. package/lib/fleet.js +71 -35
  126. package/lib/html-render.js +1 -1
  127. package/lib/intents.js +172 -0
  128. package/lib/journal.js +1 -1
  129. package/lib/known-commands.js +2 -2
  130. package/lib/land-green.js +364 -0
  131. package/lib/lesson-contradiction.js +2 -2
  132. package/lib/lesson-preflight.js +1 -1
  133. package/lib/manifest.js +5 -5
  134. package/lib/member-alive.js +9 -5
  135. package/lib/memory-view.js +3 -3
  136. package/lib/mission-protected-lane.js +1 -1
  137. package/lib/mission-root.js +2 -2
  138. package/lib/orb-context.js +10 -10
  139. package/lib/pack-capabilities.js +861 -3
  140. package/lib/permission-grants.js +2 -2
  141. package/lib/policy-lessons.js +6 -6
  142. package/lib/pulse.js +8 -8
  143. package/lib/receipt-evidence.js +1 -1
  144. package/lib/reel.js +1 -1
  145. package/lib/review-integrity.js +1 -1
  146. package/lib/runner-command.js +35 -5
  147. package/lib/scorecard.js +4 -4
  148. package/lib/security-scan.js +5 -5
  149. package/lib/self-drive.js +1 -1
  150. package/lib/site-publish.js +372 -0
  151. package/lib/site.js +1 -1
  152. package/lib/slides-deck.js +2 -2
  153. package/lib/state-detection.js +2 -2
  154. package/lib/sync-telemetry.js +2 -2
  155. package/lib/task-db.js +17 -10
  156. package/lib/task-explanation.js +1 -1
  157. package/lib/task-proof.js +2 -2
  158. package/lib/task-receipt.js +3 -3
  159. package/lib/theme.js +12 -12
  160. package/lib/todo-fallback.js +3 -3
  161. package/lib/todo-sections.js +1 -1
  162. package/lib/tree-hash.js +79 -0
  163. package/lib/usage.js +1 -1
  164. package/lib/wish-delegate.js +1 -1
  165. package/lib/workspace-safety.js +4 -4
  166. package/lib/workspace-scaffold.js +3 -3
  167. package/package.json +1 -1
  168. package/scripts/det/concierge-bakeoff.sh +26 -0
  169. package/scripts/det/ytquote-repair.js +0 -1
  170. package/scripts/det/ytrail-race.js +0 -5
  171. package/scripts/member-operate.mjs +52 -14
  172. package/scripts/outbound-artifact-gate.js +0 -3
  173. package/utils/auth.js +0 -3
  174. package/utils/config.js +0 -1
  175. package/scripts/det/checklist-score.js +0 -191
package/lib/engine-ask.js CHANGED
@@ -24,13 +24,13 @@ const DEFAULT_ASK_CONCURRENCY = 3;
24
24
  const DEFAULT_ASK_TIMEOUT_MS = 120000;
25
25
  const MAX_ASK_CONCURRENCY = 4;
26
26
  const MAX_ASK_JOBS = 8;
27
- const MAX_ASK_TIMEOUT_MS = 10 * 60 * 1000;
28
- const DEFAULT_FABLE_ASK_TIMEOUT_MS = MAX_ASK_TIMEOUT_MS;
27
+ const DEFAULT_FABLE_ASK_TIMEOUT_MS = 10 * 60 * 1000;
28
+ const MAX_ASK_TIMEOUT_MS = 60 * 60 * 1000;
29
29
  const MAX_ASK_PROMPT_BYTES = 16 * 1024;
30
30
  const MAX_ASK_TOTAL_PROMPT_BYTES = 64 * 1024;
31
31
  const MAX_ASK_OUTPUT_BYTES = 1024 * 1024;
32
32
  const ASK_STOP_GRACE_MS = 250;
33
- const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok', 'agy']);
33
+ const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok', 'agy', 'opencode', 'commandcode']);
34
34
  const READ_ONLY_PREAMBLE = [
35
35
  'This is a read-only request.',
36
36
  'Do not modify files, create worktrees, start background agents, or run commands with side effects.',
@@ -49,6 +49,9 @@ function askUsage() {
49
49
  ` --timeout <sec> per-engine timeout, 1-${MAX_ASK_TIMEOUT_MS / 1000} (default ${DEFAULT_ASK_TIMEOUT_MS / 1000}; Fable ${DEFAULT_FABLE_ASK_TIMEOUT_MS / 1000})`,
50
50
  ' --json print the receipt as json',
51
51
  '',
52
+ 'long jobs: set --timeout explicitly; agy receives the same print deadline.',
53
+ 'watch a running receipt from another shell: atris engine watch latest',
54
+ '',
52
55
  `jobs files contain up to ${MAX_ASK_JOBS} objects: [{"engine":"codex","model":"optional","prompt":"question","label":"optional"}]`,
53
56
  ].join('\n');
54
57
  }
@@ -202,7 +205,9 @@ function resolveAskModel(engine, modelName = '') {
202
205
  return RUNNER_PROFILE_DEFS[engine]?.model || DEFAULT_CLAUDE_RUNNER_MODEL;
203
206
  }
204
207
 
205
- function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
208
+ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '', {
209
+ timeoutMs = DEFAULT_ASK_TIMEOUT_MS,
210
+ } = {}) {
206
211
  const engine = canonicalEngineName(engineName);
207
212
  const profile = RUNNER_PROFILE_DEFS[engine];
208
213
  if (!profile) throw new Error(`unknown engine "${engineName}"`);
@@ -222,7 +227,6 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
222
227
  '--model', model || profile.model || DEFAULT_CLAUDE_RUNNER_MODEL,
223
228
  '--tools', 'Read,Glob,Grep,WebSearch,WebFetch',
224
229
  '--permission-mode', 'plan',
225
- '--safe-mode',
226
230
  '--no-session-persistence',
227
231
  ],
228
232
  };
@@ -259,9 +263,28 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
259
263
  return {
260
264
  engine,
261
265
  bin: profile.bin,
262
- args: ['--mode', 'plan', '--sandbox', '--dangerously-skip-permissions', ...(model ? ['--model', model] : []), '-p', request],
266
+ args: [
267
+ '--mode', 'plan',
268
+ '--sandbox',
269
+ '--dangerously-skip-permissions',
270
+ '--print-timeout', `${Math.max(1, Math.ceil(timeoutMs / 1000))}s`,
271
+ ...(model ? ['--model', model] : []),
272
+ '-p', request,
273
+ ],
263
274
  };
264
275
  }
276
+ if (engine === 'commandcode') {
277
+ // Headless print mode blocks file edits and shell commands by default,
278
+ // so plain -p is already read-only; no sandbox flags needed. Default is
279
+ // no --model: the run rides this machine's configured Command Code
280
+ // default, with an explicit ask --model still honored.
281
+ return { engine, bin: profile.bin, args: ['-p', request, ...(model ? ['--model', model] : [])] };
282
+ }
283
+ if (engine === 'opencode') {
284
+ // `run` with a message is headless print mode; the built-in plan agent
285
+ // keeps the ask read-only (verified live 2026-08-21, ~7s per lookup).
286
+ return { engine, bin: profile.bin, args: ['--agent', 'plan', ...(model ? ['-m', model] : []), request] };
287
+ }
265
288
  throw new Error(`engine ask has no read-only command for ${engine}`);
266
289
  }
267
290
 
@@ -293,6 +316,33 @@ function cancelledAskResult(job, extra = {}) {
293
316
  };
294
317
  }
295
318
 
319
+ function normalizeAskResult(engine, result = {}) {
320
+ const output = `${String(result.stdout || '')}\n${String(result.stderr || '')}`;
321
+ if (engine === 'agy' && !result.cancelled && /timeout waiting for response/i.test(output)) {
322
+ return { ...result, ok: false, reason: 'timeout', timed_out: true };
323
+ }
324
+ return result;
325
+ }
326
+
327
+ function osAccountName() {
328
+ try {
329
+ return String(os.userInfo().username || '').trim();
330
+ } catch {
331
+ return '';
332
+ }
333
+ }
334
+
335
+ // Claude Code looks up macOS Keychain with process.env.USER, not whoami.
336
+ // Host apps sometimes pass a display name, so pin USER and LOGNAME to the OS account.
337
+ function buildAskSpawnEnv(sourceEnv = process.env) {
338
+ const env = { ...sourceEnv };
339
+ const username = osAccountName();
340
+ if (!username) return env;
341
+ env.USER = username;
342
+ env.LOGNAME = username;
343
+ return env;
344
+ }
345
+
296
346
  function runAskProcess(invocation, {
297
347
  cwd = process.cwd(),
298
348
  timeoutMs = DEFAULT_ASK_TIMEOUT_MS,
@@ -387,7 +437,7 @@ function runAskProcess(invocation, {
387
437
  try {
388
438
  child = spawnProcess(invocation.bin, invocation.args, {
389
439
  cwd,
390
- env: process.env,
440
+ env: buildAskSpawnEnv(),
391
441
  detached: process.platform !== 'win32',
392
442
  stdio: ['ignore', 'pipe', 'pipe'],
393
443
  });
@@ -430,7 +480,7 @@ async function runEngineAskJobs(jobs, {
430
480
  const answers = new Array(jobs.length);
431
481
  let nextIndex = 0;
432
482
  const execute = executeAskJob || (async (job) => {
433
- const invocation = buildReadOnlyEngineInvocation(job.engine, job.prompt, job.model);
483
+ const invocation = buildReadOnlyEngineInvocation(job.engine, job.prompt, job.model, { timeoutMs });
434
484
  return runAskProcess(invocation, { cwd: invocation.cwd || root, timeoutMs, signal, onOutputChunk });
435
485
  });
436
486
  const workerCount = Math.min(concurrency, jobs.length);
@@ -447,7 +497,8 @@ async function runEngineAskJobs(jobs, {
447
497
  }
448
498
  try {
449
499
  assertAskModelSupported(job.engine, job.model);
450
- answers[index] = { ...job, ...(await execute(job, { root, timeoutMs, index, signal, onOutputChunk })) };
500
+ const result = await execute(job, { root, timeoutMs, index, signal, onOutputChunk });
501
+ answers[index] = { ...job, ...normalizeAskResult(job.engine, result) };
451
502
  } catch (error) {
452
503
  answers[index] = {
453
504
  ...job,
@@ -568,12 +619,17 @@ async function runEngineAskCommand(args, root = process.cwd(), deps = {}) {
568
619
  started_at: startedAt,
569
620
  at: startedAt,
570
621
  read_only: true,
622
+ timeout_ms: parsed.timeoutMs,
571
623
  };
572
624
  const receiptPath = (deps.createReceipt || createEngineAskReceipt)(root, runningReceipt);
573
625
  const relativeReceiptPath = path.relative(root, receiptPath) || receiptPath;
574
626
  const liveLogPath = (deps.createLiveLog || createEngineLiveLog)(receiptPath);
575
627
  runningReceipt.live_log = path.relative(root, liveLogPath) || liveLogPath;
576
628
  (deps.updateReceipt || atomicWriteEngineAskReceipt)(receiptPath, runningReceipt);
629
+ if (!parsed.json) {
630
+ console.log(`engine ask started: ${relativeReceiptPath}`);
631
+ console.log(`watch: atris engine watch ${path.basename(receiptPath, '.json')}`);
632
+ }
577
633
  const appendLiveLog = deps.appendLiveLog
578
634
  || ((filePath, chunk) => appendEngineLiveLogChunk(filePath, chunk));
579
635
  let interruptedSignal = '';
@@ -671,6 +727,7 @@ module.exports = {
671
727
  MAX_ASK_PROMPT_BYTES,
672
728
  parseEngineAskArgs,
673
729
  buildReadOnlyEngineInvocation,
730
+ buildAskSpawnEnv,
674
731
  runAskProcess,
675
732
  runEngineAskJobs,
676
733
  runEngineAskCommand,
@@ -7,6 +7,7 @@ const {
7
7
  RUNNER_PROFILE_DEFS,
8
8
  RUNNER_PROFILE_ALIASES,
9
9
  RUNNER_PROFILE_NAMES,
10
+ HIDDEN_PROFILE_NAMES,
10
11
  } = require('./runner-command');
11
12
  const { rankEnginesDetailed, routerPickExplanation } = require('./router-brain');
12
13
 
@@ -53,6 +54,7 @@ const ENGINE_SEED_META = Object.freeze({
53
54
  tier: 'pro',
54
55
  roles: Object.freeze(['executor']),
55
56
  models: Object.freeze([
57
+ 'gemini-3.8-flash-high',
56
58
  'gemini-3.7-flash-high',
57
59
  'gemini-3.1-pro-high',
58
60
  'claude-sonnet-4-6',
@@ -62,6 +64,15 @@ const ENGINE_SEED_META = Object.freeze({
62
64
  duty: 'errands',
63
65
  fallback_order: 90,
64
66
  }),
67
+ opencode: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['opencode/big-pickle', 'opencode/claude-opus-4-8', 'opencode/gpt-5.2']), fallback_order: 95 }),
68
+ commandcode: Object.freeze({
69
+ tier: 'max',
70
+ roles: Object.freeze(['validator', 'executor']),
71
+ // No pinned model: the engine rides this machine's Command Code default.
72
+ models: Object.freeze(['machine default']),
73
+ duty: 'errands',
74
+ fallback_order: 98,
75
+ }),
65
76
  });
66
77
 
67
78
  function engineRegistryFile(root = process.cwd()) {
@@ -176,7 +187,9 @@ function seededRegistry(root = process.cwd(), preloadedRaw = null) {
176
187
  return {
177
188
  schema: ENGINE_REGISTRY_SCHEMA,
178
189
  updated_at: new Date().toISOString(),
179
- engines: RUNNER_PROFILE_NAMES.map((id) => normalizeEngineEntry(id, savedById.get(id) || {})),
190
+ // Hidden profiles are registered engines too: they seed, route, and take
191
+ // health policy exactly like visible ones.
192
+ engines: Object.keys(RUNNER_PROFILE_DEFS).map((id) => normalizeEngineEntry(id, savedById.get(id) || {})),
180
193
  };
181
194
  }
182
195
 
@@ -192,7 +205,7 @@ function writeEngineRegistry(root, registry) {
192
205
 
193
206
  function setEngineOverrides(name, overrides = {}, root = process.cwd()) {
194
207
  const id = canonicalEngineName(name);
195
- const knownIds = RUNNER_PROFILE_NAMES.filter((engineId) => ENGINE_SEED_META[engineId]);
208
+ const knownIds = Object.keys(RUNNER_PROFILE_DEFS).filter((engineId) => ENGINE_SEED_META[engineId]);
196
209
  if (!id || !knownIds.includes(id)) {
197
210
  throw new Error(`Unknown engine "${name}". Known engines: ${knownIds.join(', ')}`);
198
211
  }
package/lib/file-ops.js CHANGED
@@ -83,7 +83,7 @@ function createLogFile(logFile, dateFormatted) {
83
83
  const backlogBody = carryBacklog ? `${carryBacklog}\n\n` : '';
84
84
  const inboxBody = carryInbox ? `${carryInbox}\n\n` : '';
85
85
 
86
- const initialContent = `# Log ${dateFormatted}\n\n## Handoff\n\n---\n\n## Completed ✅\n\n---\n\n## In Progress 🔄\n\n${inProgressBody}---\n\n## Backlog\n\n${backlogBody}---\n\n## Notes\n\n---\n\n## Inbox\n\n${inboxBody}\n`;
86
+ const initialContent = `# Log, ${dateFormatted}\n\n## Handoff\n\n---\n\n## Completed ✅\n\n---\n\n## In Progress 🔄\n\n${inProgressBody}---\n\n## Backlog\n\n${backlogBody}---\n\n## Notes\n\n---\n\n## Inbox\n\n${inboxBody}\n`;
87
87
  fs.writeFileSync(logFile, initialContent);
88
88
  }
89
89
 
package/lib/fleet.js CHANGED
@@ -4,11 +4,11 @@
4
4
  // --fleet`), the full loop inside: staff every idle engine on claimable
5
5
  // safe-lane tasks, one mission per task, one worktree per engine, land
6
6
  // arrivals serially. Proven by hand on 2026-07-02 (PRs #191-193, three
7
- // engines) before any of this code existed each function here is one thing
7
+ // engines) before any of this code existed, each function here is one thing
8
8
  // the orchestrator did manually that day.
9
9
  //
10
10
  // INVARIANT: the fleet has NO state file. Flight state IS missions +
11
- // worktrees + receipts. Kill a flight mid-run and nothing is orphaned it
11
+ // worktrees + receipts. Kill a flight mid-run and nothing is orphaned, it
12
12
  // is just missions in flight, resumable the normal way.
13
13
 
14
14
  const fs = require('fs');
@@ -48,7 +48,7 @@ const { appendEngineLiveLogChunk, createEngineLiveLog, engineTerminalReason } =
48
48
  const DENIED_TAGS = ['billing', 'money', 'payments', 'deploy', 'security', 'customer', 'external', 'feedback'];
49
49
 
50
50
  // ---------------------------------------------------------------------------
51
- // T1 dispatch primitive
51
+ // T1: dispatch primitive
52
52
 
53
53
  // A task's own text is its spec: the board convention writes "Done: ..." and
54
54
  // "Check: ..." into the title/description. Extract both so the dispatch
@@ -68,10 +68,10 @@ function parseDoneCheck(text) {
68
68
  // hypothesis-driven unsticking, smallest diff.
69
69
  const METHOD_KERNEL = [
70
70
  'Read this whole brief before acting; locate every file you will touch before editing any of them.',
71
- 'Never pipe a verify or test command through tail/head/grep run it bare and read the real exit code.',
71
+ 'Never pipe a verify or test command through tail/head/grep, run it bare and read the real exit code.',
72
72
  'Done requires a receipt: paste the exact verify command and its final output lines in your report.',
73
73
  '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.',
74
- 'Stuck? Never run the same failing command a third time write 3 one-line hypotheses, then run the cheapest test that discriminates between them.',
74
+ 'Stuck? Never run the same failing command a third time, write 3 one-line hypotheses, then run the cheapest test that discriminates between them.',
75
75
  'Smallest diff that satisfies Done wins; prefer deleting code over adding it.',
76
76
  ];
77
77
 
@@ -99,7 +99,7 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
99
99
  '',
100
100
  ATRIS_BUILD_PROCESS_PREAMBLE,
101
101
  '',
102
- `You are working task ${ref} in this repo checkout (an isolated git worktree${worktreePath ? ` at ${worktreePath}` : ''} commit here, NEVER push).`,
102
+ `You are working task ${ref} in this repo checkout (an isolated git worktree${worktreePath ? ` at ${worktreePath}` : ''}, commit here, NEVER push).`,
103
103
  '',
104
104
  `Task: ${title}`,
105
105
  '',
@@ -145,10 +145,10 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
145
145
  }
146
146
 
147
147
  // Shell command that runs one engine on one prompt file, from the worktree.
148
- // Rides the same profile definitions the whole CLI uses no fleet-only
148
+ // Rides the same profile definitions the whole CLI uses, no fleet-only
149
149
  // spawn shapes.
150
150
  // Build work needs file tools: allowedTools reaches the default claude-shaped
151
- // spawn; template engines (cursor/codex/devin) ignore it their CLIs manage
151
+ // spawn; template engines (cursor/codex/devin) ignore it, their CLIs manage
152
152
  // their own permissions.
153
153
  const FLEET_ALLOWED_TOOLS = 'Bash,Read,Edit,Write,Grep,Glob';
154
154
  const VALIDATOR_ALLOWED_TOOLS = 'Bash,Read,Grep,Glob';
@@ -864,7 +864,7 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
864
864
  }
865
865
 
866
866
  // ---------------------------------------------------------------------------
867
- // T2 staffing
867
+ // T2: staffing
868
868
 
869
869
  const {
870
870
  taskTagTokens,
@@ -894,7 +894,7 @@ function isSafeLane(task) {
894
894
  }
895
895
 
896
896
  // Rough file-surface guess from a task's text: paths and bare `commands/x.js`
897
- // style mentions. Used only to keep concurrent picks disjoint the merge
897
+ // style mentions. Used only to keep concurrent picks disjoint, the merge
898
898
  // conflict on 2026-07-02 (two engines in commands/mission.js) is the tax this
899
899
  // avoids.
900
900
  function fileSurface(task) {
@@ -938,7 +938,7 @@ function assignEngines(staffed, engines) {
938
938
  }
939
939
 
940
940
  // ---------------------------------------------------------------------------
941
- // T3 serial landing lane
941
+ // T3: serial landing lane
942
942
 
943
943
  // Land one arrival: rebase onto latest base first; a conflict pauses the
944
944
  // landing (never auto-resolve) and reports which files collided so the
@@ -971,7 +971,7 @@ function dispatchResultToTerminal(entry = {}, result = {}) {
971
971
  }
972
972
 
973
973
  // Fleet landings always target master. Without the explicit --target, ship
974
- // falls back to the launcher branch's atris-base a flight launched from a
974
+ // falls back to the launcher branch's atris-base, a flight launched from a
975
975
  // feature-branch checkout would merge PRs into that branch while the receipt
976
976
  // and land board define "landed" as in-master (PRs #207/#208, 2026-07-04).
977
977
  // A ship failure must name its real cause. MODULE_NOT_FOUND from the spawned
@@ -1010,8 +1010,8 @@ function fleetShipArgs(entry, check) {
1010
1010
  // A fleet build worktree is ephemeral. If any process inside one runs
1011
1011
  // `npm link` (which repoints the global `atris` package symlink at that
1012
1012
  // worktree), then removing the worktree at teardown leaves the global `atris`
1013
- // binary a dangling symlink, and everything resolving `atris` through PATH
1014
- // interactive shells, any tooling that shells out to the bare command dies
1013
+ // binary a dangling symlink, and everything resolving `atris` through PATH:
1014
+ // interactive shells, any tooling that shells out to the bare command, dies
1015
1015
  // with 'command not found'. Measured scope, not assumed: crons that invoke
1016
1016
  // `node <checkout>/bin/atris.js` by absolute path are UNAFFECTED; an autoland
1017
1017
  // tick ran fine four minutes before the repair. The failure is silent and
@@ -1083,7 +1083,7 @@ function guardGlobalCliLink({
1083
1083
  };
1084
1084
  }
1085
1085
 
1086
- // One-line " (path, path)" suffix naming the files behind a paused landing
1086
+ // One-line " (path, path)" suffix naming the files behind a paused landing,
1087
1087
  // unmerged files for a rebase_conflict, uncommitted paths for a dirty_worktree.
1088
1088
  function pausedPathsSuffix(landed) {
1089
1089
  const paths = (landed && landed.conflicts) || (landed && landed.dirty) || [];
@@ -1166,10 +1166,10 @@ module.exports = {
1166
1166
  };
1167
1167
 
1168
1168
  // ---------------------------------------------------------------------------
1169
- // T4 the conductor: one flight, watchable, receipted
1169
+ // T4, the conductor: one flight, watchable, receipted
1170
1170
 
1171
1171
  // Engines that can edit a repo headlessly. atris-fast (ax) is a chat lane,
1172
- // not a repo worker it keeps owning normal mission ticks, not fleet builds.
1172
+ // not a repo worker, it keeps owning normal mission ticks, not fleet builds.
1173
1173
  const FLEET_CAPABLE = ['claude', 'codex', 'cursor', 'devin', 'grok'];
1174
1174
  const DISPATCH_CAPABLE = [...FLEET_CAPABLE, 'fable', 'commandcode'];
1175
1175
 
@@ -2326,6 +2326,33 @@ function defaultOwnCli(root) {
2326
2326
  };
2327
2327
  }
2328
2328
 
2329
+ // A merge and its task handoff are separate writes. Keep the merge on disk
2330
+ // before attempting the handoff, and never report success if that write fails.
2331
+ function recordLandedTaskReview(cli, args, { flight, landedRow, worktree, save }) {
2332
+ landedRow.review_recorded = false;
2333
+ save();
2334
+ let result;
2335
+ try { result = cli(args); } catch (error) {
2336
+ result = { status: null, stderr: error.message };
2337
+ }
2338
+ if (!result || result.status !== 0) {
2339
+ flight.paused.push({
2340
+ task: landedRow.task,
2341
+ engine: landedRow.engine,
2342
+ stage: 'task_ready',
2343
+ landed: true,
2344
+ worktree,
2345
+ detail: headTail(String(result && (result.stderr || result.stdout) || 'task ready failed')),
2346
+ next_action: 'record fresh task proof with task ready; do not rebuild or merge again',
2347
+ });
2348
+ save();
2349
+ return false;
2350
+ }
2351
+ landedRow.review_recorded = true;
2352
+ save();
2353
+ return true;
2354
+ }
2355
+
2329
2356
  // Extract a runnable focused check from the task's Check: line; the wide
2330
2357
  // gates (npm test, CI) stay at the ship gate, not per-arrival.
2331
2358
  function focusedCheck(task) {
@@ -2539,7 +2566,7 @@ async function runFleetFlight({
2539
2566
 
2540
2567
  const staffed = assignEngines(staffedTasks, roster);
2541
2568
  // The receipt path is decided BEFORE landings so each task's ready-proof
2542
- // can cite it the proof policy certifies receipt-backed proofs agent-side.
2569
+ // can cite it, the proof policy certifies receipt-backed proofs agent-side.
2543
2570
  const receiptPath = path.join(root, 'atris', 'runs', `fleet-${nowStamp()}.json`);
2544
2571
  const flight = {
2545
2572
  at: new Date().toISOString(),
@@ -2557,7 +2584,7 @@ async function runFleetFlight({
2557
2584
  }
2558
2585
 
2559
2586
  log('');
2560
- log(` fleet ${roster.length} engine${roster.length === 1 ? '' : 's'} ready, ${staffed.length} task${staffed.length === 1 ? '' : 's'} staffed`);
2587
+ log(` fleet, ${roster.length} engine${roster.length === 1 ? '' : 's'} ready, ${staffed.length} task${staffed.length === 1 ? '' : 's'} staffed`);
2561
2588
  for (const s of staffed) log(` ${s.engine.padEnd(8)} → ${s.task.display_id} ${String(s.task.title || '').slice(0, 80)}`);
2562
2589
  log('');
2563
2590
 
@@ -2596,7 +2623,7 @@ async function runFleetFlight({
2596
2623
  if (!claimed || claimed.status !== 0) {
2597
2624
  const detail = String(claimed && (claimed.stderr || claimed.stdout) || 'claim failed').trim().slice(0, 300);
2598
2625
  flight.paused.push({ task: entry.task.display_id, engine: entry.engine, stage: 'claim', detail });
2599
- log(` ${entry.engine.padEnd(8)} ⏸ paused ${entry.task.display_id} at claim${detail ? ` ${detail}` : ''}`);
2626
+ log(` ${entry.engine.padEnd(8)} ⏸ paused ${entry.task.display_id} at claim${detail ? `, ${detail}` : ''}`);
2600
2627
  continue;
2601
2628
  }
2602
2629
  const started = cli(['worktree', 'start', '--agent', entry.engine, '--task', `fleet-${String(entry.task.display_id).toLowerCase()}`, ...startBaseArgs]);
@@ -2658,7 +2685,7 @@ async function runFleetFlight({
2658
2685
  if (result.deadEngine) paused.deadEngine = result.deadEngine;
2659
2686
  flight.paused.push(paused);
2660
2687
  stampDispatchBrief(root, result.brief_id, 'fail', `build failed for ${entry.task.display_id}`);
2661
- log(` ${activeEngine.padEnd(8)} ✗ build failed ${entry.task.display_id} worktree kept for takeover`);
2688
+ log(` ${activeEngine.padEnd(8)} ✗ build failed ${entry.task.display_id}, worktree kept for takeover`);
2662
2689
  continue;
2663
2690
  }
2664
2691
  log(` ${activeEngine.padEnd(8)} landing ${entry.task.display_id}...`);
@@ -2668,19 +2695,24 @@ async function runFleetFlight({
2668
2695
  if (result.brief_id) landedRow.brief_id = result.brief_id;
2669
2696
  if (result.restaffed) landedRow.restaffed = result.restaffed;
2670
2697
  flight.landed.push(landedRow);
2671
- stampDispatchBrief(root, result.brief_id, 'pass', `landed ${entry.task.display_id} via fleet ship`);
2672
2698
  const restaffProof = result.restaffed ? ` Restaffed from ${result.restaffed.from} to ${result.restaffed.to} (${result.restaffed.reason}).` : '';
2673
- cli([
2699
+ const reviewRecorded = recordLandedTaskReview(cli, [
2674
2700
  'task', 'ready', String(entry.task.display_id),
2675
2701
  '--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: ${headTail(String(result.report || '').replace(/\n/g, ' '))}`,
2676
2702
  '--result', 'Operators can now review fleet-shipped work faster because the worktree was verified before landing.',
2677
2703
  '--as', `fleet-${activeEngine}`,
2678
- ]);
2704
+ ], { flight, landedRow, worktree: entry.worktreePath, save: () => atomicWriteFlightReceipt(flight, receiptPath) });
2705
+ if (!reviewRecorded) {
2706
+ stampDispatchBrief(root, result.brief_id, 'partial', `landed ${entry.task.display_id}, but task ready failed`);
2707
+ log(' the change landed, but saving its proof failed. repair the task record before starting more work.');
2708
+ continue;
2709
+ }
2710
+ stampDispatchBrief(root, result.brief_id, 'pass', `landed ${entry.task.display_id} via fleet ship`);
2679
2711
  log(` ${activeEngine.padEnd(8)} ✓ landed ${entry.task.display_id}`);
2680
2712
  } else {
2681
2713
  flight.paused.push({ task: entry.task.display_id, engine: activeEngine, ...(result.restaffed ? { restaffed: result.restaffed } : {}), ...landed });
2682
2714
  stampDispatchBrief(root, result.brief_id, 'partial', `paused ${entry.task.display_id} at ${landed.stage || 'landing'}`);
2683
- log(` ${activeEngine.padEnd(8)} ⏸ paused ${entry.task.display_id} at ${landed.stage}${pausedPathsSuffix(landed)} worktree kept`);
2715
+ log(` ${activeEngine.padEnd(8)} ⏸ paused ${entry.task.display_id} at ${landed.stage}${pausedPathsSuffix(landed)}, worktree kept`);
2684
2716
  }
2685
2717
  }
2686
2718
 
@@ -2714,7 +2746,7 @@ async function runFleetFlight({
2714
2746
  }
2715
2747
 
2716
2748
  // ---------------------------------------------------------------------------
2717
- // T5 one-command dispatch: `atris engine dispatch <task-id> --engine <name>`
2749
+ // T5, one-command dispatch: `atris engine dispatch <task-id> --engine <name>`
2718
2750
 
2719
2751
  // A claim taken before the engine starts must not stay held if the flight
2720
2752
  // refuses or errors first. Release through the same task plane the claim used.
@@ -2736,7 +2768,7 @@ function releaseUnstartedDispatchClaim(cli, { taskId, actor, detail }) {
2736
2768
  // claim so takeover stays attributed.
2737
2769
  function releasePausedClaimsWithoutWork(cli, { paused, worktreeByTask, actorByTask, baseRef = 'origin/master', log = () => {} }) {
2738
2770
  for (const row of paused || []) {
2739
- if (row.stage === 'claim' || row.stage === 'task_lookup') continue; // claim never held
2771
+ if (row.stage === 'claim' || row.stage === 'task_lookup' || row.landed === true) continue; // no claim, or work already landed
2740
2772
  if (row.claim_released !== undefined) continue;
2741
2773
  const taskId = String(row.task || '');
2742
2774
  const actor = actorByTask.get(taskId);
@@ -2815,7 +2847,7 @@ async function runDispatchFlight({
2815
2847
  }
2816
2848
 
2817
2849
  const cli = ownCli || defaultOwnCli(root);
2818
- // Cut worktrees from and check self-landing against the workspace's real
2850
+ // Cut worktrees from, and check self-landing against, the workspace's real
2819
2851
  // protected branch, not a hard-coded origin/master (CLI-1298).
2820
2852
  const landTarget = resolveDispatchLandTarget(root, checkoutBase);
2821
2853
  const verify = verifier || (trustedVerifier ? defaultTrustedVerifyRunner : defaultVerifyRunner);
@@ -2844,7 +2876,7 @@ async function runDispatchFlight({
2844
2876
  const liveLogPath = startLiveFlight(root, receiptPath, flight);
2845
2877
 
2846
2878
  log('');
2847
- log(` dispatch ${ids.length} task${ids.length === 1 ? '' : 's'} -> ${engine}`);
2879
+ log(` dispatch, ${ids.length} task${ids.length === 1 ? '' : 's'} -> ${engine}`);
2848
2880
  log('');
2849
2881
 
2850
2882
  // Same rule as the fleet: dispatch worktrees cut from the protected branch by
@@ -3100,7 +3132,7 @@ async function runDispatchFlight({
3100
3132
  if (result.deadEngine) paused.deadEngine = result.deadEngine;
3101
3133
  flight.paused.push(paused);
3102
3134
  stampDispatchBrief(root, result.brief_id, 'fail', `build failed for ${entry.taskId}`);
3103
- log(` ✗ build failed ${entry.taskId} worktree kept for takeover`);
3135
+ log(` ✗ build failed ${entry.taskId}, worktree kept for takeover`);
3104
3136
  continue;
3105
3137
  }
3106
3138
  if (enforceRemoteBoundary && (!candidate || candidate.ok !== true)) {
@@ -3445,16 +3477,20 @@ async function runDispatchFlight({
3445
3477
  if (result.brief_id) landedRow.brief_id = result.brief_id;
3446
3478
  if (result.restaffed) landedRow.restaffed = result.restaffed;
3447
3479
  flight.landed.push(landedRow);
3448
- stampDispatchBrief(root, result.brief_id, 'pass', `landed ${entry.taskId} via engine dispatch`);
3449
- cli([
3480
+ const reviewRecorded = recordLandedTaskReview(cli, [
3450
3481
  'task', 'ready', entry.taskId,
3451
3482
  '--proof', `Built by ${activeEngine} engine via atris engine dispatch.${restaffProof} Landed via worktree ship gate (rebase-before-ship, verify re-run). The declared verifier passed (exit 0). Verify output: ${verifyTail || '(command produced no output, exit 0)'}. Receipt saved at ${path.relative(root, receiptPath)}.`,
3452
3483
  '--result', 'Operators can now review engine-built work faster because dispatch reran the verifier before landing.',
3453
- '--landing', 'The verified change is on master and ready for review.',
3454
3484
  '--checked', `${landed.check} passed before landing`,
3455
3485
  '--tested', 'The requested behavior passed its declared verifier.',
3456
3486
  '--as', readyActor,
3457
- ]);
3487
+ ], { flight, landedRow, worktree: entry.worktreePath, save: () => writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary }) });
3488
+ if (!reviewRecorded) {
3489
+ stampDispatchBrief(root, result.brief_id, 'partial', `landed ${entry.taskId}, but task ready failed`);
3490
+ log(' the change landed, but saving its proof failed. repair the task record before starting more work.');
3491
+ continue;
3492
+ }
3493
+ stampDispatchBrief(root, result.brief_id, 'pass', `landed ${entry.taskId} via engine dispatch`);
3458
3494
  log(` ✓ landed ${entry.taskId}`);
3459
3495
  } else {
3460
3496
  flight.paused.push({
@@ -3465,7 +3501,7 @@ async function runDispatchFlight({
3465
3501
  ...landed,
3466
3502
  });
3467
3503
  stampDispatchBrief(root, result.brief_id, 'partial', `paused ${entry.taskId} at ${landed.stage || 'landing'}`);
3468
- log(` ⏸ paused ${entry.taskId} at ${landed.stage}${pausedPathsSuffix(landed)} worktree kept`);
3504
+ log(` ⏸ paused ${entry.taskId} at ${landed.stage}${pausedPathsSuffix(landed)}, worktree kept`);
3469
3505
  }
3470
3506
  }
3471
3507
 
@@ -1,4 +1,4 @@
1
- // Atris HTML renderer same content model as the deck engine, rendered to
1
+ // Atris HTML renderer, same content model as the deck engine, rendered to
2
2
  // beautiful, anti-slop HTML made of semantic blocks. Pure: spec -> HTML string.
3
3
  //
4
4
  // Connects with the web apps: the `atris` theme matches atrisos-web tokens