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.
- package/AGENTS.md +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- package/utils/update-check.js +27 -6
- 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', '
|
|
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 (!
|
|
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 || !
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
317
|
+
return { ok: true, cwd, command };
|
|
192
318
|
}
|
|
193
319
|
|
|
194
|
-
function
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
||
|
|
434
|
+
|| isAllowedAtrisVerifyArtifact(argv)
|
|
435
|
+
|| isAllowedGitDiffCheck(argv)
|
|
436
|
+
|| isAllowedPythonCommand(argv);
|
|
223
437
|
if (!allowed) return { ok: false, reason: 'verify_command_not_allowed' };
|
|
224
|
-
return {
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
269
|
-
|
|
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)
|
|
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
|
-
|
|
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: '
|
|
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 (
|
|
640
|
+
if (requiresStrictVerify) {
|
|
297
641
|
const verify = metadata.verify;
|
|
298
642
|
if (!verify) return strictVerifyMissingResult(ref);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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:
|
|
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:
|
|
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
|
};
|