atris 3.35.0 → 3.37.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.
Files changed (133) hide show
  1. package/AGENTS.md +37 -0
  2. package/README.md +5 -3
  3. package/atris/GETTING_STARTED.md +1 -1
  4. package/atris/atris.md +3 -0
  5. package/atris/policies/day-loop-voice.md +102 -0
  6. package/atris/policies/outbound-artifact-gate.md +2 -0
  7. package/atris/skills/design/SKILL.md +56 -32
  8. package/atris/skills/endgame/SKILL.md +12 -6
  9. package/atris/skills/engines/SKILL.md +22 -4
  10. package/atris/skills/fable-method/SKILL.md +66 -0
  11. package/atris/skills/improve/SKILL.md +65 -45
  12. package/atris/skills/youtube/SKILL.md +10 -1
  13. package/atris.md +2 -0
  14. package/ax +147 -19
  15. package/bin/atris.js +576 -267
  16. package/commands/activate.js +194 -88
  17. package/commands/agents.js +166 -0
  18. package/commands/autoland.js +459 -107
  19. package/commands/autopilot-front.js +20 -2
  20. package/commands/autopilot.js +118 -2
  21. package/commands/avail.js +407 -0
  22. package/commands/bench.js +188 -0
  23. package/commands/brain.js +3 -0
  24. package/commands/brief.js +651 -0
  25. package/commands/business-sync.js +192 -6
  26. package/commands/clean.js +50 -24
  27. package/commands/close.js +1083 -0
  28. package/commands/cloud.js +245 -0
  29. package/commands/compile.js +292 -1
  30. package/commands/computer.js +150 -3
  31. package/commands/dream.js +365 -0
  32. package/commands/drill.js +371 -0
  33. package/commands/engine.js +993 -32
  34. package/commands/experiments.js +28 -0
  35. package/commands/feedback.js +34 -12
  36. package/commands/fleet-report.js +206 -0
  37. package/commands/gm.js +23 -0
  38. package/commands/goal.js +247 -0
  39. package/commands/improve.js +642 -26
  40. package/commands/init.js +72 -44
  41. package/commands/interview.js +67 -1
  42. package/commands/land.js +152 -52
  43. package/commands/lifecycle.js +39 -3
  44. package/commands/log.js +84 -1
  45. package/commands/loops.js +220 -16
  46. package/commands/meet.js +220 -0
  47. package/commands/member.js +511 -34
  48. package/commands/mission.js +3144 -338
  49. package/commands/next.js +137 -0
  50. package/commands/now.js +220 -25
  51. package/commands/one-lap.js +776 -0
  52. package/commands/orb.js +314 -0
  53. package/commands/pack-craft.js +179 -0
  54. package/commands/pack.js +823 -0
  55. package/commands/play.js +3 -2
  56. package/commands/probe.js +30 -3
  57. package/commands/pulse.js +241 -46
  58. package/commands/push.js +260 -82
  59. package/commands/rainmaker.js +49 -0
  60. package/commands/report.js +415 -0
  61. package/commands/scout.js +147 -0
  62. package/commands/search.js +363 -0
  63. package/commands/skill.js +47 -3
  64. package/commands/slop.js +50 -2
  65. package/commands/soul.js +1 -1
  66. package/commands/stream.js +861 -0
  67. package/commands/study.js +693 -0
  68. package/commands/sync.js +67 -54
  69. package/commands/task.js +1346 -117
  70. package/commands/team.js +73 -0
  71. package/commands/verify.js +96 -0
  72. package/commands/watch.js +303 -0
  73. package/commands/wish.js +500 -0
  74. package/commands/workflow.js +11 -5
  75. package/commands/worktree.js +234 -13
  76. package/commands/xp.js +29 -11
  77. package/lib/auto-accept-certified.js +331 -34
  78. package/lib/autoland.js +319 -54
  79. package/lib/ax-auto-lane.js +96 -0
  80. package/lib/bench/context.js +147 -0
  81. package/lib/bench/engines.js +141 -0
  82. package/lib/bench/report.js +140 -0
  83. package/lib/bench/runner.js +512 -0
  84. package/lib/brief-ledger.js +350 -0
  85. package/lib/cloud-mission.js +259 -0
  86. package/lib/codex-flight.js +154 -0
  87. package/lib/default-runner.js +45 -0
  88. package/lib/default-verifier.js +70 -0
  89. package/lib/engine-registry.js +232 -0
  90. package/lib/experiments/daily.js +640 -0
  91. package/lib/fleet.js +2219 -67
  92. package/lib/improve-vitals-html.js +171 -0
  93. package/lib/known-commands.js +58 -0
  94. package/lib/loop-doctor.js +416 -0
  95. package/lib/member-switches.js +144 -0
  96. package/lib/mission-room.js +1 -0
  97. package/lib/mission-root.js +52 -0
  98. package/lib/next-moves.js +327 -10
  99. package/lib/one-lap-validator.js +60 -0
  100. package/lib/orb-context.js +477 -0
  101. package/lib/orb-scorecard.js +224 -0
  102. package/lib/policy-lessons.js +52 -1
  103. package/lib/pulse.js +277 -3
  104. package/lib/receipt-block.js +168 -0
  105. package/lib/receipt-evidence.js +65 -4
  106. package/lib/router-brain.js +352 -0
  107. package/lib/runner-command.js +10 -0
  108. package/lib/self-drive.js +258 -0
  109. package/lib/short-name.js +103 -0
  110. package/lib/spawn-env.js +18 -0
  111. package/lib/state-detection.js +56 -1
  112. package/lib/sync-status.js +59 -0
  113. package/lib/task-db.js +108 -29
  114. package/lib/task-proof.js +23 -1
  115. package/lib/team-presence.js +260 -0
  116. package/lib/tool-result-encode.js +7 -0
  117. package/lib/trust-tiers.js +90 -0
  118. package/lib/usage.js +107 -0
  119. package/lib/voice-gate.js +163 -0
  120. package/lib/wish-audit.js +1368 -0
  121. package/lib/wish-delegate.js +1840 -0
  122. package/lib/wish-design.js +110 -0
  123. package/lib/wish-stats.js +183 -0
  124. package/lib/wish-store.js +354 -0
  125. package/lib/zip.js +221 -0
  126. package/package.json +3 -1
  127. package/templates/loops/atris/loops/LOOPS.md +55 -0
  128. package/templates/loops/atris/loops/TICK.md +24 -0
  129. package/templates/loops/atris/loops/feedback.md +22 -0
  130. package/templates/loops/atris/loops/quality.md +22 -0
  131. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  132. package/utils/api.js +5 -1
  133. package/utils/auth.js +57 -21
@@ -4,18 +4,26 @@ const { spawnSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { taskProofState, taskProofExecutionState } = require('./task-proof');
7
+ const { extractReceiptEvidence } = require('./receipt-evidence');
7
8
  const reviewIntegrity = require('./review-integrity');
9
+ const { computeTrustTier } = require('./trust-tiers');
8
10
 
9
11
  const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
10
12
  // Kept for compat with older callers/tests; the pass-count landing lane it
11
13
  // once powered is gone. Passes alone never land work, an independent
12
14
  // reviewer does.
13
15
  const AUTO_ACCEPT_HIGH_CONFIDENCE_PASSES = 3;
14
- const DENIED_TAGS = new Set(['billing', 'deploy', 'feedback', 'voice', 'security', 'customer', 'external']);
16
+ const DENIED_TAGS = new Set(['billing', 'deploy', 'feedback', 'security', 'customer', 'external']);
15
17
 
16
18
  const SIMPLE_VERIFY_TOKEN_RE = /^[a-zA-Z0-9_./:@=+-]+$/;
17
19
  const GIT_WORKTREE_PATH_RE = /^[a-zA-Z0-9_./@=+-]+$/;
18
20
  const GIT_REV_TOKEN_RE = /^[a-zA-Z0-9_./@=+~^-]+$/;
21
+ const SAFE_RELATIVE_PATH_RE = /^[a-zA-Z0-9_./-]+$/;
22
+ const SAFE_ENV_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;
23
+ const SAFE_ENV_VALUE_RE = /^[A-Za-z0-9._/:-]*$/;
24
+ const PYTHON_SHORT_FLAG_RE = /^-[a-zA-Z-]+$/;
25
+ const PYTHON_LONG_FLAG_RE = /^--[a-zA-Z-]+(?:=[A-Za-z0-9._:-]+)?$/;
26
+ const NODE_TEST_PATTERN_RE = /^[A-Za-z0-9 _.,:!?@#%&=+|()[\]{}*^$~\\/-]+$/;
19
27
 
20
28
  function hasUnsafePathSegment(token) {
21
29
  const text = String(token || '');
@@ -58,11 +66,63 @@ function safeNodePathArgs(args) {
58
66
  return args.every(token => safeRelativePathToken(token));
59
67
  }
60
68
 
69
+ function safeCdPathToken(token) {
70
+ const text = String(token || '');
71
+ return Boolean(text)
72
+ && !text.startsWith('-')
73
+ && !text.includes(':')
74
+ && SAFE_RELATIVE_PATH_RE.test(text)
75
+ && !hasUnsafePathSegment(text);
76
+ }
77
+
78
+ function safePythonPathToken(token) {
79
+ const text = String(token || '');
80
+ return Boolean(text)
81
+ && SAFE_RELATIVE_PATH_RE.test(text)
82
+ && !/^[a-zA-Z]:[\\/]/.test(text);
83
+ }
84
+
85
+ function safePythonBinaryToken(token) {
86
+ const text = String(token || '');
87
+ return text === 'python'
88
+ || text === 'python3'
89
+ || (safePythonPathToken(text) && /(^|\/)venv\/bin\/python3?$/.test(text));
90
+ }
91
+
92
+ function safePythonRelativePathToken(token) {
93
+ const text = String(token || '');
94
+ return Boolean(text)
95
+ && !text.startsWith('-')
96
+ && !text.includes(':')
97
+ && SAFE_RELATIVE_PATH_RE.test(text)
98
+ && !hasUnsafePathSegment(text);
99
+ }
100
+
101
+ function safePythonFlagToken(token) {
102
+ const text = String(token || '');
103
+ return PYTHON_SHORT_FLAG_RE.test(text) || PYTHON_LONG_FLAG_RE.test(text);
104
+ }
105
+
106
+ function safePytestArg(token) {
107
+ return safePythonRelativePathToken(token) || safePythonFlagToken(token);
108
+ }
109
+
110
+ function safePythonScriptPath(token) {
111
+ const text = String(token || '');
112
+ return text.startsWith('scripts/')
113
+ && text.endsWith('.py')
114
+ && safePythonRelativePathToken(text);
115
+ }
116
+
117
+ function safePythonScriptArg(token) {
118
+ return safePythonRelativePathToken(token) || safePythonFlagToken(token);
119
+ }
120
+
61
121
  function safeNodeTestArgs(args) {
62
122
  let expectPattern = false;
63
123
  for (const token of args) {
64
124
  if (expectPattern) {
65
- if (!safeVerifyToken(token) || hasUnsafePathSegment(token)) return false;
125
+ if (!NODE_TEST_PATTERN_RE.test(token) || token.length > 500) return false;
66
126
  expectPattern = false;
67
127
  continue;
68
128
  }
@@ -72,7 +132,7 @@ function safeNodeTestArgs(args) {
72
132
  }
73
133
  if (String(token || '').startsWith('--test-name-pattern=')) {
74
134
  const pattern = String(token).slice('--test-name-pattern='.length);
75
- if (!pattern || !safeVerifyToken(pattern) || hasUnsafePathSegment(pattern)) return false;
135
+ if (!pattern || !NODE_TEST_PATTERN_RE.test(pattern) || pattern.length > 500) return false;
76
136
  continue;
77
137
  }
78
138
  if (!safeRelativePathToken(token)) return false;
@@ -105,15 +165,60 @@ function isAllowedAtrisCleanDryRun(argv) {
105
165
  && argv[4] === '--json';
106
166
  }
107
167
 
168
+ // `atris verify artifact <path>` is read-only by contract: it reads one file
169
+ // and prints substance checks. Allow the path plus numeric/json flags only;
170
+ // quoted --objective text stays outside the strict re-execution allowlist.
171
+ function isAllowedAtrisVerifyArtifact(argv) {
172
+ let rest = null;
173
+ if (argv[0] === 'atris' && argv[1] === 'verify' && argv[2] === 'artifact') rest = argv.slice(3);
174
+ if (argv[0] === 'node' && argv[1] === 'bin/atris.js' && argv[2] === 'verify' && argv[3] === 'artifact') rest = argv.slice(4);
175
+ if (!rest || !rest.length || !safeRelativePathToken(rest[0])) return false;
176
+ let index = 1;
177
+ while (index < rest.length) {
178
+ const token = rest[index];
179
+ if (token === '--json') { index += 1; continue; }
180
+ if ((token === '--min-lines' || token === '--max-age-hours') && /^\d{1,6}$/.test(rest[index + 1] || '')) {
181
+ index += 2;
182
+ continue;
183
+ }
184
+ return false;
185
+ }
186
+ return true;
187
+ }
188
+
108
189
  function isInsidePath(candidate, root) {
109
190
  const relative = path.relative(root, candidate);
110
191
  return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
111
192
  }
112
193
 
113
- function validateGitWorktreePath(argv, workspaceRoot) {
194
+ function parentArenaDir(workspace) {
195
+ let cursor = path.resolve(workspace);
196
+ while (cursor && cursor !== path.dirname(cursor)) {
197
+ if (path.basename(cursor) === 'arena') return cursor;
198
+ cursor = path.dirname(cursor);
199
+ }
200
+ return path.dirname(path.resolve(workspace));
201
+ }
202
+
203
+ function validateCommandCwd(parsed, workspaceRoot) {
204
+ const workspace = path.resolve(workspaceRoot || process.cwd());
205
+ if (!parsed.cwd) return { ok: true, cwd: workspace };
206
+ const target = path.resolve(workspace, parsed.cwd);
207
+ if (!isInsidePath(target, workspace)) return { ok: false, reason: 'verify_command_not_allowed' };
208
+ try {
209
+ if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
210
+ return { ok: false, reason: 'verify_workdir_missing' };
211
+ }
212
+ } catch {
213
+ return { ok: false, reason: 'verify_workdir_missing' };
214
+ }
215
+ return { ok: true, cwd: target };
216
+ }
217
+
218
+ function validateGitWorktreePath(argv, workspaceRoot, commandCwd) {
114
219
  if (!(argv[0] === 'git' && argv[1] === '-C')) return { ok: true };
115
220
  const workspace = path.resolve(workspaceRoot || process.cwd());
116
- const target = path.resolve(workspace, argv[2]);
221
+ const target = path.resolve(commandCwd || workspace, argv[2]);
117
222
  const allowedRoot = path.dirname(workspace);
118
223
  if (!isInsidePath(target, workspace) && !isInsidePath(target, allowedRoot)) {
119
224
  return { ok: false, reason: 'verify_command_not_allowed' };
@@ -122,6 +227,18 @@ function validateGitWorktreePath(argv, workspaceRoot) {
122
227
  return { ok: true };
123
228
  }
124
229
 
230
+ function validatePythonBinaryPath(argv, workspaceRoot, commandCwd) {
231
+ const bin = argv[0];
232
+ if (bin === 'python' || bin === 'python3' || !safePythonBinaryToken(bin)) return { ok: true };
233
+ const workspace = path.resolve(workspaceRoot || process.cwd());
234
+ const target = path.resolve(commandCwd || workspace, bin);
235
+ const allowedRoot = parentArenaDir(workspace);
236
+ if (!isInsidePath(target, workspace) && !isInsidePath(target, allowedRoot)) {
237
+ return { ok: false, reason: 'verify_command_not_allowed' };
238
+ }
239
+ return { ok: true };
240
+ }
241
+
125
242
  function reviewPassCount(task) {
126
243
  const metadata = task.metadata || {};
127
244
  const review = task.review || {};
@@ -189,20 +306,114 @@ function distinctReviewActors(task) {
189
306
  return reviewIntegrity.reviewEventActors(task);
190
307
  }
191
308
 
192
- function parseVerifyCommand(verify) {
193
- const cmd = String(verify || '').trim();
194
- if (!cmd) return { ok: false, reason: 'no_verify_command' };
195
- if (/[;&|`$<>\n\r]/.test(cmd)) return { ok: false, reason: 'verify_command_not_allowed' };
196
- const argv = cmd.split(/\s+/).filter(Boolean);
197
- if (!argv.length || argv.some((token, index) => {
309
+ function parseCdPrefix(cmd) {
310
+ const match = cmd.match(/^cd\s+(\S+)\s+&&\s+(.+)$/);
311
+ if (!match) return { ok: true, command: cmd };
312
+ const cwd = match[1];
313
+ const command = String(match[2] || '').trim();
314
+ if (!safeCdPathToken(cwd) || !command) {
315
+ return { ok: false, reason: 'verify_command_not_allowed' };
316
+ }
317
+ return { ok: true, cwd, command };
318
+ }
319
+
320
+ function parseLeadingEnv(tokens) {
321
+ const env = {};
322
+ let index = 0;
323
+ while (index < tokens.length) {
324
+ const token = tokens[index];
325
+ const equals = token.indexOf('=');
326
+ if (equals <= 0) break;
327
+ const key = token.slice(0, equals);
328
+ const value = token.slice(equals + 1);
329
+ if (!SAFE_ENV_KEY_RE.test(key) || !SAFE_ENV_VALUE_RE.test(value)) {
330
+ return { ok: false, reason: 'verify_command_not_allowed' };
331
+ }
332
+ env[key] = value;
333
+ index += 1;
334
+ }
335
+ return { ok: true, env, argv: tokens.slice(index) };
336
+ }
337
+
338
+ function safeVerifyArgv(argv) {
339
+ if (argv[0] === 'node' && argv[1] === '--test') {
340
+ return safeNodeTestArgs(argv.slice(2));
341
+ }
342
+ return argv.every((token, index) => {
343
+ if (safePythonBinaryToken(argv[0]) && index === 0) return true;
198
344
  if (argv[0] === 'git' && argv[1] === '-C' && index === 2) {
199
- return !safeGitWorktreePathToken(token);
345
+ return safeGitWorktreePathToken(token);
200
346
  }
201
347
  if (argv[0] === 'git' && ['diff', '-C'].includes(argv[1]) && index >= (argv[1] === '-C' ? 5 : 3)) {
202
- return !safeGitRevToken(token);
348
+ return safeGitRevToken(token);
349
+ }
350
+ return safeVerifyToken(token);
351
+ });
352
+ }
353
+
354
+ // Parse enough shell quoting to recover direct argv without ever invoking a
355
+ // shell. Operators are rejected outside quotes; quoted test-name regexes are
356
+ // passed as one literal argument to spawnSync({ shell: false }).
357
+ function tokenizeVerifyCommand(command) {
358
+ const tokens = [];
359
+ let current = '';
360
+ let quote = null;
361
+ let started = false;
362
+ const flush = () => {
363
+ if (!started) return;
364
+ tokens.push(current);
365
+ current = '';
366
+ started = false;
367
+ };
368
+ for (const char of String(command || '')) {
369
+ if (quote) {
370
+ if (char === quote) quote = null;
371
+ else if (char === '\n' || char === '\r') return { ok: false, reason: 'verify_command_not_allowed' };
372
+ else current += char;
373
+ started = true;
374
+ continue;
203
375
  }
204
- return !safeVerifyToken(token);
205
- })) {
376
+ if (char === "'" || char === '"') {
377
+ quote = char;
378
+ started = true;
379
+ continue;
380
+ }
381
+ if (/\s/.test(char)) {
382
+ flush();
383
+ continue;
384
+ }
385
+ if (/[;&|`$<>\\]/.test(char)) return { ok: false, reason: 'verify_command_not_allowed' };
386
+ current += char;
387
+ started = true;
388
+ }
389
+ if (quote) return { ok: false, reason: 'verify_command_not_allowed' };
390
+ flush();
391
+ return { ok: true, tokens };
392
+ }
393
+
394
+ function isAllowedPythonCommand(argv) {
395
+ const [bin, first, second] = argv;
396
+ if (!safePythonBinaryToken(bin)) return false;
397
+ if (first === '-m' && second === 'pytest') {
398
+ return argv.slice(3).every(safePytestArg);
399
+ }
400
+ if (safePythonScriptPath(first)) {
401
+ return argv.slice(2).every(safePythonScriptArg);
402
+ }
403
+ return false;
404
+ }
405
+
406
+ function parseVerifyCommand(verify) {
407
+ const cmd = String(verify || '').trim();
408
+ if (!cmd) return { ok: false, reason: 'no_verify_command' };
409
+ const prefixed = parseCdPrefix(cmd);
410
+ if (!prefixed.ok) return prefixed;
411
+ const tokenized = tokenizeVerifyCommand(prefixed.command);
412
+ if (!tokenized.ok) return tokenized;
413
+ const envParsed = parseLeadingEnv(tokenized.tokens);
414
+ if (!envParsed.ok) return envParsed;
415
+ const argv = envParsed.argv;
416
+ if (!argv.length || !safeVerifyArgv(argv)) {
206
417
  return { ok: false, reason: 'verify_command_not_allowed' };
207
418
  }
208
419
  const [bin, first, second] = argv;
@@ -216,10 +427,20 @@ function parseVerifyCommand(verify) {
216
427
  || (/^scripts\/[a-zA-Z0-9_./-]+$/.test(first || '') && safeNodePathArgs(argv.slice(1)))
217
428
  ))
218
429
  || (bin === 'tsc' && argv.length === 1)
430
+ // Mission verifiers use `test -s <artifact>`; accept the read-only
431
+ // file-check shapes so honest verifiers are recordable at review time.
432
+ || (bin === 'test' && argv.length === 3 && (first === '-s' || first === '-f') && safeRelativePathToken(second))
219
433
  || isAllowedAtrisCleanDryRun(argv)
220
- || isAllowedGitDiffCheck(argv);
434
+ || isAllowedAtrisVerifyArtifact(argv)
435
+ || isAllowedGitDiffCheck(argv)
436
+ || isAllowedPythonCommand(argv);
221
437
  if (!allowed) return { ok: false, reason: 'verify_command_not_allowed' };
222
- return { ok: true, argv };
438
+ return {
439
+ ok: true,
440
+ argv,
441
+ ...(prefixed.cwd ? { cwd: prefixed.cwd } : {}),
442
+ ...(Object.keys(envParsed.env).length ? { env: envParsed.env } : {}),
443
+ };
223
444
  }
224
445
 
225
446
  function isAutoCertifyVerifyCommandAllowed(verify) {
@@ -229,10 +450,15 @@ function isAutoCertifyVerifyCommandAllowed(verify) {
229
450
  function runVerifyCommand(verify, workspaceRoot) {
230
451
  const parsed = parseVerifyCommand(verify);
231
452
  if (!parsed.ok) return parsed;
232
- const gitPathCheck = validateGitWorktreePath(parsed.argv, workspaceRoot);
453
+ const cwdCheck = validateCommandCwd(parsed, workspaceRoot);
454
+ if (!cwdCheck.ok) return cwdCheck;
455
+ const gitPathCheck = validateGitWorktreePath(parsed.argv, workspaceRoot, cwdCheck.cwd);
233
456
  if (!gitPathCheck.ok) return gitPathCheck;
457
+ const pythonPathCheck = validatePythonBinaryPath(parsed.argv, workspaceRoot, cwdCheck.cwd);
458
+ if (!pythonPathCheck.ok) return pythonPathCheck;
234
459
  const result = spawnSync(parsed.argv[0], parsed.argv.slice(1), {
235
- cwd: workspaceRoot,
460
+ cwd: cwdCheck.cwd,
461
+ env: parsed.env ? { ...process.env, ...parsed.env } : process.env,
236
462
  shell: false,
237
463
  encoding: 'utf8',
238
464
  timeout: 120000,
@@ -245,6 +471,15 @@ function runVerifyCommand(verify, workspaceRoot) {
245
471
  };
246
472
  }
247
473
 
474
+ function runVerifyCommandCached(verify, workspaceRoot, cache = null) {
475
+ if (!(cache instanceof Map)) return runVerifyCommand(verify, workspaceRoot);
476
+ const key = `${path.resolve(workspaceRoot || process.cwd())}\u0000${String(verify || '').trim()}`;
477
+ if (cache.has(key)) return { ...cache.get(key), reused: true };
478
+ const result = runVerifyCommand(verify, workspaceRoot);
479
+ cache.set(key, result);
480
+ return { ...result, reused: false };
481
+ }
482
+
248
483
  function strictVerifyMissingResult(ref) {
249
484
  return {
250
485
  eligible: false,
@@ -255,12 +490,41 @@ function strictVerifyMissingResult(ref) {
255
490
  };
256
491
  }
257
492
 
493
+ // Receipts named in the proof are read back before landing. A forced
494
+ // completion or an explicitly failing receipt blocks auto-accept in every
495
+ // policy; a proof that names no receipts is unaffected (absence of a check
496
+ // does not block, same rule as verify commands).
497
+ function receiptEvidenceBlock(task, ref) {
498
+ const evidence = extractReceiptEvidence(latestProof(task), task.workspace_root || process.cwd());
499
+ if (!evidence) return null;
500
+ if (evidence.any_forced) {
501
+ return { eligible: false, ref, reason: 'forced_completion_needs_human', evidence };
502
+ }
503
+ const failing = evidence.receipts.find((entry) => entry.verifier_passed === false);
504
+ if (failing) {
505
+ return { eligible: false, ref, reason: 'receipt_verifier_failed', receipt: failing.path, evidence };
506
+ }
507
+ return null;
508
+ }
509
+
258
510
  function evaluateAutoAccept(task, options = {}) {
259
- const { strictVerify = true, minPasses = AGENT_CERTIFICATION_REVIEW_PASSES, acceptAll = false } = options;
511
+ const {
512
+ strictVerify = true,
513
+ minPasses = AGENT_CERTIFICATION_REVIEW_PASSES,
514
+ acceptAll = false,
515
+ executeVerify = true,
516
+ verifyCache = null,
517
+ } = options;
260
518
  const ref = task.display_id || task.legacy_ref || task.id;
261
519
  if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
262
520
  const metadata = task.metadata || {};
263
521
  const review = task.review || {};
522
+ const trustTier = computeTrustTier(
523
+ task.claimed_by || metadata.executed_by,
524
+ task.workspace_root || process.cwd(),
525
+ );
526
+ const tierRequiresStrictVerify = trustTier !== 'trusted';
527
+ const requiresStrictVerify = strictVerify || tierRequiresStrictVerify;
264
528
  const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
265
529
  if (approval && approval !== 'pending' && approval !== 'agent_certified') {
266
530
  return { eligible: false, ref, reason: `approval_${approval}` };
@@ -285,18 +549,36 @@ function evaluateAutoAccept(task, options = {}) {
285
549
  if (proofHasUnmergedPullRequestBoundary(proof)) {
286
550
  return unmergedPullRequestBoundaryResult(ref, proof);
287
551
  }
552
+ const proofCheck = taskProofState(proof);
553
+ if (proofCheck.code === 'suite_green_citation_required') {
554
+ return {
555
+ eligible: false,
556
+ ref,
557
+ reason: proofCheck.reason,
558
+ proof_state: proofCheck.code,
559
+ proof,
560
+ };
561
+ }
562
+ const evidenceBlock = receiptEvidenceBlock(task, ref);
563
+ if (evidenceBlock) return evidenceBlock;
288
564
  const verify = metadata.verify;
289
- if (verify) {
290
- const verifyResult = runVerifyCommand(verify, task.workspace_root || process.cwd());
565
+ if (tierRequiresStrictVerify && !verify) return strictVerifyMissingResult(ref);
566
+ if (verify && executeVerify) {
567
+ const verifyResult = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
291
568
  // A check that runs and fails blocks. So does a check whose worktree
292
569
  // is gone — otherwise the daily reap converts "has a failing check"
293
570
  // into "lands unchecked" the morning after it clears the worktree.
294
571
  // A check that merely isn't in the runnable allowlist counts as no
295
572
  // check at all.
296
- if (verifyResult.reason === 'verify_failed' || verifyResult.reason === 'verify_worktree_missing') {
573
+ if ((tierRequiresStrictVerify && !verifyResult.ok)
574
+ || verifyResult.reason === 'verify_failed'
575
+ || verifyResult.reason === 'verify_worktree_missing') {
297
576
  return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
298
577
  }
299
578
  }
579
+ if (trustTier === 'probation' && !reviewIntegrity.hasIndependentReview(task)) {
580
+ return { eligible: false, ref, reason: 'probation_needs_review' };
581
+ }
300
582
  return {
301
583
  eligible: true,
302
584
  ref,
@@ -304,6 +586,7 @@ function evaluateAutoAccept(task, options = {}) {
304
586
  passes: reviewPassCount(task),
305
587
  proof,
306
588
  policy: 'all_but_protected',
589
+ verification_pending: tierRequiresStrictVerify && Boolean(verify) && !executeVerify,
307
590
  };
308
591
  }
309
592
 
@@ -317,8 +600,18 @@ function evaluateAutoAccept(task, options = {}) {
317
600
  return unmergedPullRequestBoundaryResult(ref, proof);
318
601
  }
319
602
  const proofCheck = taskProofState(proof);
320
- if (!proofCheck.ok) return { eligible: false, ref, reason: proofCheck.reason, proof };
321
- if (!strictVerify) {
603
+ if (!proofCheck.ok) {
604
+ return {
605
+ eligible: false,
606
+ ref,
607
+ reason: proofCheck.reason,
608
+ proof_state: proofCheck.code || null,
609
+ proof,
610
+ };
611
+ }
612
+ const evidenceBlock = receiptEvidenceBlock(task, ref);
613
+ if (evidenceBlock) return evidenceBlock;
614
+ if (!requiresStrictVerify) {
322
615
  const executionCheck = taskProofExecutionState(proof);
323
616
  if (!executionCheck.ok) {
324
617
  return {
@@ -326,7 +619,7 @@ function evaluateAutoAccept(task, options = {}) {
326
619
  ref,
327
620
  reason: executionCheck.reason,
328
621
  detail: executionCheck.detail,
329
- next_action: 'run an allowed verifier with `atris task ready --verify` or keep strict auto-accept enabled so the verifier executes before landing',
622
+ next_action: 'run an allowed verifier with `atris task ready --verify "<cmd>" --result "<day-one PM sentence>"` or keep strict auto-accept enabled so the verifier executes before landing',
330
623
  proof,
331
624
  };
332
625
  }
@@ -337,31 +630,34 @@ function evaluateAutoAccept(task, options = {}) {
337
630
  return {
338
631
  eligible: false,
339
632
  ref,
340
- reason: 'needs_independent_reviewer',
633
+ reason: trustTier === 'probation' ? 'probation_needs_review' : 'needs_independent_reviewer',
341
634
  passes,
342
635
  builder: reviewIntegrity.taskBuilder(task),
343
636
  actors: [...actors],
344
637
  };
345
638
  }
346
639
 
347
- if (strictVerify) {
640
+ if (requiresStrictVerify) {
348
641
  const verify = metadata.verify;
349
642
  if (!verify) return strictVerifyMissingResult(ref);
350
- const workspaceRoot = task.workspace_root || process.cwd();
351
- const verifyResult = runVerifyCommand(verify, workspaceRoot);
352
- if (!verifyResult.ok) {
353
- return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
643
+ if (executeVerify) {
644
+ const workspaceRoot = task.workspace_root || process.cwd();
645
+ const verifyResult = runVerifyCommandCached(verify, workspaceRoot, verifyCache);
646
+ if (!verifyResult.ok) {
647
+ return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
648
+ }
354
649
  }
355
650
  }
356
651
 
357
652
  return {
358
653
  eligible: true,
359
654
  ref,
360
- reason: strictVerify ? 'certified_strict_verify' : 'certified_independent_review',
655
+ reason: requiresStrictVerify ? 'certified_strict_verify' : 'certified_independent_review',
361
656
  passes,
362
657
  actors: [...actors],
363
658
  proof,
364
- policy: strictVerify ? 'strict_verify' : 'independent_reviewer',
659
+ policy: requiresStrictVerify ? 'strict_verify' : 'independent_reviewer',
660
+ verification_pending: requiresStrictVerify && !executeVerify,
365
661
  };
366
662
  }
367
663
 
@@ -374,4 +670,5 @@ module.exports = {
374
670
  isAgentCertified,
375
671
  parseVerifyCommand,
376
672
  runVerifyCommand,
673
+ runVerifyCommandCached,
377
674
  };