atris 3.31.0 → 3.32.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/README.md +3 -3
- package/atris/CLAUDE.md +8 -0
- package/atris.md +8 -0
- package/ax +94 -4
- package/bin/atris.js +148 -73
- package/commands/autoland.js +70 -10
- package/commands/autopilot-front.js +273 -0
- package/commands/init.js +21 -0
- package/commands/member.js +32 -0
- package/commands/mission.js +386 -30
- package/commands/run-front.js +144 -0
- package/commands/run.js +9 -6
- package/commands/sign.js +90 -0
- package/commands/task.js +207 -6
- package/commands/truth.js +2 -1
- package/lib/autoland.js +133 -25
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +4 -3
- package/lib/runner-command.js +32 -13
- package/lib/task-db.js +19 -2
- package/package.json +2 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// atris run: thin front door over the mission runtime loop.
|
|
2
|
+
// One bounded pursuit: start a mission from the objective (or resume the most
|
|
3
|
+
// logical runnable mission), tick it, complete on pass, then exit.
|
|
4
|
+
// The old plan→do→review loop lives on behind `atris run --legacy`.
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
9
|
+
|
|
10
|
+
const RUN_VALUE_FLAGS = new Set([
|
|
11
|
+
'--owner',
|
|
12
|
+
'--cadence',
|
|
13
|
+
'--minutes',
|
|
14
|
+
'--hours',
|
|
15
|
+
'--max-ticks',
|
|
16
|
+
'--max-wall',
|
|
17
|
+
'--runner',
|
|
18
|
+
'--runner-bin',
|
|
19
|
+
'--runner-template',
|
|
20
|
+
'--runner-model',
|
|
21
|
+
'--runner-profile',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function readValueFlag(args, name) {
|
|
25
|
+
const index = args.indexOf(name);
|
|
26
|
+
return index >= 0 && index + 1 < args.length ? args[index + 1] : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function positiveNumber(value) {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runBudgetSeconds(args = []) {
|
|
35
|
+
const hours = positiveNumber(readValueFlag(args, '--hours'));
|
|
36
|
+
if (hours) return Math.round(hours * 3600);
|
|
37
|
+
const minutes = positiveNumber(readValueFlag(args, '--minutes'));
|
|
38
|
+
if (minutes) return Math.round(minutes * 60);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runObjective(args = []) {
|
|
43
|
+
const words = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const arg = args[i];
|
|
46
|
+
if (RUN_VALUE_FLAGS.has(arg)) { i++; continue; }
|
|
47
|
+
if (arg.startsWith('-')) continue;
|
|
48
|
+
words.push(arg);
|
|
49
|
+
}
|
|
50
|
+
return words.join(' ').trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Runners a spawned `mission run` can drive unattended.
|
|
54
|
+
const HEADLESS_DRIVABLE_RUNNERS = new Set(['claude', 'atris2']);
|
|
55
|
+
const CALLER_SESSION_RUNNERS = new Set(['codex_goal', 'caller_session', 'current_agent']);
|
|
56
|
+
|
|
57
|
+
function liveCodexSession(env = process.env) {
|
|
58
|
+
return Boolean(String(env.CODEX_THREAD_ID || '').trim());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runnerDrivableForFrontDoor(runner, options = {}) {
|
|
62
|
+
const normalized = String(runner || '').trim().toLowerCase();
|
|
63
|
+
if (HEADLESS_DRIVABLE_RUNNERS.has(normalized)) return true;
|
|
64
|
+
if (options.allowCallerSessionRunners && CALLER_SESSION_RUNNERS.has(normalized)) return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Most logical mission: a moving one beats a planned one, newer beats older.
|
|
69
|
+
// ready is excluded — it waits for review, not for another worker pass.
|
|
70
|
+
function pickRunnableMission(root = process.cwd(), missionMap = null, options = {}) {
|
|
71
|
+
let map = missionMap;
|
|
72
|
+
if (!map) {
|
|
73
|
+
try { map = require('./mission').loadMissionMap(root); } catch { return null; }
|
|
74
|
+
}
|
|
75
|
+
const candidates = Array.from(map.values())
|
|
76
|
+
.filter((mission) => mission && (mission.status === 'running' || mission.status === 'planning'))
|
|
77
|
+
.filter((mission) => runnerDrivableForFrontDoor(mission?.runner, options))
|
|
78
|
+
.filter((mission) => !/^decide and start the next useful mission after:/i.test(String(mission?.objective || '')))
|
|
79
|
+
.sort((a, b) => {
|
|
80
|
+
if (a.status !== b.status) return a.status === 'running' ? -1 : 1;
|
|
81
|
+
return String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || ''));
|
|
82
|
+
});
|
|
83
|
+
return candidates[0] || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runTickBudget(args, budgetSeconds) {
|
|
87
|
+
const explicit = positiveNumber(readValueFlag(args, '--max-ticks'));
|
|
88
|
+
if (explicit) return Math.floor(explicit);
|
|
89
|
+
// A timed run is a loop contract: keep picking the next useful move until
|
|
90
|
+
// the wall clock spends the budget (same rule as member run).
|
|
91
|
+
return budgetSeconds ? Math.max(4, Math.ceil(budgetSeconds / 300)) : 4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runMissionFront(args = []) {
|
|
95
|
+
const objective = runObjective(args);
|
|
96
|
+
const budgetSeconds = runBudgetSeconds(args);
|
|
97
|
+
const maxTicks = runTickBudget(args, budgetSeconds);
|
|
98
|
+
const maxWallSeconds = positiveNumber(readValueFlag(args, '--max-wall')) || budgetSeconds || 1200;
|
|
99
|
+
|
|
100
|
+
if (objective) {
|
|
101
|
+
const { runRuntimeMissionLoop } = require('../lib/mission-runtime-loop');
|
|
102
|
+
const result = await runRuntimeMissionLoop({
|
|
103
|
+
objective,
|
|
104
|
+
owner: readValueFlag(args, '--owner') || 'mission-lead',
|
|
105
|
+
cadence: readValueFlag(args, '--cadence') || '',
|
|
106
|
+
maxTicks,
|
|
107
|
+
maxWallSeconds,
|
|
108
|
+
noComplete: args.includes('--no-complete'),
|
|
109
|
+
}, {
|
|
110
|
+
cliPath: CLI_PATH,
|
|
111
|
+
onProgress: (line) => console.log(` ${line}`),
|
|
112
|
+
});
|
|
113
|
+
console.log(result.text || '');
|
|
114
|
+
return result.ok ? 0 : 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const mission = pickRunnableMission(process.cwd(), null, {
|
|
118
|
+
allowCallerSessionRunners: liveCodexSession(),
|
|
119
|
+
});
|
|
120
|
+
if (!mission) {
|
|
121
|
+
console.log('No objective given and no runnable mission found.');
|
|
122
|
+
console.log('Start one: atris run "<objective>" [--minutes N] [--owner <member>]');
|
|
123
|
+
console.log('Or keep going indefinitely: atris autopilot');
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
console.log(`Resuming mission ${mission.id} (${mission.status}): ${mission.objective}`);
|
|
127
|
+
const result = spawnSync(process.execPath, [
|
|
128
|
+
CLI_PATH, 'mission', 'run', mission.id,
|
|
129
|
+
'--max-ticks', String(maxTicks),
|
|
130
|
+
'--max-wall', String(maxWallSeconds),
|
|
131
|
+
'--complete-on-pass',
|
|
132
|
+
], { cwd: process.cwd(), stdio: 'inherit' });
|
|
133
|
+
return result.status || 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
runMissionFront,
|
|
138
|
+
pickRunnableMission,
|
|
139
|
+
liveCodexSession,
|
|
140
|
+
runnerDrivableForFrontDoor,
|
|
141
|
+
runObjective,
|
|
142
|
+
runBudgetSeconds,
|
|
143
|
+
runTickBudget,
|
|
144
|
+
};
|
package/commands/run.js
CHANGED
|
@@ -381,12 +381,15 @@ async function runAtris(options = {}) {
|
|
|
381
381
|
process.exit(1);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
// Check configured runner CLI is available.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
384
|
+
// Check configured runner CLI is available. Dry runs preview context only —
|
|
385
|
+
// they must work on machines (and CI) without the runner installed.
|
|
386
|
+
if (!dryRun) {
|
|
387
|
+
try {
|
|
388
|
+
execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' });
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error(runnerAvailabilityFailureMessage(err));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
console.log('');
|
package/commands/sign.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// atris sign: co-author trailer on every commit in an atris workspace.
|
|
5
|
+
// Installs a prepare-commit-msg hook that appends "Co-authored-by: Atris"
|
|
6
|
+
// when the repo has an atris/ folder — same credit line Claude and Cursor use.
|
|
7
|
+
// Idempotent and non-destructive: appends a marked block, removes only its own block.
|
|
8
|
+
|
|
9
|
+
const MARKER = '# atris co-author';
|
|
10
|
+
const TRAILER = 'Co-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>';
|
|
11
|
+
|
|
12
|
+
const HOOK_BLOCK = `
|
|
13
|
+
${MARKER}
|
|
14
|
+
if [ -d "$(git rev-parse --show-toplevel 2>/dev/null)/atris" ] && [ "$2" != "merge" ] && [ "$2" != "squash" ]; then
|
|
15
|
+
if ! grep -qi "^co-authored-by: atris" "$1"; then
|
|
16
|
+
printf '\\nCo-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>\\n' >> "$1"
|
|
17
|
+
fi
|
|
18
|
+
fi
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
function hookPathFor(root) {
|
|
22
|
+
return path.join(root, '.git', 'hooks', 'prepare-commit-msg');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Install the marked block into .git/hooks/prepare-commit-msg. Returns { hookPath, already }.
|
|
26
|
+
function installSignHook(root = process.cwd()) {
|
|
27
|
+
if (!fs.existsSync(path.join(root, '.git'))) throw new Error('not a git repo (no .git here)');
|
|
28
|
+
const hookPath = hookPathFor(root);
|
|
29
|
+
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
30
|
+
let content = '';
|
|
31
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
32
|
+
if (content.includes(MARKER)) return { hookPath, already: true };
|
|
33
|
+
if (!content) content = '#!/bin/sh\n';
|
|
34
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
35
|
+
content += HOOK_BLOCK;
|
|
36
|
+
fs.writeFileSync(hookPath, content);
|
|
37
|
+
fs.chmodSync(hookPath, 0o755);
|
|
38
|
+
return { hookPath, already: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Remove only our marked block; leave the rest of the hook untouched.
|
|
42
|
+
function removeSignHook(root = process.cwd()) {
|
|
43
|
+
const hookPath = hookPathFor(root);
|
|
44
|
+
let content = '';
|
|
45
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch { return { hookPath, removed: false }; }
|
|
46
|
+
if (!content.includes(MARKER)) return { hookPath, removed: false };
|
|
47
|
+
const cleaned = content.replace(HOOK_BLOCK, '');
|
|
48
|
+
if (cleaned.trim() === '#!/bin/sh') {
|
|
49
|
+
fs.unlinkSync(hookPath);
|
|
50
|
+
} else {
|
|
51
|
+
fs.writeFileSync(hookPath, cleaned);
|
|
52
|
+
}
|
|
53
|
+
return { hookPath, removed: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function signStatus(root = process.cwd()) {
|
|
57
|
+
const hookPath = hookPathFor(root);
|
|
58
|
+
let content = '';
|
|
59
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
60
|
+
return { hookPath, installed: content.includes(MARKER) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function signCommand(sub) {
|
|
64
|
+
const rel = (p) => path.relative(process.cwd(), p);
|
|
65
|
+
if (sub === 'off' || sub === 'remove') {
|
|
66
|
+
const { hookPath, removed } = removeSignHook();
|
|
67
|
+
console.log(removed
|
|
68
|
+
? `\n ✓ atris co-author removed: ${rel(hookPath)}\n`
|
|
69
|
+
: `\n nothing to remove — atris co-author is not installed\n`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (sub === 'status') {
|
|
73
|
+
const { hookPath, installed } = signStatus();
|
|
74
|
+
console.log(installed
|
|
75
|
+
? `\n ✓ atris co-author is on: ${rel(hookPath)}\n every commit gets: ${TRAILER}\n`
|
|
76
|
+
: `\n atris co-author is off — run 'atris sign' to turn it on\n`);
|
|
77
|
+
return installed ? 0 : 1;
|
|
78
|
+
}
|
|
79
|
+
if (sub && sub !== 'on' && sub !== 'install') {
|
|
80
|
+
console.log(`\n atris sign — co-author trailer on every commit\n\n atris sign install the prepare-commit-msg hook\n atris sign off remove it\n atris sign status check whether it's installed\n\n While installed, any commit in a repo with an atris/ folder gets:\n ${TRAILER}\n`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const { hookPath, already } = installSignHook();
|
|
84
|
+
console.log(already
|
|
85
|
+
? `\n already installed: ${rel(hookPath)}\n`
|
|
86
|
+
: `\n ✓ atris co-author installed: ${rel(hookPath)}\n every commit here now gets: ${TRAILER}\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { signCommand, installSignHook, removeSignHook, signStatus, TRAILER };
|
package/commands/task.js
CHANGED
|
@@ -8,13 +8,14 @@ const http = require('http');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const { taskProofState, buildVerifiedProof } = require('../lib/task-proof');
|
|
11
|
-
const { evaluateAutoAccept, parseVerifyCommand } = require('../lib/auto-accept-certified');
|
|
11
|
+
const { evaluateAutoAccept, parseVerifyCommand, runVerifyCommand, DENIED_TAGS } = require('../lib/auto-accept-certified');
|
|
12
12
|
const { extractReceiptEvidence } = require('../lib/receipt-evidence');
|
|
13
13
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
14
14
|
const {
|
|
15
15
|
normalizeOwnerSlug,
|
|
16
16
|
resolveFunctionalOwner: resolveFunctionalTaskOwner,
|
|
17
17
|
} = require('../lib/functional-owner');
|
|
18
|
+
const { operatorReady, hasAgentJargon } = require('./autoland');
|
|
18
19
|
|
|
19
20
|
const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
|
|
20
21
|
|| process.env.USER
|
|
@@ -90,6 +91,14 @@ function getTaskDb() {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
function warnIfTaskTitleNeedsOperatorWhy(title) {
|
|
95
|
+
const text = String(title || '').trim();
|
|
96
|
+
if (!text || operatorReady(text)) return null;
|
|
97
|
+
const warning = 'Warning: add the why in plain words to this task title: what it buys or costs, who benefits, and no flags or identifiers.';
|
|
98
|
+
console.error(warning);
|
|
99
|
+
return warning;
|
|
100
|
+
}
|
|
101
|
+
|
|
93
102
|
function taskUsageText() {
|
|
94
103
|
return `
|
|
95
104
|
atris task - durable local task state (SQLite, gitignored)
|
|
@@ -113,6 +122,8 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
113
122
|
atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
|
|
114
123
|
atris task accept <id> [--proof "..."] [--public]
|
|
115
124
|
Human accepts proof, marks done; --public also publishes AgentXP
|
|
125
|
+
atris task certify-verified [--dry-run] [--limit <n>] [--as <actor>]
|
|
126
|
+
Re-run the runnable check named in each Review proof as a second actor; passing rows certify (denied lanes and check-less rows wait for a human)
|
|
116
127
|
atris task auto-accept-certified --dry-run [--strict-verify] [--limit <n>]
|
|
117
128
|
Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
|
|
118
129
|
atris task revise <id> --note "..." Send reviewed work back to Do
|
|
@@ -374,13 +385,36 @@ function textFlag(args, names) {
|
|
|
374
385
|
|
|
375
386
|
function landingFlags(args) {
|
|
376
387
|
return {
|
|
377
|
-
happened: textFlag(args, ['--happened', '--what-happened']),
|
|
388
|
+
happened: textFlag(args, ['--landing', '--happened', '--what-happened']),
|
|
378
389
|
checked: textFlag(args, ['--checked', '--how-checked']),
|
|
379
390
|
tested: textFlag(args, ['--tested', '--what-tested']),
|
|
380
391
|
decision: textFlag(args, ['--decision', '--acceptance']),
|
|
381
392
|
};
|
|
382
393
|
}
|
|
383
394
|
|
|
395
|
+
function normalizedLandingSentence(value) {
|
|
396
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function defaultLandingSentenceForTitle(title) {
|
|
400
|
+
return `Completed: ${String(title || '').trim()}.`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function landingNeedsDayOnePm(sentence, title) {
|
|
404
|
+
const text = normalizedLandingSentence(sentence);
|
|
405
|
+
if (!text) return true;
|
|
406
|
+
if (text.toLowerCase() === normalizedLandingSentence(defaultLandingSentenceForTitle(title)).toLowerCase()) return true;
|
|
407
|
+
return hasAgentJargon(text) || !operatorReady(text);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function warnIfLandingNeedsDayOnePm(landing, title) {
|
|
411
|
+
const sentence = landing && typeof landing === 'object' ? landing.happened : '';
|
|
412
|
+
if (!landingNeedsDayOnePm(sentence, title)) return null;
|
|
413
|
+
const warning = 'Advisory: add --landing with one capability sentence a day-one PM could read, with the result in plain words and no flags or identifiers.';
|
|
414
|
+
console.error(warning);
|
|
415
|
+
return warning;
|
|
416
|
+
}
|
|
417
|
+
|
|
384
418
|
function numericFlag(args, name) {
|
|
385
419
|
const value = flag(args, name);
|
|
386
420
|
if (value === null || value === true || value === undefined) return null;
|
|
@@ -597,18 +631,19 @@ function buildPendingReviewChatPredicate(taskDb, db, workspaceRoot) {
|
|
|
597
631
|
function createReviewNextTask(taskDb, db, currentTask, title) {
|
|
598
632
|
const nextTitle = String(title || '').trim();
|
|
599
633
|
if (!nextTitle) return null;
|
|
634
|
+
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(nextTitle);
|
|
600
635
|
const currentMetadata = currentTask && currentTask.metadata && typeof currentTask.metadata === 'object'
|
|
601
636
|
? currentTask.metadata
|
|
602
637
|
: {};
|
|
603
638
|
const existing = findExistingReviewNextTask(taskDb, db, currentTask, nextTitle);
|
|
604
|
-
if (existing) return { id: existing.id, inserted: false };
|
|
639
|
+
if (existing) return { id: existing.id, inserted: false, operator_title_warning: operatorTitleWarning };
|
|
605
640
|
const goalId = currentMetadata.goal_id || currentMetadata.goalId || null;
|
|
606
641
|
const parentId = currentTask && currentTask.id || null;
|
|
607
642
|
const sourceKey = parentId && typeof taskDb.sourceKey === 'function'
|
|
608
643
|
? taskDb.sourceKey(`task_review_next:${parentId}`, nextTitle)
|
|
609
644
|
: null;
|
|
610
645
|
try {
|
|
611
|
-
|
|
646
|
+
const created = taskDb.addTask(db, {
|
|
612
647
|
title: nextTitle,
|
|
613
648
|
tag: currentTask && currentTask.tag || null,
|
|
614
649
|
workspaceRoot: taskDb.workspaceRoot(),
|
|
@@ -619,10 +654,11 @@ function createReviewNextTask(taskDb, db, currentTask, title) {
|
|
|
619
654
|
source: 'task_review_next',
|
|
620
655
|
},
|
|
621
656
|
});
|
|
657
|
+
return { ...created, operator_title_warning: operatorTitleWarning };
|
|
622
658
|
} catch (error) {
|
|
623
659
|
if (sourceKey && /constraint|unique/i.test(String(error && (error.code || error.message) || error))) {
|
|
624
660
|
const racedExisting = findExistingReviewNextTask(taskDb, db, currentTask, nextTitle);
|
|
625
|
-
if (racedExisting) return { id: racedExisting.id, inserted: false };
|
|
661
|
+
if (racedExisting) return { id: racedExisting.id, inserted: false, operator_title_warning: operatorTitleWarning };
|
|
626
662
|
}
|
|
627
663
|
throw error;
|
|
628
664
|
}
|
|
@@ -4885,6 +4921,7 @@ function cmdAdd(args) {
|
|
|
4885
4921
|
const taskDb = getTaskDb();
|
|
4886
4922
|
const db = taskDb.open();
|
|
4887
4923
|
const ws = taskDb.workspaceRoot();
|
|
4924
|
+
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
|
|
4888
4925
|
// Generation throttle — the named root cause is generation > human-review rate. An AGENT cannot keep
|
|
4889
4926
|
// minting tasks while a wall of certified-but-unaccepted work waits; that treadmill is what accept-group
|
|
4890
4927
|
// only drains. Humans and --force bypass. Closes the tap instead of just enlarging the bucket.
|
|
@@ -4910,6 +4947,7 @@ function cmdAdd(args) {
|
|
|
4910
4947
|
action: 'created',
|
|
4911
4948
|
task_id: result.id,
|
|
4912
4949
|
inserted: result.inserted !== false,
|
|
4950
|
+
operator_title_warning: operatorTitleWarning,
|
|
4913
4951
|
projection_path: outPath,
|
|
4914
4952
|
task,
|
|
4915
4953
|
});
|
|
@@ -4952,6 +4990,7 @@ function cmdDelegate(args) {
|
|
|
4952
4990
|
const taskDb = getTaskDb();
|
|
4953
4991
|
const db = taskDb.open();
|
|
4954
4992
|
const ws = taskDb.workspaceRoot();
|
|
4993
|
+
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
|
|
4955
4994
|
const ownerResolution = resolveFunctionalTaskOwner({
|
|
4956
4995
|
requestedOwner: requestedOwner && requestedOwner !== true ? requestedOwner : null,
|
|
4957
4996
|
title,
|
|
@@ -5007,6 +5046,7 @@ function cmdDelegate(args) {
|
|
|
5007
5046
|
executed_by: executedBy || null,
|
|
5008
5047
|
via,
|
|
5009
5048
|
handoff,
|
|
5049
|
+
operator_title_warning: operatorTitleWarning,
|
|
5010
5050
|
projection_path: outPath,
|
|
5011
5051
|
task,
|
|
5012
5052
|
});
|
|
@@ -5271,6 +5311,7 @@ function buildEndgameTaskSeed({ slug, horizon, owner }) {
|
|
|
5271
5311
|
|
|
5272
5312
|
function createEndgameSeedTask(taskDb, db, seed, owner) {
|
|
5273
5313
|
const taskOwner = String(owner || DEFAULT_OWNER);
|
|
5314
|
+
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(seed.title);
|
|
5274
5315
|
const result = taskDb.addTask(db, {
|
|
5275
5316
|
title: seed.title,
|
|
5276
5317
|
tag: seed.tag,
|
|
@@ -5294,6 +5335,7 @@ function createEndgameSeedTask(taskDb, db, seed, owner) {
|
|
|
5294
5335
|
ok: true,
|
|
5295
5336
|
task_id: result.id,
|
|
5296
5337
|
inserted: result.inserted !== false,
|
|
5338
|
+
operator_title_warning: operatorTitleWarning,
|
|
5297
5339
|
note_version: note.event.version,
|
|
5298
5340
|
};
|
|
5299
5341
|
}
|
|
@@ -5367,6 +5409,7 @@ function cmdNext(args) {
|
|
|
5367
5409
|
handoff,
|
|
5368
5410
|
next_agent_action: nextAgentAction,
|
|
5369
5411
|
note_version: created.note_version,
|
|
5412
|
+
operator_title_warning: created.operator_title_warning || null,
|
|
5370
5413
|
task: createdTask,
|
|
5371
5414
|
review_task: reviewTask,
|
|
5372
5415
|
});
|
|
@@ -5445,6 +5488,9 @@ function cmdNext(args) {
|
|
|
5445
5488
|
}
|
|
5446
5489
|
console.log(`next ${taskRef(compactTaskFromProjection(projection, open[0].id))} @${owner}`);
|
|
5447
5490
|
console.log(open[0].title);
|
|
5491
|
+
const ref = taskRef(compactTaskFromProjection(projection, open[0].id));
|
|
5492
|
+
const { printOperatorNext } = require('../lib/operator-next');
|
|
5493
|
+
printOperatorNext(`atris task step ${ref}`);
|
|
5448
5494
|
}
|
|
5449
5495
|
|
|
5450
5496
|
function continueWorkForReviewTask(taskDb, db, taskId, { owner = DEFAULT_OWNER } = {}) {
|
|
@@ -5488,6 +5534,7 @@ function continueWorkForReviewTask(taskDb, db, taskId, { owner = DEFAULT_OWNER }
|
|
|
5488
5534
|
parent_task_id: taskId,
|
|
5489
5535
|
next_task_id: nextCreated ? nextCreated.id : null,
|
|
5490
5536
|
created: Boolean(nextCreated && nextCreated.inserted !== false),
|
|
5537
|
+
operator_title_warning: nextCreated ? nextCreated.operator_title_warning || null : null,
|
|
5491
5538
|
owner: String(owner || DEFAULT_OWNER),
|
|
5492
5539
|
projection_path: outPath,
|
|
5493
5540
|
parent,
|
|
@@ -7549,6 +7596,7 @@ function cmdFinish(args) {
|
|
|
7549
7596
|
const currentTask = taskDb.getTask(db, taskId);
|
|
7550
7597
|
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
7551
7598
|
const proof = proofFlagValue(args);
|
|
7599
|
+
const landing = landingFlags(args);
|
|
7552
7600
|
const failed = hasFlag(args, '--failed');
|
|
7553
7601
|
const hasReview = hasFlag(args, '--review') || flag(args, '--lesson') || flag(args, '--next') || flag(args, '--proof') || flag(args, '--reward');
|
|
7554
7602
|
if (agentProofOnlyMode() && !failed) {
|
|
@@ -7585,6 +7633,7 @@ function cmdFinish(args) {
|
|
|
7585
7633
|
process.exit(1);
|
|
7586
7634
|
}
|
|
7587
7635
|
if (hasReview) {
|
|
7636
|
+
const landingAdvisory = !failed ? warnIfLandingNeedsDayOnePm(landing, currentTask && currentTask.title) : null;
|
|
7588
7637
|
const result = taskDb.reviewTask(db, {
|
|
7589
7638
|
id: taskId,
|
|
7590
7639
|
actor,
|
|
@@ -7593,6 +7642,7 @@ function cmdFinish(args) {
|
|
|
7593
7642
|
nextTask: typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '',
|
|
7594
7643
|
proof,
|
|
7595
7644
|
careerXpEligible: false,
|
|
7645
|
+
landing,
|
|
7596
7646
|
});
|
|
7597
7647
|
const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
|
|
7598
7648
|
const xpProjection = refreshCareerXpAfterReview(result);
|
|
@@ -7606,6 +7656,7 @@ function cmdFinish(args) {
|
|
|
7606
7656
|
reward: result.episode.reward.value,
|
|
7607
7657
|
episode: result.episode,
|
|
7608
7658
|
xp_projection: xpProjection,
|
|
7659
|
+
landing_advisory: landingAdvisory,
|
|
7609
7660
|
next_task_id: nextCreated ? nextCreated.id : null,
|
|
7610
7661
|
projection_path: outPath,
|
|
7611
7662
|
task: compactTaskFromProjection(projection, taskId),
|
|
@@ -7700,6 +7751,7 @@ function cmdReady(args) {
|
|
|
7700
7751
|
console.error(`ready failed: ${result.reason}`);
|
|
7701
7752
|
process.exit(1);
|
|
7702
7753
|
}
|
|
7754
|
+
const landingAdvisory = warnIfLandingNeedsDayOnePm(landing, result.row && result.row.title);
|
|
7703
7755
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
7704
7756
|
const agentCertified = result.event.payload.agent_certified === true;
|
|
7705
7757
|
const projectionTask = taskFromProjection(projection, taskId)
|
|
@@ -7741,6 +7793,7 @@ function cmdReady(args) {
|
|
|
7741
7793
|
agent_certified: agentCertified,
|
|
7742
7794
|
handoff,
|
|
7743
7795
|
result_trace: resultTrace,
|
|
7796
|
+
landing_advisory: landingAdvisory,
|
|
7744
7797
|
...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
|
|
7745
7798
|
projection_path: outPath,
|
|
7746
7799
|
task: compactTaskFromProjection(projection, taskId),
|
|
@@ -7909,6 +7962,151 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
|
|
|
7909
7962
|
return { ok: true, reviewed };
|
|
7910
7963
|
}
|
|
7911
7964
|
|
|
7965
|
+
// Second-actor certification for proof-backed Review rows whose proof names a
|
|
7966
|
+
// runnable, allowlisted check. Re-running that check as a distinct actor IS
|
|
7967
|
+
// the independent verification the certification gate asks for — the row
|
|
7968
|
+
// becomes certified and autoland can land it on the same heartbeat. Rows in
|
|
7969
|
+
// denied lanes, without a runnable check, or already certified keep their
|
|
7970
|
+
// existing paths (review chats, human accept).
|
|
7971
|
+
function certifyVerifyCandidate(task) {
|
|
7972
|
+
const metadata = task.metadata || {};
|
|
7973
|
+
const review = task.review || {};
|
|
7974
|
+
const recorded = typeof metadata.verify === 'string' && metadata.verify.trim() ? [metadata.verify.trim()] : [];
|
|
7975
|
+
const proof = String(review.proof || metadata.latest_agent_proof || '');
|
|
7976
|
+
for (const raw of [...recorded, ...taskReviewEvidenceCommands(proof)]) {
|
|
7977
|
+
// The evidence extractor can keep trailing prose ("git diff --check. Evidence
|
|
7978
|
+
// inspected: ..."); try the full clause, then the first sentence of it.
|
|
7979
|
+
for (const candidate of [raw, raw.split(/\.(?:\s|$)/)[0]].map((c) => String(c || '').trim())) {
|
|
7980
|
+
if (candidate && parseVerifyCommand(candidate).ok) return candidate;
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
return null;
|
|
7984
|
+
}
|
|
7985
|
+
|
|
7986
|
+
function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
|
|
7987
|
+
const row = taskDb.getTask(db, taskId);
|
|
7988
|
+
if (!row) return;
|
|
7989
|
+
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
7990
|
+
metadata.verify = metadata.verify || verify;
|
|
7991
|
+
metadata.certified_verified_at = new Date().toISOString();
|
|
7992
|
+
metadata.certified_verified_by = actor;
|
|
7993
|
+
db.prepare(`
|
|
7994
|
+
UPDATE tasks
|
|
7995
|
+
SET metadata = ?,
|
|
7996
|
+
updated_at = ?
|
|
7997
|
+
WHERE id = ?
|
|
7998
|
+
`).run(JSON.stringify(metadata), Date.now(), taskId);
|
|
7999
|
+
}
|
|
8000
|
+
|
|
8001
|
+
function cmdCertifyVerified(args) {
|
|
8002
|
+
const dryRun = hasFlag(args, '--dry-run');
|
|
8003
|
+
const asJson = wantsJson(args);
|
|
8004
|
+
const actor = String(flag(args, '--as') || 'autoland-verifier');
|
|
8005
|
+
const limitRaw = flag(args, '--limit');
|
|
8006
|
+
const max = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 6) : 6;
|
|
8007
|
+
|
|
8008
|
+
const taskDb = getTaskDb();
|
|
8009
|
+
const db = taskDb.open();
|
|
8010
|
+
const { projection } = writeDefaultProjection(taskDb, db);
|
|
8011
|
+
const candidates = (projection.tasks || [])
|
|
8012
|
+
.filter((t) => t && t.status === 'review')
|
|
8013
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
|
|
8014
|
+
.slice(0, max);
|
|
8015
|
+
const results = [];
|
|
8016
|
+
for (const item of candidates) {
|
|
8017
|
+
const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
|
|
8018
|
+
const task = fullProjection.tasks[0] || null;
|
|
8019
|
+
if (!task) {
|
|
8020
|
+
results.push({ ref: item.display_id || item.id, action: 'skipped', reason: 'task_not_found' });
|
|
8021
|
+
continue;
|
|
8022
|
+
}
|
|
8023
|
+
const ref = task.display_id || task.legacy_ref || task.id;
|
|
8024
|
+
const metadata = task.metadata || {};
|
|
8025
|
+
const review = task.review || {};
|
|
8026
|
+
if (task.status !== 'review') {
|
|
8027
|
+
results.push({ ref, action: 'skipped', reason: 'not_in_review' });
|
|
8028
|
+
continue;
|
|
8029
|
+
}
|
|
8030
|
+
const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
|
|
8031
|
+
if (approval !== 'pending') {
|
|
8032
|
+
results.push({ ref, action: 'skipped', reason: `approval_${approval}` });
|
|
8033
|
+
continue;
|
|
8034
|
+
}
|
|
8035
|
+
const tag = String(task.tag || '').toLowerCase();
|
|
8036
|
+
if (DENIED_TAGS.has(tag)) {
|
|
8037
|
+
results.push({ ref, action: 'skipped', reason: `denied_tag_${tag}` });
|
|
8038
|
+
continue;
|
|
8039
|
+
}
|
|
8040
|
+
// Skip only rows the accept lane can already land, or rows blocked by
|
|
8041
|
+
// something an executed second-actor check cannot cure. A row with two
|
|
8042
|
+
// passes from ONE actor is exactly what this command exists to cure —
|
|
8043
|
+
// "certified" alone is not landable.
|
|
8044
|
+
const evaluation = evaluateAutoAccept(task, { strictVerify: false });
|
|
8045
|
+
if (evaluation.eligible) {
|
|
8046
|
+
results.push({ ref, action: 'skipped', reason: 'already_landable' });
|
|
8047
|
+
continue;
|
|
8048
|
+
}
|
|
8049
|
+
const curable = ['not_agent_certified', 'needs_second_reviewer_or_third_pass', 'insufficient_review_passes'];
|
|
8050
|
+
if (!curable.includes(evaluation.reason)) {
|
|
8051
|
+
results.push({ ref, action: 'skipped', reason: evaluation.reason });
|
|
8052
|
+
continue;
|
|
8053
|
+
}
|
|
8054
|
+
const verify = certifyVerifyCandidate(task);
|
|
8055
|
+
if (!verify) {
|
|
8056
|
+
results.push({ ref, action: 'skipped', reason: 'no_runnable_check_in_proof' });
|
|
8057
|
+
continue;
|
|
8058
|
+
}
|
|
8059
|
+
if (dryRun) {
|
|
8060
|
+
results.push({ ref, action: 'would_certify', verify });
|
|
8061
|
+
continue;
|
|
8062
|
+
}
|
|
8063
|
+
const run = runVerifyCommand(verify, task.workspace_root || process.cwd());
|
|
8064
|
+
if (!run.ok) {
|
|
8065
|
+
results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
|
|
8066
|
+
continue;
|
|
8067
|
+
}
|
|
8068
|
+
const builderProof = String(review.proof || metadata.latest_agent_proof || '').slice(0, 200);
|
|
8069
|
+
const readied = taskDb.readyTask(db, {
|
|
8070
|
+
id: task.id,
|
|
8071
|
+
actor,
|
|
8072
|
+
proof: `Second-actor check: \`${verify}\` re-run by ${actor}, exited 0. Builder proof inspected: ${builderProof}`,
|
|
8073
|
+
lesson: '',
|
|
8074
|
+
nextTask: '',
|
|
8075
|
+
});
|
|
8076
|
+
if (!readied.ready) {
|
|
8077
|
+
results.push({ ref, action: 'certify_failed', reason: readied.reason, verify });
|
|
8078
|
+
continue;
|
|
8079
|
+
}
|
|
8080
|
+
stampCertifyVerifyMetadata(taskDb, db, task.id, actor, verify);
|
|
8081
|
+
const certifiedNow = readied.event?.payload?.agent_certified === true;
|
|
8082
|
+
results.push({ ref, action: certifiedNow ? 'certified' : 'pass_recorded', verify });
|
|
8083
|
+
}
|
|
8084
|
+
const { outPath } = writeDefaultProjection(taskDb, db);
|
|
8085
|
+
const certified = results.filter((r) => r.action === 'certified').length;
|
|
8086
|
+
const payload = {
|
|
8087
|
+
ok: true,
|
|
8088
|
+
action: dryRun ? 'certify_verified_dry_run' : 'certify_verified',
|
|
8089
|
+
actor,
|
|
8090
|
+
certified,
|
|
8091
|
+
would_certify: results.filter((r) => r.action === 'would_certify').length,
|
|
8092
|
+
skipped: results.filter((r) => r.action === 'skipped').length,
|
|
8093
|
+
failed: results.filter((r) => r.action === 'verify_failed' || r.action === 'certify_failed').length,
|
|
8094
|
+
results,
|
|
8095
|
+
projection_path: outPath,
|
|
8096
|
+
};
|
|
8097
|
+
if (asJson) {
|
|
8098
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
8099
|
+
} else if (results.length === 0) {
|
|
8100
|
+
console.log('certify-verified: no review rows to consider.');
|
|
8101
|
+
} else {
|
|
8102
|
+
for (const r of results) {
|
|
8103
|
+
console.log(`${r.action.padEnd(14)} ${r.ref}${r.verify ? ` \`${r.verify}\`` : ''}${r.reason ? ` (${r.reason})` : ''}`);
|
|
8104
|
+
}
|
|
8105
|
+
console.log(`certified ${certified}; humans keep denied lanes and rows without a runnable check.`);
|
|
8106
|
+
}
|
|
8107
|
+
return payload;
|
|
8108
|
+
}
|
|
8109
|
+
|
|
7912
8110
|
function cmdAutoAcceptCertified(args) {
|
|
7913
8111
|
const dryRun = hasFlag(args, '--dry-run');
|
|
7914
8112
|
const strictVerify = hasFlag(args, '--strict-verify');
|
|
@@ -9256,13 +9454,14 @@ async function handleTaskApi(req, res, taskDb, db) {
|
|
|
9256
9454
|
const body = await readJsonBody(req);
|
|
9257
9455
|
const title = String(body.title || '').trim();
|
|
9258
9456
|
if (!title) return sendJson(res, 400, { ok: false, reason: 'missing_title', detail: 'title required' });
|
|
9457
|
+
const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
|
|
9259
9458
|
const result = taskDb.addTask(db, {
|
|
9260
9459
|
title,
|
|
9261
9460
|
tag: body.tag ? String(body.tag) : 'tasks',
|
|
9262
9461
|
workspaceRoot: taskDb.workspaceRoot(),
|
|
9263
9462
|
});
|
|
9264
9463
|
const { projection, outPath } = writeDefaultProjection(taskDb, db);
|
|
9265
|
-
return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, projection_path: outPath, task: taskFromProjection(projection, result.id) });
|
|
9464
|
+
return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, operator_title_warning: operatorTitleWarning, projection_path: outPath, task: taskFromProjection(projection, result.id) });
|
|
9266
9465
|
}
|
|
9267
9466
|
if (req.method === 'POST' && url.pathname === '/api/tasks/clear-plan') {
|
|
9268
9467
|
const body = await readJsonBody(req);
|
|
@@ -9769,6 +9968,8 @@ async function run(args) {
|
|
|
9769
9968
|
case 'auto-accept-certified':
|
|
9770
9969
|
case 'auto-accept':
|
|
9771
9970
|
return cmdAutoAcceptCertified(rest);
|
|
9971
|
+
case 'certify-verified':
|
|
9972
|
+
return cmdCertifyVerified(rest);
|
|
9772
9973
|
case 'accept-group':
|
|
9773
9974
|
return cmdAcceptGroup(rest);
|
|
9774
9975
|
case 'revise': return cmdRevise(rest);
|
package/commands/truth.js
CHANGED
|
@@ -38,7 +38,8 @@ function loadMissions(cwd) {
|
|
|
38
38
|
if (id) seen.set(id, rec);
|
|
39
39
|
} catch { /* skip bad lines */ }
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
// stopped = killed-on-purpose = a closed loop; only genuinely open states count
|
|
42
|
+
return [...seen.values()].filter((m) => m.status && !['complete', 'archived', 'stopped'].includes(m.status));
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function loadTaskCounts() {
|