atris 3.65.0 → 3.66.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/atris/skills/engines/SKILL.md +2 -2
- package/bin/atris.js +28 -2
- package/commands/autoland.js +62 -0
- package/commands/brain.js +4 -0
- package/commands/engine.js +23 -1
- package/commands/link.js +367 -0
- package/commands/loops.js +4 -1
- package/commands/member.js +8 -1
- package/commands/orb.js +1 -1
- package/commands/xp.js +2 -0
- package/commands/youtube.js +1 -1
- package/lib/auto-accept-certified.js +89 -9
- package/lib/cleanup-summary.js +129 -0
- package/lib/engine-ask.js +3 -2
- package/lib/engine-registry.js +203 -15
- package/lib/idle-review-sweep.js +130 -0
- package/lib/known-commands.js +1 -1
- package/lib/land-green.js +18 -4
- package/lib/member-result.js +303 -0
- package/lib/roster-models.js +1 -1
- package/lib/runner-command.js +4 -4
- package/lib/task-list-keeper.js +25 -4
- package/lib/trust-tiers.js +131 -14
- package/lib/verify-worktree-rewrite.js +115 -0
- package/lib/voice-gate.js +2 -0
- package/package.json +1 -1
- package/scripts/det/ytnotes +1 -1
package/lib/trust-tiers.js
CHANGED
|
@@ -7,6 +7,10 @@ const HISTORY_FILES = [
|
|
|
7
7
|
path.join('.atris', 'state', 'career_xp_receipts.jsonl'),
|
|
8
8
|
path.join('.atris', 'state', 'scorecards.jsonl'),
|
|
9
9
|
];
|
|
10
|
+
// Where a legacy history row's builder is recovered from: the row only names
|
|
11
|
+
// its task, and these name who built that task.
|
|
12
|
+
const EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
13
|
+
const PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
|
|
10
14
|
|
|
11
15
|
const PASS_OUTCOMES = new Set(['accepted', 'approved', 'done', 'pass', 'passed', 'success', 'succeeded']);
|
|
12
16
|
const FAIL_OUTCOMES = new Set(['bounced', 'fail', 'failed', 'rejected', 'revised', 'rework_requested']);
|
|
@@ -15,17 +19,34 @@ function normalizedActor(value) {
|
|
|
15
19
|
return String(value || '').trim().toLowerCase();
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// Trust belongs to whoever BUILT the work, never to whoever approved it.
|
|
23
|
+
// History rows record `actor` as the approving reviewer (one human on 1,029
|
|
24
|
+
// of 1,057 receipts in one busy project), so keying on it, or on fields the
|
|
25
|
+
// rows never carried, left every builder at zero history and "still earning
|
|
26
|
+
// trust" forever. `actor` is deliberately not read here.
|
|
27
|
+
function stampedBuilder(row) {
|
|
28
|
+
return normalizedActor(
|
|
29
|
+
row?.builder
|
|
30
|
+
|| row?.built_by
|
|
31
|
+
|| row?.claimed_by
|
|
32
|
+
|| row?.metadata?.built_by
|
|
33
|
+
|| row?.metadata?.executed_by,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rowTaskId(row) {
|
|
38
|
+
return String(row?.source_task_id || row?.task_id || '').trim();
|
|
20
39
|
}
|
|
21
40
|
|
|
22
41
|
function rowPassed(row) {
|
|
23
42
|
for (const value of [row?.passed, row?.verify_passed, row?.metadata?.verify_passed]) {
|
|
24
43
|
if (typeof value === 'boolean') return value;
|
|
25
44
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
for (const value of [row?.outcome, row?.status, row?.rl_label]) {
|
|
46
|
+
const outcome = String(value || '').trim().toLowerCase();
|
|
47
|
+
if (PASS_OUTCOMES.has(outcome)) return true;
|
|
48
|
+
if (FAIL_OUTCOMES.has(outcome)) return false;
|
|
49
|
+
}
|
|
29
50
|
return null;
|
|
30
51
|
}
|
|
31
52
|
|
|
@@ -37,6 +58,53 @@ function rowTime(row) {
|
|
|
37
58
|
return null;
|
|
38
59
|
}
|
|
39
60
|
|
|
61
|
+
function fileStamp(file) {
|
|
62
|
+
try {
|
|
63
|
+
const stat = fs.statSync(file);
|
|
64
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
65
|
+
} catch {
|
|
66
|
+
return 'missing';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function eachJsonLine(file, fn) {
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = fs.readFileSync(file, 'utf8');
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const line of text.split(/\r?\n/)) {
|
|
78
|
+
if (!line.trim()) continue;
|
|
79
|
+
try {
|
|
80
|
+
const row = JSON.parse(line);
|
|
81
|
+
if (row && typeof row === 'object' && !Array.isArray(row)) fn(row);
|
|
82
|
+
} catch {
|
|
83
|
+
// A torn line in the episode log only costs that one lookup.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// task id -> builder, from the episode log (every review snapshot carries the
|
|
89
|
+
// task's built_by / claimed_by) and the live projection.
|
|
90
|
+
function readTaskBuilders(root) {
|
|
91
|
+
const builders = new Map();
|
|
92
|
+
const remember = (taskId, task) => {
|
|
93
|
+
const id = String(taskId || '').trim();
|
|
94
|
+
const builder = normalizedActor(task?.metadata?.built_by || task?.claimed_by);
|
|
95
|
+
if (id && builder) builders.set(id, builder);
|
|
96
|
+
};
|
|
97
|
+
eachJsonLine(path.join(root, EPISODES_FILE), (episode) => remember(episode.task_id, episode.state));
|
|
98
|
+
try {
|
|
99
|
+
const projection = JSON.parse(fs.readFileSync(path.join(root, PROJECTION_FILE), 'utf8'));
|
|
100
|
+
const tasks = Array.isArray(projection) ? projection : projection?.tasks;
|
|
101
|
+
for (const task of Array.isArray(tasks) ? tasks : []) remember(task?.id, task);
|
|
102
|
+
} catch {
|
|
103
|
+
// No projection: episode builders still apply.
|
|
104
|
+
}
|
|
105
|
+
return builders;
|
|
106
|
+
}
|
|
107
|
+
|
|
40
108
|
function readHistory(root) {
|
|
41
109
|
const rows = [];
|
|
42
110
|
let sequence = 0;
|
|
@@ -62,16 +130,58 @@ function readHistory(root) {
|
|
|
62
130
|
return rows;
|
|
63
131
|
}
|
|
64
132
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
133
|
+
// One outcome per review episode: a receipt and a scorecard written for the
|
|
134
|
+
// same episode are the same verdict, not two.
|
|
135
|
+
function builderOutcomes(root) {
|
|
68
136
|
const history = readHistory(root);
|
|
69
|
-
if (!history) return
|
|
137
|
+
if (!history) return null;
|
|
138
|
+
let builders = null;
|
|
139
|
+
const byBuilder = new Map();
|
|
140
|
+
const seenEpisodes = new Set();
|
|
141
|
+
for (const entry of history) {
|
|
142
|
+
const passed = rowPassed(entry.row);
|
|
143
|
+
if (passed === null) continue;
|
|
144
|
+
const episode = String(entry.row.source_episode_id || '').trim();
|
|
145
|
+
if (episode) {
|
|
146
|
+
if (seenEpisodes.has(episode)) continue;
|
|
147
|
+
seenEpisodes.add(episode);
|
|
148
|
+
}
|
|
149
|
+
let builder = stampedBuilder(entry.row);
|
|
150
|
+
if (!builder && rowTaskId(entry.row)) {
|
|
151
|
+
if (!builders) builders = readTaskBuilders(root);
|
|
152
|
+
builder = builders.get(rowTaskId(entry.row)) || '';
|
|
153
|
+
}
|
|
154
|
+
if (!builder) continue;
|
|
155
|
+
if (!byBuilder.has(builder)) byBuilder.set(builder, []);
|
|
156
|
+
byBuilder.get(builder).push({ ...entry, passed });
|
|
157
|
+
}
|
|
158
|
+
return byBuilder;
|
|
159
|
+
}
|
|
70
160
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
161
|
+
// Called once per waiting task on every status and tick, against a 12 MB
|
|
162
|
+
// episode log; memoize per root until one of the files changes.
|
|
163
|
+
const outcomeCache = new Map();
|
|
164
|
+
|
|
165
|
+
function cachedBuilderOutcomes(root) {
|
|
166
|
+
const resolved = path.resolve(root);
|
|
167
|
+
const stamp = [...HISTORY_FILES, EPISODES_FILE, PROJECTION_FILE]
|
|
168
|
+
.map((relative) => fileStamp(path.join(resolved, relative)))
|
|
169
|
+
.join('|');
|
|
170
|
+
const hit = outcomeCache.get(resolved);
|
|
171
|
+
if (hit && hit.stamp === stamp) return hit.value;
|
|
172
|
+
const value = builderOutcomes(resolved);
|
|
173
|
+
outcomeCache.set(resolved, { stamp, value });
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function computeTrustTier(builder, root = process.cwd()) {
|
|
178
|
+
const target = normalizedActor(builder);
|
|
179
|
+
if (!target) return 'probation';
|
|
180
|
+
const byBuilder = cachedBuilderOutcomes(root);
|
|
181
|
+
if (!byBuilder) return 'probation';
|
|
182
|
+
|
|
183
|
+
const outcomes = (byBuilder.get(target) || [])
|
|
184
|
+
.slice()
|
|
75
185
|
.sort((a, b) => {
|
|
76
186
|
if (a.time !== null && b.time !== null && a.time !== b.time) return a.time - b.time;
|
|
77
187
|
if (a.time !== null && b.time === null) return 1;
|
|
@@ -87,4 +197,11 @@ function computeTrustTier(actor, root = process.cwd()) {
|
|
|
87
197
|
return 'probation';
|
|
88
198
|
}
|
|
89
199
|
|
|
90
|
-
|
|
200
|
+
// The builder of a task: the stamped builder first, then whoever claimed it.
|
|
201
|
+
// Never the approver.
|
|
202
|
+
function taskTrustSubject(task) {
|
|
203
|
+
const metadata = task?.metadata || {};
|
|
204
|
+
return normalizedActor(metadata.built_by || task?.claimed_by || metadata.executed_by) || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { computeTrustTier, taskTrustSubject };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Member agents build in temporary copies of the repo
|
|
4
|
+
// (<arena>/.agent-worktrees/<project>/<name>, older ones under
|
|
5
|
+
// <arena>/agent-work-trees/...) and record their proof check pointed at that
|
|
6
|
+
// copy: "cd <arena>/.agent-worktrees/<project>/x && node scripts/t.mjs".
|
|
7
|
+
// The landing gate only runs checks relative to the main checkout, so it
|
|
8
|
+
// refused 201 of 218 finished items in project-obelisk every hour. Rewrite
|
|
9
|
+
// such a check relative to the main checkout so the landing pass reruns it
|
|
10
|
+
// there, and say in plain words when it cannot (the file it runs only exists
|
|
11
|
+
// on the agent's branch).
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
function escapeRegex(text) {
|
|
17
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function arenaDirFor(root) {
|
|
21
|
+
let cursor = root;
|
|
22
|
+
while (cursor && cursor !== path.dirname(cursor)) {
|
|
23
|
+
if (path.basename(cursor) === 'arena') return cursor;
|
|
24
|
+
cursor = path.dirname(cursor);
|
|
25
|
+
}
|
|
26
|
+
return path.dirname(root);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function copyRootPattern(root) {
|
|
30
|
+
const arena = escapeRegex(arenaDirFor(root));
|
|
31
|
+
const project = escapeRegex(path.basename(root));
|
|
32
|
+
const name = '[A-Za-z0-9._-]+';
|
|
33
|
+
return `(?:${[
|
|
34
|
+
`${arena}/\\.agent-worktrees/${project}/${name}`,
|
|
35
|
+
`${arena}/agent-work-trees/${project}/${name}`,
|
|
36
|
+
`\\.\\./\\.agent-worktrees/${project}/${name}`,
|
|
37
|
+
escapeRegex(root),
|
|
38
|
+
].join('|')})(?![A-Za-z0-9._-])`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stripQuotes(token) {
|
|
42
|
+
return token.replace(/^'(.*)'$/, '$1').replace(/^"(.*)"$/, '$1');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Returns { command, rewritten, from }. `rewritten` is true only when the
|
|
46
|
+
// command named an agent copy (or the main checkout by absolute path) and
|
|
47
|
+
// now runs relative to the main checkout. A command that still names an
|
|
48
|
+
// agent copy after the rewrite is returned untouched with rewritten:false.
|
|
49
|
+
function rewriteWorktreeVerify(command, workspaceRoot) {
|
|
50
|
+
const original = String(command || '').trim();
|
|
51
|
+
const unchanged = { command: original, rewritten: false, from: null };
|
|
52
|
+
if (!original || !workspaceRoot) return unchanged;
|
|
53
|
+
const root = path.resolve(workspaceRoot);
|
|
54
|
+
const anyRoot = copyRootPattern(root);
|
|
55
|
+
if (!new RegExp(anyRoot).test(original)) return unchanged;
|
|
56
|
+
|
|
57
|
+
let cmd = original;
|
|
58
|
+
// ( cd X && cmd ) -> cd X && cmd
|
|
59
|
+
const paren = /^\((.*)\)$/s.exec(cmd);
|
|
60
|
+
if (paren) cmd = paren[1].trim();
|
|
61
|
+
// cd '<copy>[/sub]' && rest -> [cd sub && ]rest
|
|
62
|
+
const cd = new RegExp(`^cd\\s+(['"]?)${anyRoot}(/[^'"\\s]*)?\\1\\s+&&\\s+(.+)$`, 's').exec(cmd);
|
|
63
|
+
if (cd) {
|
|
64
|
+
const sub = (cd[2] || '').replace(/^\/+|\/+$/g, '');
|
|
65
|
+
cmd = sub ? `cd ${sub} && ${cd[3]}` : cd[3];
|
|
66
|
+
}
|
|
67
|
+
// npm --prefix <copy> run x -> npm run x
|
|
68
|
+
cmd = cmd.replace(new RegExp(`\\bnpm\\s+--prefix\\s+(['"]?)${anyRoot}/?\\1\\s+`, 'g'), 'npm ');
|
|
69
|
+
// '<copy>/a/b' -> a/b ; bare '<copy>' -> .
|
|
70
|
+
cmd = cmd.replace(new RegExp(`(['"]?)${anyRoot}/([^'"\\s]+)\\1`, 'g'), (_m, _q, rest) => rest);
|
|
71
|
+
cmd = cmd.replace(new RegExp(`(['"]?)${anyRoot}/?\\1(?=\\s|$)`, 'g'), '.');
|
|
72
|
+
// Only plain path quoting is stripped; quoted text with spaces stays put.
|
|
73
|
+
cmd = cmd.split(/(\s+)/).map((part) => (/^\s+$/.test(part) ? part : stripQuotes(part))).join('').trim();
|
|
74
|
+
if (new RegExp(anyRoot).test(cmd) || /\.agent-worktrees|agent-work-trees/.test(cmd)) return unchanged;
|
|
75
|
+
return { command: cmd, rewritten: true, from: original };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Files a parsed check step reads by relative path: its folder, the script
|
|
79
|
+
// it runs, the file it tests. Used to tell "this check cannot run on main
|
|
80
|
+
// because its file only exists on the agent's branch" from a real failure.
|
|
81
|
+
function stepReferencedPaths(step) {
|
|
82
|
+
const argv = Array.isArray(step?.argv) ? step.argv : [];
|
|
83
|
+
const paths = [];
|
|
84
|
+
if (step?.cwd) paths.push({ path: step.cwd, kind: 'dir' });
|
|
85
|
+
const [bin, first, second] = argv;
|
|
86
|
+
const looksLikePath = (token) => Boolean(token) && !String(token).startsWith('-') && /[/.]/.test(String(token));
|
|
87
|
+
if (bin === 'node' && first === '--check' && looksLikePath(second)) paths.push({ path: second, kind: 'file' });
|
|
88
|
+
else if (bin === 'node' && first === '--test') {
|
|
89
|
+
for (const token of argv.slice(2)) {
|
|
90
|
+
if (looksLikePath(token) && !String(token).includes('*') && /\.[cm]?[jt]s$/.test(token)) paths.push({ path: token, kind: 'file' });
|
|
91
|
+
}
|
|
92
|
+
} else if (bin === 'node' && looksLikePath(first)) paths.push({ path: first, kind: 'file' });
|
|
93
|
+
if ((bin === 'python' || bin === 'python3' || /venv\/bin\/python3?$/.test(String(bin || ''))) && looksLikePath(first) && first !== '-m') {
|
|
94
|
+
paths.push({ path: first, kind: 'file' });
|
|
95
|
+
}
|
|
96
|
+
if (bin === 'test' && (first === '-f' || first === '-s') && looksLikePath(second)) paths.push({ path: second, kind: 'file' });
|
|
97
|
+
return paths;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// First referenced path missing from the main checkout, or null.
|
|
101
|
+
function missingOnMain(steps, workspaceRoot) {
|
|
102
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
103
|
+
for (const step of steps || []) {
|
|
104
|
+
const base = step?.cwd ? path.resolve(root, step.cwd) : root;
|
|
105
|
+
for (const ref of stepReferencedPaths(step)) {
|
|
106
|
+
const target = ref.kind === 'dir' ? path.resolve(root, ref.path) : path.resolve(base, ref.path);
|
|
107
|
+
if (!fs.existsSync(target)) {
|
|
108
|
+
return path.relative(root, target) || ref.path;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { rewriteWorktreeVerify, missingOnMain, stepReferencedPaths };
|
package/lib/voice-gate.js
CHANGED
|
@@ -255,6 +255,8 @@ const LANDING_REASON_SENTENCES = {
|
|
|
255
255
|
verifier_is_builder: 'the re-check actor built this row, another actor must re-check',
|
|
256
256
|
verify_command_not_allowed: 'its recorded check is not on the list this machine may run alone, run it yourself to unblock',
|
|
257
257
|
verify_failed: 'its check command failed on re-run, fix the failure and re-certify',
|
|
258
|
+
verify_failed_on_main: 'its check was recorded in an agent copy and fails when rerun from main, so its work is likely still on the agent branch',
|
|
259
|
+
verify_only_on_agent_branch: 'its check runs a file that only exists on the agent branch, not on main yet, so that branch has to land first',
|
|
258
260
|
verify_passed: 'its check command passed on re-run',
|
|
259
261
|
verify_unrunnable: 'its check command could not even start, fix the command and re-certify',
|
|
260
262
|
verify_workdir_missing: 'the folder its check was recorded in is gone, re-run the check from a real folder',
|
package/package.json
CHANGED
package/scripts/det/ytnotes
CHANGED
|
@@ -373,7 +373,7 @@ run_grok() {
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
run_haiku() {
|
|
376
|
-
run_with_timeout 240 claude -p "$PROMPT" --model "${1:-claude-haiku-
|
|
376
|
+
run_with_timeout 240 claude -p "$PROMPT" --model "${1:-claude-haiku-5-5}"
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
# Gemini Flash through the Antigravity CLI (agy). On an 85-minute transcript it
|