atris 3.52.0 → 3.53.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 +8 -7
- package/atris/skills/youtube/SKILL.md +8 -8
- package/ax +592 -91
- package/bin/atris.js +17 -5
- package/commands/computer.js +2 -2
- package/commands/mission.js +52 -24
- package/commands/now.js +10 -3
- package/commands/radar.js +65 -33
- package/commands/youtube.js +0 -8
- package/lib/auto-accept-certified.js +2 -1
- package/lib/engine-ask.js +11 -3
- package/lib/engine-registry.js +32 -7
- package/lib/mission-ledger-compact.js +127 -0
- package/lib/mission-runtime-loop.js +30 -2
- package/lib/next-moves.js +34 -34
- package/lib/permission-grants.js +10 -1
- package/lib/runner-command.js +5 -4
- package/lib/task-db.js +18 -2
- package/lib/task-proof.js +6 -1
- package/lib/task-receipt.js +5 -1
- package/package.json +2 -1
- package/scripts/outbound-artifact-gate.js +227 -0
package/bin/atris.js
CHANGED
|
@@ -3140,7 +3140,7 @@ function inspectAgentCliWiring() {
|
|
|
3140
3140
|
},
|
|
3141
3141
|
];
|
|
3142
3142
|
|
|
3143
|
-
const binaries = ['atris', 'ax', 'claude', 'codex', 'cursor-agent', 'devin', '
|
|
3143
|
+
const binaries = ['atris', 'ax', 'claude', 'codex', 'cursor-agent', 'devin', 'agy'].map((name) => ({
|
|
3144
3144
|
name,
|
|
3145
3145
|
path: commandOnPath(name),
|
|
3146
3146
|
}));
|
|
@@ -3319,7 +3319,7 @@ async function chatAtris() {
|
|
|
3319
3319
|
for (const arg of rawArgs) {
|
|
3320
3320
|
if (arg === '--agent') {
|
|
3321
3321
|
agentLane = true;
|
|
3322
|
-
} else if (arg === '--print' || arg === '--headless') {
|
|
3322
|
+
} else if (arg === '--print' || arg === '--headless' || arg === '--rapid' || arg === '--auto-approve') {
|
|
3323
3323
|
fastLaneFlags.push(arg);
|
|
3324
3324
|
} else {
|
|
3325
3325
|
messageArgs.push(arg);
|
|
@@ -3332,9 +3332,12 @@ async function chatAtris() {
|
|
|
3332
3332
|
console.log('Usage: atris chat ["message"]');
|
|
3333
3333
|
console.log('');
|
|
3334
3334
|
console.log(' Chat with Atris 2 Fast in this workspace: tools attached, same turn as `ax --fast`.');
|
|
3335
|
+
console.log(' Pass --rapid to use Atris Rapid instead.');
|
|
3335
3336
|
console.log(' Requires `atris login`.');
|
|
3336
3337
|
console.log('');
|
|
3337
3338
|
console.log(' atris chat Interactive chat (ax --fast --chat)');
|
|
3339
|
+
console.log(' atris chat --rapid Interactive Rapid chat (ax --rapid --chat)');
|
|
3340
|
+
console.log(' atris chat --rapid --auto-approve Rapid chat that auto-approves safe git push');
|
|
3338
3341
|
console.log(' atris chat "what now?" One-shot message (ax --fast)');
|
|
3339
3342
|
console.log(' atris chat --print "..." Headless JSON result (ax --fast --print)');
|
|
3340
3343
|
console.log(' atris chat --agent [...] Legacy cloud-agent lane (needs `atris agent`)');
|
|
@@ -3361,9 +3364,16 @@ async function chatAtris() {
|
|
|
3361
3364
|
if (!agentLane) {
|
|
3362
3365
|
try {
|
|
3363
3366
|
const axPath = path.join(__dirname, '..', 'ax');
|
|
3364
|
-
const
|
|
3367
|
+
const lane = fastLaneFlags.includes('--rapid') ? '--rapid' : '--fast';
|
|
3368
|
+
const extraFlags = fastLaneFlags.filter((flag) => flag !== '--rapid');
|
|
3369
|
+
const axArgs = message
|
|
3370
|
+
? [lane, ...extraFlags, message].filter(Boolean)
|
|
3371
|
+
: [lane, ...extraFlags, '--chat'];
|
|
3365
3372
|
const run = spawnSync(process.execPath, [axPath, ...axArgs], { stdio: 'inherit' });
|
|
3366
|
-
|
|
3373
|
+
// spawnSync reports spawn failure via run.error (status stays null), and
|
|
3374
|
+
// a signal-killed ax also leaves status null; neither is a success.
|
|
3375
|
+
if (!run.error) process.exit(run.status ?? 1);
|
|
3376
|
+
// ax unavailable: fall through to the agent lane.
|
|
3367
3377
|
} catch {
|
|
3368
3378
|
// ax unavailable: fall through to the agent lane.
|
|
3369
3379
|
}
|
|
@@ -3632,7 +3642,9 @@ async function atrisFastChat() {
|
|
|
3632
3642
|
const axModule = require('../ax');
|
|
3633
3643
|
if (axModule.resolveRoute(message) === 'local') {
|
|
3634
3644
|
const run = spawnSync(process.execPath, [path.join(__dirname, '..', 'ax'), '--fast', message], { stdio: 'inherit' });
|
|
3635
|
-
|
|
3645
|
+
// Same rule as the chat lane: spawn failure or a signal kill leaves
|
|
3646
|
+
// status null and must not read as success.
|
|
3647
|
+
if (!run.error) process.exit(run.status ?? 1);
|
|
3636
3648
|
}
|
|
3637
3649
|
} catch {
|
|
3638
3650
|
// ax unavailable: fall through to the plain cloud one-shot.
|
package/commands/computer.js
CHANGED
|
@@ -2141,7 +2141,7 @@ async function runBusinessPromptViaRunnerProxy(token, ctx, prompt, options = {})
|
|
|
2141
2141
|
const payload = {
|
|
2142
2142
|
prompt,
|
|
2143
2143
|
permission_mode: 'bypassPermissions',
|
|
2144
|
-
max_turns: Math.min(Math.max(Number(options.maxTurns ||
|
|
2144
|
+
max_turns: Math.min(Math.max(Number(options.maxTurns || 40), 1), 100),
|
|
2145
2145
|
reset_context: Boolean(options.resetContext),
|
|
2146
2146
|
};
|
|
2147
2147
|
if (options.worker) payload.worker = options.worker;
|
|
@@ -3399,7 +3399,7 @@ async function sendBusinessChat(token, ctx, message, sessionId, resetContext = f
|
|
|
3399
3399
|
const fallback = await runBusinessPromptViaRunnerProxy(token, ctx, message, {
|
|
3400
3400
|
...options,
|
|
3401
3401
|
resetContext,
|
|
3402
|
-
maxTurns:
|
|
3402
|
+
maxTurns: 60,
|
|
3403
3403
|
});
|
|
3404
3404
|
if (!fallback.ok) {
|
|
3405
3405
|
if (typeof options.onFailure === 'function') {
|
package/commands/mission.js
CHANGED
|
@@ -53,6 +53,7 @@ const {
|
|
|
53
53
|
runsPruneLines,
|
|
54
54
|
formatBytes,
|
|
55
55
|
} = require('../lib/runs-prune');
|
|
56
|
+
const { compactMissionLedger } = require('../lib/mission-ledger-compact');
|
|
56
57
|
const autolandLib = require('../lib/autoland');
|
|
57
58
|
const { gateForHuman, landingWhyClause } = require('../lib/voice-gate');
|
|
58
59
|
const { operatorReady, hasAgentJargon } = require('./autoland');
|
|
@@ -1347,6 +1348,11 @@ function saveMission(mission, root = process.cwd(), eventType = 'mission_updated
|
|
|
1347
1348
|
updated_at: stampIso(),
|
|
1348
1349
|
});
|
|
1349
1350
|
appendJsonLine(paths.missionsJsonl, next);
|
|
1351
|
+
// Self-maintenance: once history outweighs live missions 4:1 the ledger is
|
|
1352
|
+
// rewritten to one row per mission (latest state, first display number —
|
|
1353
|
+
// exactly what loadMissionMap reconstructs). Every status/tick path parses
|
|
1354
|
+
// this file several times, so unbounded snapshot history taxes everything.
|
|
1355
|
+
try { compactMissionLedger(paths.missionsJsonl); } catch {}
|
|
1350
1356
|
const event = appendEvent(eventType, next, payload, root);
|
|
1351
1357
|
renderMissionStatus(root);
|
|
1352
1358
|
renderMemberMissionState(next.owner, root);
|
|
@@ -4810,27 +4816,60 @@ function missionTickVerification(tick) {
|
|
|
4810
4816
|
return { verified: false, state: ran ? 'unchecked' : 'skipped', unchecked: ran };
|
|
4811
4817
|
}
|
|
4812
4818
|
|
|
4813
|
-
//
|
|
4814
|
-
//
|
|
4815
|
-
//
|
|
4816
|
-
//
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
+
// Per-process index of mission run receipts, keyed by runs dir. Reading and
|
|
4820
|
+
// parsing ~1400 receipt files per mission made `mission list` scale as
|
|
4821
|
+
// O(missions × runs-files); building the mission_id → receipts map once per
|
|
4822
|
+
// invocation keeps outputs identical while scanning the dir a single time.
|
|
4823
|
+
// The dir mtime guards the memo so a receipt written mid-process (new file in
|
|
4824
|
+
// the dir) invalidates it; malformed receipts stay silently skipped, matching
|
|
4825
|
+
// readJson's existing tolerance.
|
|
4826
|
+
const missionRunsIndexCache = new Map();
|
|
4827
|
+
|
|
4828
|
+
function missionRunsReceiptIndex(root = process.cwd()) {
|
|
4829
|
+
const runsDir = statePaths(root).runsDir;
|
|
4830
|
+
let dirMtime = -1;
|
|
4831
|
+
try {
|
|
4832
|
+
dirMtime = fs.statSync(runsDir).mtimeMs;
|
|
4833
|
+
} catch {
|
|
4834
|
+
dirMtime = -1;
|
|
4835
|
+
}
|
|
4836
|
+
const cached = missionRunsIndexCache.get(runsDir);
|
|
4837
|
+
if (cached && cached.dirMtime === dirMtime) return cached.byMission;
|
|
4838
|
+
|
|
4819
4839
|
let files = [];
|
|
4820
4840
|
try {
|
|
4821
|
-
files = fs.readdirSync(
|
|
4841
|
+
files = fs.readdirSync(runsDir)
|
|
4822
4842
|
.filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
|
|
4823
|
-
.map((file) => path.join(
|
|
4843
|
+
.map((file) => path.join(runsDir, file));
|
|
4824
4844
|
} catch {
|
|
4825
4845
|
files = [];
|
|
4826
4846
|
}
|
|
4847
|
+
const byMission = new Map();
|
|
4848
|
+
for (const file of files) {
|
|
4849
|
+
const receipt = readJson(file);
|
|
4850
|
+
if (!receipt || !receipt.mission_id) continue;
|
|
4851
|
+
let bucket = byMission.get(receipt.mission_id);
|
|
4852
|
+
if (!bucket) {
|
|
4853
|
+
bucket = [];
|
|
4854
|
+
byMission.set(receipt.mission_id, bucket);
|
|
4855
|
+
}
|
|
4856
|
+
bucket.push({ file, receipt });
|
|
4857
|
+
}
|
|
4858
|
+
missionRunsIndexCache.set(runsDir, { dirMtime, byMission });
|
|
4859
|
+
return byMission;
|
|
4860
|
+
}
|
|
4861
|
+
|
|
4862
|
+
// Roll every ran tick for a mission into a verification-debt tally. `unchecked`
|
|
4863
|
+
// counts ran ticks that recorded no verifier result at all — the "tick recorded
|
|
4864
|
+
// but nothing was checked" red the report and now.md rollup surface and count
|
|
4865
|
+
// against the mission.
|
|
4866
|
+
function missionVerificationDebt(mission, root = process.cwd()) {
|
|
4867
|
+
const entries = missionRunsReceiptIndex(root).get(mission.id) || [];
|
|
4827
4868
|
let ran = 0;
|
|
4828
4869
|
let verified = 0;
|
|
4829
4870
|
let unchecked = 0;
|
|
4830
4871
|
const seen = new Set();
|
|
4831
|
-
for (const
|
|
4832
|
-
const receipt = readJson(file);
|
|
4833
|
-
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
4872
|
+
for (const { receipt } of entries) {
|
|
4834
4873
|
for (const tick of missionReceiptTicks(receipt)) {
|
|
4835
4874
|
if (!tick || tick.status !== 'ran') continue;
|
|
4836
4875
|
const key = `${tick.tick_index || ''}:${tick.finished_at || tick.started_at || ''}`;
|
|
@@ -4853,21 +4892,10 @@ function missionVerificationDebtLine(debt) {
|
|
|
4853
4892
|
}
|
|
4854
4893
|
|
|
4855
4894
|
function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
|
|
4856
|
-
const
|
|
4857
|
-
let files = [];
|
|
4858
|
-
try {
|
|
4859
|
-
files = fs.readdirSync(paths.runsDir)
|
|
4860
|
-
.filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
|
|
4861
|
-
.map((file) => path.join(paths.runsDir, file));
|
|
4862
|
-
} catch {
|
|
4863
|
-
files = [];
|
|
4864
|
-
}
|
|
4865
|
-
|
|
4895
|
+
const entries = missionRunsReceiptIndex(root).get(mission.id) || [];
|
|
4866
4896
|
const items = [];
|
|
4867
4897
|
const seen = new Set();
|
|
4868
|
-
for (const file of
|
|
4869
|
-
const receipt = readJson(file);
|
|
4870
|
-
if (!receipt || receipt.mission_id !== mission.id) continue;
|
|
4898
|
+
for (const { file, receipt } of entries) {
|
|
4871
4899
|
const receiptPath = path.relative(root, file);
|
|
4872
4900
|
for (const tick of missionReceiptTicks(receipt)) {
|
|
4873
4901
|
const summary = missionTickReportSummary(tick);
|
package/commands/now.js
CHANGED
|
@@ -270,7 +270,13 @@ function taskReceiptTitle(row) {
|
|
|
270
270
|
// one per receipt, landed result + human proof state. Shares the collector
|
|
271
271
|
// with the count so the number and the visible list always match.
|
|
272
272
|
function todayTaskReceiptLines(root = process.cwd(), date = new Date(), limit = 6) {
|
|
273
|
-
return collectTaskReceiptsToday(root, date)
|
|
273
|
+
return formatTaskReceiptLines(collectTaskReceiptsToday(root, date), limit);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Format an already-collected receipt list, so a caller that needs both the
|
|
277
|
+
// count and the lines (renderDefaultNow) parses the receipt files only once.
|
|
278
|
+
function formatTaskReceiptLines(receipts, limit = 6) {
|
|
279
|
+
return receipts
|
|
274
280
|
.slice(-limit)
|
|
275
281
|
.reverse()
|
|
276
282
|
.map((r) => {
|
|
@@ -411,7 +417,8 @@ function renderDefaultNow(root = process.cwd()) {
|
|
|
411
417
|
const journalPath = currentJournalPath(root);
|
|
412
418
|
const openTodoCount = countOpenWorkItems(root, todoPath);
|
|
413
419
|
const inboxCount = countMatches(journalPath, /^-\s+\*\*I\d+:/gm);
|
|
414
|
-
const
|
|
420
|
+
const taskReceipts = collectTaskReceiptsToday(root);
|
|
421
|
+
const taskReceiptCount = taskReceipts.length;
|
|
415
422
|
const missionReceiptCount = countMissionReceiptsToday(root);
|
|
416
423
|
const completedCount = taskReceiptCount + missionReceiptCount || countJournalCompletedReceipts(journalPath);
|
|
417
424
|
const generated = formatLocalTimestamp();
|
|
@@ -419,7 +426,7 @@ function renderDefaultNow(root = process.cwd()) {
|
|
|
419
426
|
const whatMattersNow = moveLine
|
|
420
427
|
? `${moveLine}\n\n- Run the named next action and leave proof before choosing another.`
|
|
421
428
|
: '- Decide the next useful move before opening more context.';
|
|
422
|
-
const receiptLines =
|
|
429
|
+
const receiptLines = formatTaskReceiptLines(taskReceipts);
|
|
423
430
|
const missionReceiptLines = todayMissionReceiptLines(root);
|
|
424
431
|
const commitLines = landedCommitLines(root);
|
|
425
432
|
const hiddenReceiptCount = Math.max(0, completedCount - missionReceiptLines.length - receiptLines.length);
|
package/commands/radar.js
CHANGED
|
@@ -54,23 +54,39 @@ function agentTypeForCommand(command) {
|
|
|
54
54
|
if (/(^|\s|\/)claude(\s|$)/.test(cmd) && !/Claude\.app/.test(cmd)) return 'claude';
|
|
55
55
|
if (/(^|\s|\/)opencode(\s|$)/.test(cmd)) return 'opencode';
|
|
56
56
|
if (/(^|\s|\/)devin(\s|$)/.test(cmd)) return 'devin';
|
|
57
|
+
if (/(^|\s|\/)agy(\s|$)/.test(cmd)) return 'agy';
|
|
57
58
|
if (/(^|\s|\/)droid(\s|$)/.test(cmd)) return 'droid';
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
// Resolve cwd for many pids at once: one lsof call on darwin instead of one
|
|
63
|
+
// per process. Returns a Map of pid (string) -> cwd; missing pids stay absent.
|
|
64
|
+
function processCwds(pids, deps) {
|
|
62
65
|
const { platform, execFile } = deps;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const byPid = new Map();
|
|
67
|
+
if (!pids.length) return byPid;
|
|
68
|
+
if (platform === 'linux') {
|
|
69
|
+
for (const pid of pids) {
|
|
70
|
+
try { byPid.set(String(pid), deps.readlink(`/proc/${pid}/cwd`)); } catch {}
|
|
71
|
+
}
|
|
72
|
+
return byPid;
|
|
73
|
+
}
|
|
74
|
+
if (platform === 'darwin') {
|
|
75
|
+
let out = '';
|
|
76
|
+
try {
|
|
77
|
+
out = execFile('lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.map(String).join(',')], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// lsof exits non-zero when any pid in the batch is gone; keep the rows it
|
|
80
|
+
// did print instead of losing every cwd.
|
|
81
|
+
out = error && error.stdout ? error.stdout : '';
|
|
66
82
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
let currentPid = '';
|
|
84
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
85
|
+
if (line.startsWith('p')) currentPid = line.slice(1).trim();
|
|
86
|
+
else if (line.startsWith('n') && currentPid) byPid.set(currentPid, line.slice(1));
|
|
71
87
|
}
|
|
72
|
-
}
|
|
73
|
-
return
|
|
88
|
+
}
|
|
89
|
+
return byPid;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
function gitBranch(cwd, execFile) {
|
|
@@ -93,22 +109,28 @@ function collectAgents(deps) {
|
|
|
93
109
|
.map(row => ({ ...row, agent: agentTypeForCommand(row.command) }))
|
|
94
110
|
.filter(row => row.agent);
|
|
95
111
|
const parentPids = new Set(agentRows.map(row => String(row.ppid || '')).filter(Boolean));
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
const topRows = agentRows.filter(row => !parentPids.has(String(row.pid)));
|
|
113
|
+
const cwdByPid = processCwds(topRows.map(row => row.pid), deps);
|
|
114
|
+
const branchByCwd = new Map();
|
|
115
|
+
const branchFor = (cwd) => {
|
|
116
|
+
if (!cwd) return '';
|
|
117
|
+
if (!branchByCwd.has(cwd)) branchByCwd.set(cwd, gitBranch(cwd, deps.execFile));
|
|
118
|
+
return branchByCwd.get(cwd);
|
|
119
|
+
};
|
|
120
|
+
return topRows.map(row => {
|
|
121
|
+
const cwd = cwdByPid.get(String(row.pid)) || '';
|
|
122
|
+
return {
|
|
123
|
+
pid: row.pid,
|
|
124
|
+
agent: row.agent,
|
|
125
|
+
command: row.command,
|
|
126
|
+
status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
|
|
127
|
+
cwd,
|
|
128
|
+
repo: repoLabel(cwd),
|
|
129
|
+
branch: branchFor(cwd),
|
|
130
|
+
cpu: row.cpu,
|
|
131
|
+
mem: row.mem,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
function loadTasks(root, deps) {
|
|
@@ -287,8 +309,11 @@ function loadTeam(root, deps) {
|
|
|
287
309
|
};
|
|
288
310
|
}
|
|
289
311
|
|
|
290
|
-
function
|
|
291
|
-
|
|
312
|
+
function loadScorecards(root, deps) {
|
|
313
|
+
return readJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps.readFile, deps.exists);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function loadBrain(root, deps, scorecards = loadScorecards(root, deps)) {
|
|
292
317
|
const operatorDir = path.join(root, '.atris', 'state', 'operator-scorecards');
|
|
293
318
|
let operatorScorecards = 0;
|
|
294
319
|
try {
|
|
@@ -316,7 +341,7 @@ function countDirectoryEntries(dir, deps, predicate = () => true) {
|
|
|
316
341
|
return listNames(dir, deps).filter(predicate).length;
|
|
317
342
|
}
|
|
318
343
|
|
|
319
|
-
function loadBusinessCollaboration(root, deps, team = {}) {
|
|
344
|
+
function loadBusinessCollaboration(root, deps, team = {}, scorecardCount = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps)) {
|
|
320
345
|
const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
|
|
321
346
|
const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
|
|
322
347
|
const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
|
|
@@ -335,7 +360,7 @@ function loadBusinessCollaboration(root, deps, team = {}) {
|
|
|
335
360
|
const localReceipts = countDirectoryEntries(path.join(root, '.atris', 'receipts'), deps, name => /\.(json|md|txt)$/i.test(name));
|
|
336
361
|
const events = countJsonLines(path.join(root, '.atris', 'state', 'events.jsonl'), deps);
|
|
337
362
|
const episodes = countJsonLines(path.join(root, '.atris', 'state', 'episodes.jsonl'), deps);
|
|
338
|
-
const scorecards =
|
|
363
|
+
const scorecards = scorecardCount;
|
|
339
364
|
const computerDirs = countDirectoryEntries(path.join(root, 'atris', 'computers'), deps, name => !name.startsWith('.'));
|
|
340
365
|
const hasOnboarding = ingestPacks > 0 || starterBriefs > 0 || onePagers > 0;
|
|
341
366
|
const hasProofLoop = events > 0 || episodes > 0 || scorecards > 0 || localReceipts > 0;
|
|
@@ -786,12 +811,18 @@ function collectRadar(options = {}) {
|
|
|
786
811
|
const missions = loadMissions(root, deps, nowMs);
|
|
787
812
|
identityCache.set(root, { missions, missionLocks: loadMissionLocks(path.join(root, '.atris', 'state'), deps), memberLocks: loadMemberLoopLocks(path.join(root, '.atris', 'state'), deps) });
|
|
788
813
|
const worktrees = loadWorktrees(root, deps);
|
|
814
|
+
const branchIdentityCache = new Map();
|
|
815
|
+
const branchIdentityFor = (cwd) => {
|
|
816
|
+
if (!cwd) return null;
|
|
817
|
+
if (!branchIdentityCache.has(cwd)) branchIdentityCache.set(cwd, branchIdentityAtCwd(cwd, deps.execFile));
|
|
818
|
+
return branchIdentityCache.get(cwd);
|
|
819
|
+
};
|
|
789
820
|
const agents = collectAgents(deps).map(agent => {
|
|
790
821
|
const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
|
|
791
822
|
const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
|
|
792
823
|
const identity = taskWorkspaceRoot ? loadWorkspaceIdentity(taskWorkspaceRoot, deps, identityCache, nowMs) : { missions: [], missionLocks: [], memberLocks: [] };
|
|
793
824
|
const sidecar = loadAgentWorktreeSidecar(agent.cwd, deps);
|
|
794
|
-
const branchIdentity =
|
|
825
|
+
const branchIdentity = branchIdentityFor(agent.cwd);
|
|
795
826
|
const sessionId = parseSessionIdFromCommand(agent.command);
|
|
796
827
|
const interactive = agent.agent === 'claude' && isInteractiveClaudeCommand(agent.command);
|
|
797
828
|
const resolved = resolveAgentTaskBinding({
|
|
@@ -823,14 +854,15 @@ function collectRadar(options = {}) {
|
|
|
823
854
|
task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks, resolved),
|
|
824
855
|
};
|
|
825
856
|
});
|
|
857
|
+
const scorecards = loadScorecards(root, deps);
|
|
826
858
|
const osState = {
|
|
827
859
|
xp: loadXp(root, deps),
|
|
828
860
|
team: loadTeam(root, deps),
|
|
829
|
-
brain: loadBrain(root, deps),
|
|
861
|
+
brain: loadBrain(root, deps, scorecards),
|
|
830
862
|
swarlo: loadSwarlo(tasks),
|
|
831
863
|
loop: loadLoop(missions, root, deps),
|
|
832
864
|
};
|
|
833
|
-
osState.business = loadBusinessCollaboration(root, deps, osState.team);
|
|
865
|
+
osState.business = loadBusinessCollaboration(root, deps, osState.team, scorecards.length);
|
|
834
866
|
return { root, generated_at: new Date(nowMs).toISOString(), summary: summarize(tasks, missions, worktrees, agents), os: osState, next_action: nextAction(tasks, missions, worktrees, agents, osState), agents, tasks, missions, worktrees };
|
|
835
867
|
}
|
|
836
868
|
|
package/commands/youtube.js
CHANGED
|
@@ -1263,25 +1263,17 @@ module.exports = {
|
|
|
1263
1263
|
shouldRetryWithLocalTranscript,
|
|
1264
1264
|
formatYoutubeResult,
|
|
1265
1265
|
fileBriefFromNotes,
|
|
1266
|
-
looksLikeYoutubeUrl,
|
|
1267
1266
|
isPlaylistUrl,
|
|
1268
1267
|
parseNotesArgs,
|
|
1269
1268
|
expandNotesTargets,
|
|
1270
1269
|
runYoutubeNotesBatch,
|
|
1271
|
-
runYoutubeNotes,
|
|
1272
1270
|
parseDigestArgs,
|
|
1273
1271
|
collectVideoBriefs,
|
|
1274
1272
|
buildDigestPrompt,
|
|
1275
|
-
runYoutubeDigest,
|
|
1276
1273
|
normalizeWatchChannel,
|
|
1277
1274
|
channelVideosUrl,
|
|
1278
1275
|
parseFlatPlaylist,
|
|
1279
1276
|
loadWatchState,
|
|
1280
|
-
saveWatchState,
|
|
1281
|
-
addWatchChannel,
|
|
1282
|
-
listWatchChannels,
|
|
1283
|
-
removeWatchChannel,
|
|
1284
|
-
tickWatch,
|
|
1285
1277
|
watchCommand,
|
|
1286
1278
|
youtubeCommand,
|
|
1287
1279
|
};
|
|
@@ -1099,7 +1099,8 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1099
1099
|
if ((tierRequiresStrictVerify && !verifyResult.ok)
|
|
1100
1100
|
|| verifyResult.reason === 'verify_failed'
|
|
1101
1101
|
|| verifyResult.reason === 'verify_unrunnable'
|
|
1102
|
-
|| verifyResult.reason === 'verify_worktree_missing'
|
|
1102
|
+
|| verifyResult.reason === 'verify_worktree_missing'
|
|
1103
|
+
|| verifyResult.reason === 'verify_workdir_missing') {
|
|
1103
1104
|
return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
|
|
1104
1105
|
}
|
|
1105
1106
|
}
|
package/lib/engine-ask.js
CHANGED
|
@@ -29,7 +29,7 @@ const MAX_ASK_PROMPT_BYTES = 16 * 1024;
|
|
|
29
29
|
const MAX_ASK_TOTAL_PROMPT_BYTES = 64 * 1024;
|
|
30
30
|
const MAX_ASK_OUTPUT_BYTES = 1024 * 1024;
|
|
31
31
|
const ASK_STOP_GRACE_MS = 250;
|
|
32
|
-
const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok']);
|
|
32
|
+
const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok', 'agy']);
|
|
33
33
|
const READ_ONLY_PREAMBLE = [
|
|
34
34
|
'This is a read-only request.',
|
|
35
35
|
'Do not modify files, create worktrees, start background agents, or run commands with side effects.',
|
|
@@ -241,8 +241,16 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
|
|
|
241
241
|
args: ['--no-memory', '--no-subagents', '--permission-mode', 'plan', '--sandbox', 'read-only', ...(model ? ['--model', model] : []), '-p', request],
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
|
-
if (engine === '
|
|
245
|
-
|
|
244
|
+
if (engine === 'agy') {
|
|
245
|
+
// agy prompts for tool permission even in plan mode and a headless ask
|
|
246
|
+
// has no one to answer, so it denies itself `ls` and fails every ask
|
|
247
|
+
// (verified live 2026-08-19). The sandbox keeps the run read-only while
|
|
248
|
+
// skip-permissions lets sandboxed reads proceed unattended.
|
|
249
|
+
return {
|
|
250
|
+
engine,
|
|
251
|
+
bin: profile.bin,
|
|
252
|
+
args: ['--mode', 'plan', '--sandbox', '--dangerously-skip-permissions', ...(model ? ['--model', model] : []), '-p', request],
|
|
253
|
+
};
|
|
246
254
|
}
|
|
247
255
|
throw new Error(`engine ask has no read-only command for ${engine}`);
|
|
248
256
|
}
|
package/lib/engine-registry.js
CHANGED
|
@@ -34,7 +34,8 @@ function engineFailureHealthStatus(result) {
|
|
|
34
34
|
return 'credit_out';
|
|
35
35
|
}
|
|
36
36
|
if (/not installed|command not found|\benoent\b/.test(signalText)) return 'not_installed';
|
|
37
|
-
|
|
37
|
+
// Timeouts and unavailable models are transient: 'error' keeps the engine
|
|
38
|
+
// routable, while 'not_installed' would drop it from routing until a doctor run.
|
|
38
39
|
return 'error';
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -48,7 +49,19 @@ const ENGINE_SEED_META = Object.freeze({
|
|
|
48
49
|
fable: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), models: Object.freeze(['opus 5', 'opus 4.8', 'fable', 'haiku']), duty: 'leader', fallback_order: 50 }),
|
|
49
50
|
composer: Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator', 'executor']), models: Object.freeze(['composer 2.5']), fallback_order: 60 }),
|
|
50
51
|
haiku: Object.freeze({ tier: 'fast', roles: Object.freeze(['validator']), models: Object.freeze(['haiku']), fallback_order: 70 }),
|
|
51
|
-
|
|
52
|
+
agy: Object.freeze({
|
|
53
|
+
tier: 'pro',
|
|
54
|
+
roles: Object.freeze(['executor']),
|
|
55
|
+
models: Object.freeze([
|
|
56
|
+
'gemini-3.7-flash-high',
|
|
57
|
+
'gemini-3.1-pro-high',
|
|
58
|
+
'claude-sonnet-4-6',
|
|
59
|
+
'claude-opus-4-6-thinking',
|
|
60
|
+
'gpt-oss-120b-medium',
|
|
61
|
+
]),
|
|
62
|
+
duty: 'errands',
|
|
63
|
+
fallback_order: 90,
|
|
64
|
+
}),
|
|
52
65
|
});
|
|
53
66
|
|
|
54
67
|
function engineRegistryFile(root = process.cwd()) {
|
|
@@ -144,9 +157,9 @@ function normalizeEngineEntry(id, saved = {}) {
|
|
|
144
157
|
};
|
|
145
158
|
}
|
|
146
159
|
|
|
147
|
-
function seededRegistry(root = process.cwd()) {
|
|
160
|
+
function seededRegistry(root = process.cwd(), preloadedRaw = null) {
|
|
148
161
|
const file = engineRegistryFile(root);
|
|
149
|
-
const raw = readRawRegistry(file);
|
|
162
|
+
const raw = preloadedRaw || readRawRegistry(file);
|
|
150
163
|
const savedById = new Map();
|
|
151
164
|
for (const entry of raw.engines || []) {
|
|
152
165
|
const id = canonicalEngineName(entry && (entry.id || entry.name));
|
|
@@ -162,7 +175,11 @@ function seededRegistry(root = process.cwd()) {
|
|
|
162
175
|
function writeEngineRegistry(root, registry) {
|
|
163
176
|
const file = engineRegistryFile(root);
|
|
164
177
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
165
|
-
|
|
178
|
+
// Atomic write: a concurrent reader must never see a torn file, because a
|
|
179
|
+
// failed parse falls back to the seed registry and erases operator policy.
|
|
180
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
181
|
+
fs.writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
|
|
182
|
+
fs.renameSync(tmp, file);
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
function setEngineOverrides(name, overrides = {}, root = process.cwd()) {
|
|
@@ -221,9 +238,17 @@ function setEngineOverrides(name, overrides = {}, root = process.cwd()) {
|
|
|
221
238
|
return { id, ...nextOverrides };
|
|
222
239
|
}
|
|
223
240
|
|
|
241
|
+
// A read only writes when normalization actually changed the saved engines
|
|
242
|
+
// (first seed, schema drift). Settled registries stay untouched, so read
|
|
243
|
+
// paths cannot stomp a mutation another process just landed. The comparison
|
|
244
|
+
// uses the same raw snapshot the seed was built from: one read, one decision.
|
|
224
245
|
function readEngineRegistry(root = process.cwd(), options = {}) {
|
|
225
|
-
const
|
|
226
|
-
|
|
246
|
+
const raw = readRawRegistry(engineRegistryFile(root));
|
|
247
|
+
const registry = seededRegistry(root, raw);
|
|
248
|
+
const needsPersist = JSON.stringify(raw.engines || []) !== JSON.stringify(registry.engines);
|
|
249
|
+
if (options.persist !== false && needsPersist) {
|
|
250
|
+
writeEngineRegistry(root, registry);
|
|
251
|
+
}
|
|
227
252
|
return registry;
|
|
228
253
|
}
|
|
229
254
|
|