atris 3.34.0 → 3.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/AGENTS.md +35 -0
  2. package/FOR_AGENTS.md +5 -3
  3. package/README.md +5 -3
  4. package/atris/GETTING_STARTED.md +1 -1
  5. package/atris/atris.md +3 -0
  6. package/atris/policies/day-loop-voice.md +102 -0
  7. package/atris/policies/outbound-artifact-gate.md +2 -0
  8. package/atris/skills/design/SKILL.md +56 -32
  9. package/atris/skills/endgame/SKILL.md +12 -6
  10. package/atris/skills/engines/SKILL.md +35 -8
  11. package/atris/skills/fable-method/SKILL.md +66 -0
  12. package/atris/skills/improve/SKILL.md +65 -45
  13. package/atris/skills/render-cli/SKILL.md +88 -0
  14. package/atris/skills/youtube/SKILL.md +10 -1
  15. package/atris.md +6 -2
  16. package/ax +617 -31
  17. package/bin/atris.js +719 -266
  18. package/commands/activate.js +194 -88
  19. package/commands/aeo.js +52 -0
  20. package/commands/agents.js +166 -0
  21. package/commands/autoland.js +718 -72
  22. package/commands/autopilot-front.js +20 -2
  23. package/commands/autopilot.js +118 -2
  24. package/commands/avail.js +407 -0
  25. package/commands/bench.js +188 -0
  26. package/commands/brain.js +3 -0
  27. package/commands/brief.js +651 -0
  28. package/commands/business-sync.js +192 -6
  29. package/commands/business.js +91 -8
  30. package/commands/clean.js +50 -24
  31. package/commands/close.js +1083 -0
  32. package/commands/cloud.js +245 -0
  33. package/commands/codex-goal.js +26 -2
  34. package/commands/compile.js +292 -1
  35. package/commands/computer.js +150 -3
  36. package/commands/dream.js +365 -0
  37. package/commands/drill.js +371 -0
  38. package/commands/drive.js +187 -0
  39. package/commands/engine.js +1061 -29
  40. package/commands/experiments.js +28 -0
  41. package/commands/feed.js +202 -0
  42. package/commands/feedback.js +34 -12
  43. package/commands/fleet-report.js +206 -0
  44. package/commands/github.js +38 -0
  45. package/commands/gm.js +285 -3
  46. package/commands/goal.js +247 -0
  47. package/commands/improve.js +642 -26
  48. package/commands/init.js +83 -43
  49. package/commands/integrations.js +39 -11
  50. package/commands/interview.js +209 -0
  51. package/commands/land.js +253 -52
  52. package/commands/lesson.js +112 -1
  53. package/commands/lifecycle.js +39 -3
  54. package/commands/linear.js +38 -0
  55. package/commands/log.js +84 -1
  56. package/commands/loops.js +220 -16
  57. package/commands/meet.js +220 -0
  58. package/commands/member.js +899 -66
  59. package/commands/mission.js +3512 -332
  60. package/commands/next.js +137 -0
  61. package/commands/now.js +240 -21
  62. package/commands/one-lap.js +776 -0
  63. package/commands/orb.js +314 -0
  64. package/commands/pack-craft.js +179 -0
  65. package/commands/pack.js +823 -0
  66. package/commands/play.js +3 -2
  67. package/commands/probe.js +30 -3
  68. package/commands/pulse.js +241 -46
  69. package/commands/push.js +260 -82
  70. package/commands/radar.js +259 -14
  71. package/commands/rainmaker.js +49 -0
  72. package/commands/report.js +415 -0
  73. package/commands/scout.js +147 -0
  74. package/commands/search.js +363 -0
  75. package/commands/serve.js +54 -0
  76. package/commands/skill.js +47 -3
  77. package/commands/slop.js +50 -2
  78. package/commands/soul.js +1 -1
  79. package/commands/status.js +50 -5
  80. package/commands/stream.js +861 -0
  81. package/commands/stripe.js +38 -0
  82. package/commands/study.js +693 -0
  83. package/commands/supabase.js +39 -0
  84. package/commands/sync.js +67 -54
  85. package/commands/task.js +2275 -182
  86. package/commands/team.js +73 -0
  87. package/commands/truth.js +29 -3
  88. package/commands/unknowns.js +627 -0
  89. package/commands/update.js +44 -0
  90. package/commands/vercel.js +38 -0
  91. package/commands/verify.js +96 -0
  92. package/commands/watch.js +303 -0
  93. package/commands/wish.js +500 -0
  94. package/commands/workflow.js +11 -5
  95. package/commands/worktree.js +299 -20
  96. package/commands/write.js +399 -0
  97. package/commands/xp.js +29 -11
  98. package/lib/auto-accept-certified.js +391 -43
  99. package/lib/autoland.js +353 -52
  100. package/lib/ax-auto-lane.js +79 -0
  101. package/lib/bench/context.js +147 -0
  102. package/lib/bench/engines.js +141 -0
  103. package/lib/bench/report.js +140 -0
  104. package/lib/bench/runner.js +512 -0
  105. package/lib/brief-ledger.js +350 -0
  106. package/lib/cloud-mission.js +259 -0
  107. package/lib/codex-flight.js +154 -0
  108. package/lib/default-runner.js +45 -0
  109. package/lib/default-verifier.js +70 -0
  110. package/lib/engine-registry.js +232 -0
  111. package/lib/experiments/daily.js +640 -0
  112. package/lib/fleet.js +2431 -38
  113. package/lib/improve-vitals-html.js +171 -0
  114. package/lib/known-commands.js +58 -0
  115. package/lib/loop-doctor.js +416 -0
  116. package/lib/member-switches.js +144 -0
  117. package/lib/memory-view.js +14 -5
  118. package/lib/mission-room.js +1 -0
  119. package/lib/mission-root.js +52 -0
  120. package/lib/mission-runtime-loop.js +7 -0
  121. package/lib/next-moves.js +327 -10
  122. package/lib/official-cli-integration.js +174 -0
  123. package/lib/one-lap-validator.js +60 -0
  124. package/lib/orb-context.js +477 -0
  125. package/lib/orb-scorecard.js +224 -0
  126. package/lib/outbound-send-gate.js +165 -0
  127. package/lib/permission-grants.js +293 -0
  128. package/lib/policy-lessons.js +52 -1
  129. package/lib/pulse.js +277 -3
  130. package/lib/receipt-block.js +168 -0
  131. package/lib/receipt-evidence.js +65 -4
  132. package/lib/review-integrity.js +147 -0
  133. package/lib/router-brain.js +352 -0
  134. package/lib/runner-command.js +33 -0
  135. package/lib/self-drive.js +258 -0
  136. package/lib/short-name.js +103 -0
  137. package/lib/spawn-env.js +18 -0
  138. package/lib/state-detection.js +56 -1
  139. package/lib/sync-status.js +59 -0
  140. package/lib/task-db.js +319 -27
  141. package/lib/task-proof.js +43 -1
  142. package/lib/task-receipt.js +93 -0
  143. package/lib/team-presence.js +260 -0
  144. package/lib/tool-result-encode.js +7 -0
  145. package/lib/trust-tiers.js +90 -0
  146. package/lib/usage.js +107 -0
  147. package/lib/voice-gate.js +163 -0
  148. package/lib/wish-audit.js +1368 -0
  149. package/lib/wish-delegate.js +1840 -0
  150. package/lib/wish-design.js +110 -0
  151. package/lib/wish-stats.js +183 -0
  152. package/lib/wish-store.js +354 -0
  153. package/lib/zip.js +221 -0
  154. package/package.json +3 -1
  155. package/templates/loops/atris/loops/LOOPS.md +55 -0
  156. package/templates/loops/atris/loops/TICK.md +24 -0
  157. package/templates/loops/atris/loops/feedback.md +22 -0
  158. package/templates/loops/atris/loops/quality.md +22 -0
  159. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  160. package/utils/api.js +5 -1
  161. package/utils/auth.js +57 -21
  162. package/utils/update-check.js +27 -6
  163. package/atris/learnings.jsonl +0 -1
@@ -3,15 +3,27 @@
3
3
  const { spawnSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { taskProofState } = require('./task-proof');
6
+ const { taskProofState, taskProofExecutionState } = require('./task-proof');
7
+ const { extractReceiptEvidence } = require('./receipt-evidence');
8
+ const reviewIntegrity = require('./review-integrity');
9
+ const { computeTrustTier } = require('./trust-tiers');
7
10
 
8
11
  const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
12
+ // Kept for compat with older callers/tests; the pass-count landing lane it
13
+ // once powered is gone. Passes alone never land work, an independent
14
+ // reviewer does.
9
15
  const AUTO_ACCEPT_HIGH_CONFIDENCE_PASSES = 3;
10
- const DENIED_TAGS = new Set(['billing', 'deploy', 'feedback', 'voice', 'security', 'customer', 'external']);
16
+ const DENIED_TAGS = new Set(['billing', 'deploy', 'feedback', 'security', 'customer', 'external']);
11
17
 
12
18
  const SIMPLE_VERIFY_TOKEN_RE = /^[a-zA-Z0-9_./:@=+-]+$/;
13
19
  const GIT_WORKTREE_PATH_RE = /^[a-zA-Z0-9_./@=+-]+$/;
14
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 _.,:!?@#%&=+|()[\]{}*^$~\\/-]+$/;
15
27
 
16
28
  function hasUnsafePathSegment(token) {
17
29
  const text = String(token || '');
@@ -54,11 +66,63 @@ function safeNodePathArgs(args) {
54
66
  return args.every(token => safeRelativePathToken(token));
55
67
  }
56
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
+
57
121
  function safeNodeTestArgs(args) {
58
122
  let expectPattern = false;
59
123
  for (const token of args) {
60
124
  if (expectPattern) {
61
- if (!safeVerifyToken(token) || hasUnsafePathSegment(token)) return false;
125
+ if (!NODE_TEST_PATTERN_RE.test(token) || token.length > 500) return false;
62
126
  expectPattern = false;
63
127
  continue;
64
128
  }
@@ -68,7 +132,7 @@ function safeNodeTestArgs(args) {
68
132
  }
69
133
  if (String(token || '').startsWith('--test-name-pattern=')) {
70
134
  const pattern = String(token).slice('--test-name-pattern='.length);
71
- if (!pattern || !safeVerifyToken(pattern) || hasUnsafePathSegment(pattern)) return false;
135
+ if (!pattern || !NODE_TEST_PATTERN_RE.test(pattern) || pattern.length > 500) return false;
72
136
  continue;
73
137
  }
74
138
  if (!safeRelativePathToken(token)) return false;
@@ -101,15 +165,60 @@ function isAllowedAtrisCleanDryRun(argv) {
101
165
  && argv[4] === '--json';
102
166
  }
103
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
+
104
189
  function isInsidePath(candidate, root) {
105
190
  const relative = path.relative(root, candidate);
106
191
  return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
107
192
  }
108
193
 
109
- 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) {
110
219
  if (!(argv[0] === 'git' && argv[1] === '-C')) return { ok: true };
111
220
  const workspace = path.resolve(workspaceRoot || process.cwd());
112
- const target = path.resolve(workspace, argv[2]);
221
+ const target = path.resolve(commandCwd || workspace, argv[2]);
113
222
  const allowedRoot = path.dirname(workspace);
114
223
  if (!isInsidePath(target, workspace) && !isInsidePath(target, allowedRoot)) {
115
224
  return { ok: false, reason: 'verify_command_not_allowed' };
@@ -118,6 +227,18 @@ function validateGitWorktreePath(argv, workspaceRoot) {
118
227
  return { ok: true };
119
228
  }
120
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
+
121
242
  function reviewPassCount(task) {
122
243
  const metadata = task.metadata || {};
123
244
  const review = task.review || {};
@@ -182,29 +303,117 @@ function unmergedPullRequestBoundaryResult(ref, proof) {
182
303
  }
183
304
 
184
305
  function distinctReviewActors(task) {
185
- const actors = new Set();
186
- for (const event of task.events || []) {
187
- if (!['proof_ready', 'reviewed'].includes(event.event_type)) continue;
188
- const actor = event.actor || event.payload?.actor;
189
- if (actor) actors.add(String(actor));
306
+ return reviewIntegrity.reviewEventActors(task);
307
+ }
308
+
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' };
190
316
  }
191
- return actors;
317
+ return { ok: true, cwd, command };
192
318
  }
193
319
 
194
- function parseVerifyCommand(verify) {
195
- const cmd = String(verify || '').trim();
196
- if (!cmd) return { ok: false, reason: 'no_verify_command' };
197
- if (/[;&|`$<>\n\r]/.test(cmd)) return { ok: false, reason: 'verify_command_not_allowed' };
198
- const argv = cmd.split(/\s+/).filter(Boolean);
199
- if (!argv.length || argv.some((token, index) => {
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;
200
344
  if (argv[0] === 'git' && argv[1] === '-C' && index === 2) {
201
- return !safeGitWorktreePathToken(token);
345
+ return safeGitWorktreePathToken(token);
202
346
  }
203
347
  if (argv[0] === 'git' && ['diff', '-C'].includes(argv[1]) && index >= (argv[1] === '-C' ? 5 : 3)) {
204
- 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;
375
+ }
376
+ if (char === "'" || char === '"') {
377
+ quote = char;
378
+ started = true;
379
+ continue;
205
380
  }
206
- return !safeVerifyToken(token);
207
- })) {
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)) {
208
417
  return { ok: false, reason: 'verify_command_not_allowed' };
209
418
  }
210
419
  const [bin, first, second] = argv;
@@ -218,19 +427,38 @@ function parseVerifyCommand(verify) {
218
427
  || (/^scripts\/[a-zA-Z0-9_./-]+$/.test(first || '') && safeNodePathArgs(argv.slice(1)))
219
428
  ))
220
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))
221
433
  || isAllowedAtrisCleanDryRun(argv)
222
- || isAllowedGitDiffCheck(argv);
434
+ || isAllowedAtrisVerifyArtifact(argv)
435
+ || isAllowedGitDiffCheck(argv)
436
+ || isAllowedPythonCommand(argv);
223
437
  if (!allowed) return { ok: false, reason: 'verify_command_not_allowed' };
224
- 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
+ };
444
+ }
445
+
446
+ function isAutoCertifyVerifyCommandAllowed(verify) {
447
+ return parseVerifyCommand(verify).ok;
225
448
  }
226
449
 
227
450
  function runVerifyCommand(verify, workspaceRoot) {
228
451
  const parsed = parseVerifyCommand(verify);
229
452
  if (!parsed.ok) return parsed;
230
- 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);
231
456
  if (!gitPathCheck.ok) return gitPathCheck;
457
+ const pythonPathCheck = validatePythonBinaryPath(parsed.argv, workspaceRoot, cwdCheck.cwd);
458
+ if (!pythonPathCheck.ok) return pythonPathCheck;
232
459
  const result = spawnSync(parsed.argv[0], parsed.argv.slice(1), {
233
- cwd: workspaceRoot,
460
+ cwd: cwdCheck.cwd,
461
+ env: parsed.env ? { ...process.env, ...parsed.env } : process.env,
234
462
  shell: false,
235
463
  encoding: 'utf8',
236
464
  timeout: 120000,
@@ -243,6 +471,15 @@ function runVerifyCommand(verify, workspaceRoot) {
243
471
  };
244
472
  }
245
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
+
246
483
  function strictVerifyMissingResult(ref) {
247
484
  return {
248
485
  eligible: false,
@@ -253,20 +490,105 @@ function strictVerifyMissingResult(ref) {
253
490
  };
254
491
  }
255
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
+
256
510
  function evaluateAutoAccept(task, options = {}) {
257
- const { strictVerify = false, minPasses = AGENT_CERTIFICATION_REVIEW_PASSES } = options;
511
+ const {
512
+ strictVerify = true,
513
+ minPasses = AGENT_CERTIFICATION_REVIEW_PASSES,
514
+ acceptAll = false,
515
+ executeVerify = true,
516
+ verifyCache = null,
517
+ } = options;
258
518
  const ref = task.display_id || task.legacy_ref || task.id;
259
519
  if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
260
520
  const metadata = task.metadata || {};
261
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;
262
528
  const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
263
529
  if (approval && approval !== 'pending' && approval !== 'agent_certified') {
264
530
  return { eligible: false, ref, reason: `approval_${approval}` };
265
531
  }
266
532
  if (metadata.auto_accepted_at) return { eligible: false, ref, reason: 'already_auto_accepted' };
267
533
 
268
- const tag = String(task.tag || '').toLowerCase();
269
- if (DENIED_TAGS.has(tag)) return { eligible: false, ref, reason: `denied_tag_${tag}` };
534
+ // 'deploys', 'infra-deploy', and 'Billing ' are the same lanes as their
535
+ // exact-match cousins: match denied lanes on whole words with a plural
536
+ // strip, so a tag variant never slips money/deploy work past the human.
537
+ const tag = String(task.tag || '').trim().toLowerCase();
538
+ const deniedTag = [...DENIED_TAGS].find((d) =>
539
+ tag === d || tag.split(/[^a-z0-9]+/).some((w) => w === d || w.replace(/s$/, '') === d));
540
+ if (deniedTag) return { eligible: false, ref, reason: `denied_tag_${deniedTag}` };
541
+
542
+ // accept-all: the protected lanes above are the only human gate. No
543
+ // certification, pass-count, reviewer, or proof-quality bar — but work
544
+ // is never marked done against evidence it isn't: a proof naming an
545
+ // unmerged draft PR still blocks, and a recorded check that FAILS still
546
+ // blocks (absence of a check does not).
547
+ if (acceptAll) {
548
+ const proof = latestProof(task);
549
+ if (proofHasUnmergedPullRequestBoundary(proof)) {
550
+ return unmergedPullRequestBoundaryResult(ref, proof);
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;
564
+ const verify = metadata.verify;
565
+ if (tierRequiresStrictVerify && !verify) return strictVerifyMissingResult(ref);
566
+ if (verify && executeVerify) {
567
+ const verifyResult = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
568
+ // A check that runs and fails blocks. So does a check whose worktree
569
+ // is gone — otherwise the daily reap converts "has a failing check"
570
+ // into "lands unchecked" the morning after it clears the worktree.
571
+ // A check that merely isn't in the runnable allowlist counts as no
572
+ // check at all.
573
+ if ((tierRequiresStrictVerify && !verifyResult.ok)
574
+ || verifyResult.reason === 'verify_failed'
575
+ || verifyResult.reason === 'verify_worktree_missing') {
576
+ return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
577
+ }
578
+ }
579
+ if (trustTier === 'probation' && !reviewIntegrity.hasIndependentReview(task)) {
580
+ return { eligible: false, ref, reason: 'probation_needs_review' };
581
+ }
582
+ return {
583
+ eligible: true,
584
+ ref,
585
+ reason: 'accept_all_but_protected',
586
+ passes: reviewPassCount(task),
587
+ proof,
588
+ policy: 'all_but_protected',
589
+ verification_pending: tierRequiresStrictVerify && Boolean(verify) && !executeVerify,
590
+ };
591
+ }
270
592
 
271
593
  if (!isAgentCertified(task)) return { eligible: false, ref, reason: 'not_agent_certified' };
272
594
 
@@ -278,41 +600,64 @@ function evaluateAutoAccept(task, options = {}) {
278
600
  return unmergedPullRequestBoundaryResult(ref, proof);
279
601
  }
280
602
  const proofCheck = taskProofState(proof);
281
- if (!proofCheck.ok) return { eligible: false, ref, reason: proofCheck.reason, proof };
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) {
615
+ const executionCheck = taskProofExecutionState(proof);
616
+ if (!executionCheck.ok) {
617
+ return {
618
+ eligible: false,
619
+ ref,
620
+ reason: executionCheck.reason,
621
+ detail: executionCheck.detail,
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',
623
+ proof,
624
+ };
625
+ }
626
+ }
282
627
 
283
628
  const actors = distinctReviewActors(task);
284
- const multiActor = actors.size >= 2;
285
- const highConfidence = passes >= AUTO_ACCEPT_HIGH_CONFIDENCE_PASSES;
286
- if (!multiActor && !highConfidence) {
629
+ if (!reviewIntegrity.hasIndependentReview(task)) {
287
630
  return {
288
631
  eligible: false,
289
632
  ref,
290
- reason: 'needs_second_reviewer_or_third_pass',
633
+ reason: trustTier === 'probation' ? 'probation_needs_review' : 'needs_independent_reviewer',
291
634
  passes,
635
+ builder: reviewIntegrity.taskBuilder(task),
292
636
  actors: [...actors],
293
637
  };
294
638
  }
295
639
 
296
- if (strictVerify) {
640
+ if (requiresStrictVerify) {
297
641
  const verify = metadata.verify;
298
642
  if (!verify) return strictVerifyMissingResult(ref);
299
- const workspaceRoot = task.workspace_root || process.cwd();
300
- const verifyResult = runVerifyCommand(verify, workspaceRoot);
301
- if (!verifyResult.ok) {
302
- 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
+ }
303
649
  }
304
650
  }
305
651
 
306
652
  return {
307
653
  eligible: true,
308
654
  ref,
309
- reason: strictVerify
310
- ? 'certified_strict_verify'
311
- : (highConfidence ? 'certified_high_confidence' : 'certified_multi_actor'),
655
+ reason: requiresStrictVerify ? 'certified_strict_verify' : 'certified_independent_review',
312
656
  passes,
313
657
  actors: [...actors],
314
658
  proof,
315
- policy: strictVerify ? 'strict_verify' : (highConfidence ? '3_passes' : '2_actors_2_passes'),
659
+ policy: requiresStrictVerify ? 'strict_verify' : 'independent_reviewer',
660
+ verification_pending: requiresStrictVerify && !executeVerify,
316
661
  };
317
662
  }
318
663
 
@@ -321,6 +666,9 @@ module.exports = {
321
666
  AUTO_ACCEPT_HIGH_CONFIDENCE_PASSES,
322
667
  DENIED_TAGS,
323
668
  evaluateAutoAccept,
669
+ isAutoCertifyVerifyCommandAllowed,
670
+ isAgentCertified,
324
671
  parseVerifyCommand,
325
672
  runVerifyCommand,
673
+ runVerifyCommandCached,
326
674
  };