atris 3.35.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 +37 -0
- 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 +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +565 -265
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- 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/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- 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/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3029 -339
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- 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/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -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 +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- 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 +2219 -67
- 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/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -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/router-brain.js +352 -0
- package/lib/runner-command.js +10 -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 +108 -29
- package/lib/task-proof.js +23 -1
- 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
|
@@ -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', '
|
|
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 (!
|
|
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 || !
|
|
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
|
|
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
|
|
193
|
-
const
|
|
194
|
-
if (!
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
if (!
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
||
|
|
434
|
+
|| isAllowedAtrisVerifyArtifact(argv)
|
|
435
|
+
|| isAllowedGitDiffCheck(argv)
|
|
436
|
+
|| isAllowedPythonCommand(argv);
|
|
221
437
|
if (!allowed) return { ok: false, reason: 'verify_command_not_allowed' };
|
|
222
|
-
return {
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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 (
|
|
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)
|
|
321
|
-
|
|
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 (
|
|
640
|
+
if (requiresStrictVerify) {
|
|
348
641
|
const verify = metadata.verify;
|
|
349
642
|
if (!verify) return strictVerifyMissingResult(ref);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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:
|
|
655
|
+
reason: requiresStrictVerify ? 'certified_strict_verify' : 'certified_independent_review',
|
|
361
656
|
passes,
|
|
362
657
|
actors: [...actors],
|
|
363
658
|
proof,
|
|
364
|
-
policy:
|
|
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
|
};
|