atris 3.63.0 → 3.64.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/commerce/SKILL.md +24 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +11 -2
- package/cli/atris_code.py +25 -1
- package/commands/auth.js +79 -10
- package/commands/autoland.js +11 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +3 -3
- package/commands/buy.js +3 -3
- package/commands/deck.js +2 -2
- package/commands/engine.js +122 -13
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/member.js +47 -9
- package/commands/mission.js +1 -1
- package/commands/pack-craft.js +2 -2
- package/commands/publish.js +2 -2
- package/commands/radar.js +3 -2
- package/commands/router.js +2 -2
- package/commands/task.js +84 -2
- package/commands/transactions.js +3 -3
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +2 -1
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/chat-log-scan.js +2 -1
- package/lib/ci-runner.js +3 -2
- package/lib/codex-flight.js +2 -1
- package/lib/commerce.js +17 -2
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-registry.js +85 -18
- package/lib/first-minute.js +2 -2
- package/lib/fleet.js +8 -36
- package/lib/git-spawn.js +5 -2
- package/lib/ignored-cache.js +51 -0
- package/lib/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-runs.js +13 -2
- package/lib/runner-command.js +22 -6
- package/lib/sandbox-pids.js +113 -0
- package/lib/secret-gateway.js +3 -13
- package/lib/state-home.js +106 -0
- package/lib/task-db.js +4 -3
- package/lib/task-prune.js +462 -0
- package/lib/x-post-check.js +497 -0
- package/package.json +1 -1
- package/scripts/det/data/xsearch-answer.json +24 -0
- package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
- package/scripts/det/xsearch-bench.js +307 -0
- package/scripts/det/ytnotes +61 -15
- package/scripts/det/ytrail-bench.js +2 -1
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Rule lines every builder brief carries (lib/codex-flight.js buildPrompt and
|
|
4
|
+
// lib/fleet.js buildFleetPrompt). The text lives here once so the two briefs
|
|
5
|
+
// cannot drift apart.
|
|
6
|
+
const SHARED_BRIEF_RULES = [
|
|
7
|
+
'- Keep one concern per PR; split anything larger into separate PRs because git history guides future agents and small PRs are cheap to revert and bisect.',
|
|
8
|
+
'- A guard, limit, or cache must hold on every path to the thing it protects; grep for the other callers, templates, and writers, and test at least one path you were not pointed at.',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
module.exports = { SHARED_BRIEF_RULES };
|
package/lib/chat-log-scan.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { stateHome } = require('./state-home');
|
|
4
5
|
|
|
5
6
|
const SCHEMA = 'atris.chat_scan.v1';
|
|
6
7
|
const PROMPT_LINE = /^(?:max|pro|fast|code-fast)\s*›\s*(.+)$/i;
|
|
@@ -73,7 +74,7 @@ function axLogDirs(cwd = process.cwd(), options = {}) {
|
|
|
73
74
|
const local = path.join(cwd, 'atris', 'runs');
|
|
74
75
|
if (fs.existsSync(local)) dirs.push(local);
|
|
75
76
|
if (options.global) {
|
|
76
|
-
const home = path.join(
|
|
77
|
+
const home = path.join(stateHome(), '.atris', 'runs');
|
|
77
78
|
if (fs.existsSync(home) && home !== local) dirs.push(home);
|
|
78
79
|
}
|
|
79
80
|
return dirs;
|
package/lib/ci-runner.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
+
const { stateHome } = require('./state-home');
|
|
5
6
|
const os = require('node:os');
|
|
6
7
|
const https = require('node:https');
|
|
7
8
|
const { spawn, spawnSync } = require('node:child_process');
|
|
@@ -9,7 +10,7 @@ const { spawn, spawnSync } = require('node:child_process');
|
|
|
9
10
|
const RUNNER_VERSION = '2.336.0';
|
|
10
11
|
const GITHUB_API_VERSION = '2026-03-10';
|
|
11
12
|
|
|
12
|
-
function usageFilePath(home =
|
|
13
|
+
function usageFilePath(home = stateHome()) {
|
|
13
14
|
return path.join(home, '.atris', 'ci', 'usage.jsonl');
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -309,7 +310,7 @@ async function ensureRunner(options = {}) {
|
|
|
309
310
|
const version = options.version || RUNNER_VERSION;
|
|
310
311
|
const platform = options.platform || process.platform;
|
|
311
312
|
const arch = options.arch || process.arch;
|
|
312
|
-
const home = options.home ||
|
|
313
|
+
const home = options.home || stateHome();
|
|
313
314
|
const log = options.log || console.log;
|
|
314
315
|
const asset = runnerAssetName(platform, arch, version);
|
|
315
316
|
const runnerPlatform = platform === 'darwin' ? 'osx' : platform;
|
package/lib/codex-flight.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { spawn, spawnSync } = require('child_process');
|
|
7
|
+
const { SHARED_BRIEF_RULES } = require('./brief-rules');
|
|
7
8
|
|
|
8
9
|
// Worktree flights need full file access to edit and ship the requested repo.
|
|
9
10
|
const FLIGHT_SANDBOX = 'danger-full-access';
|
|
@@ -59,7 +60,7 @@ function buildPrompt({ worktreePath, branch, brief, verifyCmd }) {
|
|
|
59
60
|
'',
|
|
60
61
|
'Instructions:',
|
|
61
62
|
`- First run: atris worktree guard`,
|
|
62
|
-
|
|
63
|
+
...SHARED_BRIEF_RULES,
|
|
63
64
|
`- Verify with this bare command and use its real exit code: ${verify}`,
|
|
64
65
|
'- Stage and commit only files you changed.',
|
|
65
66
|
`- Commit with a plain-English message and include this trailer: ${COAUTHOR}`,
|
package/lib/commerce.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Shared helpers for `atris buy` and `atris transactions`.
|
|
4
4
|
// Backend: /api/commerce/* and /api/transactions (commerce_router.py).
|
|
5
5
|
|
|
6
|
-
const { ROLLOUT_MESSAGE, oneLine } = require('./developer-api');
|
|
6
|
+
const { ROLLOUT_MESSAGE, oneLine, tokenFrom } = require('./developer-api');
|
|
7
|
+
const { decodeJwtClaims } = require('../utils/auth');
|
|
7
8
|
|
|
8
9
|
// Flags whose values the backend reads as numbers.
|
|
9
10
|
const NUMERIC_KEYS = new Set(['quantity', 'dollars', 'limit_price', 'min_each_usd', 'max_each_usd', 'limit']);
|
|
@@ -72,4 +73,18 @@ function errorFrom(result) {
|
|
|
72
73
|
return oneLine(result && result.error);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
// The commerce key (minted with `atris login --agent --commerce`) when it is
|
|
77
|
+
// present, unexpired, and carries the scope this call needs; else the login.
|
|
78
|
+
function commerceToken(creds, scope, now = Date.now()) {
|
|
79
|
+
const key = creds && typeof creds.commerce_agent_token === 'string' ? creds.commerce_agent_token.trim() : '';
|
|
80
|
+
if (key) {
|
|
81
|
+
const claims = decodeJwtClaims(key) || {};
|
|
82
|
+
const scopes = Array.isArray(claims.scopes) ? claims.scopes
|
|
83
|
+
: (Array.isArray(creds.commerce_agent_token_scopes) ? creds.commerce_agent_token_scopes : []);
|
|
84
|
+
const expiry = Number.isFinite(claims.exp) ? claims.exp * 1000 : Date.parse(creds.commerce_agent_token_expires_at || '');
|
|
85
|
+
if (scopes.includes(scope) && Number.isFinite(expiry) && expiry > now) return key;
|
|
86
|
+
}
|
|
87
|
+
return tokenFrom(creds);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { parseFlags, formatCents, untilText, errorFrom, commerceToken };
|
package/lib/deck-history.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// you track iterations of a spec over time.
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
|
-
const os = require('os');
|
|
8
7
|
const path = require('path');
|
|
8
|
+
const { stateHome } = require('./state-home');
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
|
|
11
|
-
const DEFAULT_HISTORY_PATH = path.join(
|
|
11
|
+
const DEFAULT_HISTORY_PATH = path.join(stateHome(), '.atris', 'deck-history.jsonl');
|
|
12
12
|
|
|
13
13
|
// Short, stable content hash of a spec, same spec -> same hash, so you can
|
|
14
14
|
// tell a no-op rebuild from a real change.
|
package/lib/deck-review.js
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const https = require('https');
|
|
9
|
-
const os = require('os');
|
|
10
9
|
const path = require('path');
|
|
10
|
+
const { stateHome } = require('./state-home');
|
|
11
11
|
|
|
12
12
|
const SCHEMA = 'atris.deck_review.v1';
|
|
13
13
|
const RECEIPT_SCHEMA = 'atris.deck_receipt.v1';
|
|
14
|
-
const DEFAULT_OUT_ROOT = path.join(
|
|
15
|
-
const DEFAULT_RECEIPTS_PATH = path.join(
|
|
14
|
+
const DEFAULT_OUT_ROOT = path.join(stateHome(), '.atris', 'deck-review');
|
|
15
|
+
const DEFAULT_RECEIPTS_PATH = path.join(stateHome(), '.atris', 'deck-receipts.jsonl');
|
|
16
16
|
|
|
17
17
|
const AGENT_CHECKLIST = [
|
|
18
18
|
'Heading and sub do not overlap',
|
package/lib/design-api.js
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
// POST /design/search {"query", "limit"} -> sync result
|
|
15
15
|
|
|
16
16
|
const fs = require('fs');
|
|
17
|
-
const os = require('os');
|
|
18
17
|
const path = require('path');
|
|
18
|
+
const { stateHome } = require('./state-home');
|
|
19
19
|
const { getApiBaseUrl, httpRequest } = require('../utils/api');
|
|
20
20
|
const { loadCredentials } = require('../utils/auth');
|
|
21
21
|
|
|
22
|
-
const DESIGN_KEY_FILE = path.join(
|
|
22
|
+
const DESIGN_KEY_FILE = path.join(stateHome(), '.atris', 'design-api-key');
|
|
23
23
|
|
|
24
24
|
const POLL_INTERVAL_MS = 3000;
|
|
25
25
|
const POLL_TIMEOUT_MS = 3 * 60 * 1000;
|
package/lib/engine-registry.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { stateHome } = require('./state-home');
|
|
6
7
|
const childProcess = require('child_process');
|
|
7
8
|
const crypto = require('crypto');
|
|
8
9
|
const {
|
|
@@ -155,7 +156,10 @@ const ENGINE_SEED_META = Object.freeze({
|
|
|
155
156
|
agy: Object.freeze({
|
|
156
157
|
tier: 'pro',
|
|
157
158
|
roles: Object.freeze(['executor']),
|
|
159
|
+
// The plain flash name takes its effort from --effort; the -high names
|
|
160
|
+
// bake the effort into the model.
|
|
158
161
|
models: Object.freeze([
|
|
162
|
+
'gemini-3.8-flash',
|
|
159
163
|
'gemini-3.8-flash-high',
|
|
160
164
|
'gemini-3.7-flash-high',
|
|
161
165
|
'gemini-3.1-pro-high',
|
|
@@ -402,6 +406,15 @@ const MACHINE_ROSTER_SCHEMA = 'atris.machine_roster.v1';
|
|
|
402
406
|
// The owner's own job for small, tightly specified builds. A build resolved
|
|
403
407
|
// with lowStakes checks this pick first, then the plain build pick.
|
|
404
408
|
const SMALL_BUILD_JOB = 'small-build';
|
|
409
|
+
// Jobs any engine may fill, keyed to the kind they count as when assign is
|
|
410
|
+
// not told one. YouTube notes are plain read-only text, so agy (build-only)
|
|
411
|
+
// and haiku (search and review) can both write them. Every other job keeps
|
|
412
|
+
// its kind check.
|
|
413
|
+
const ANY_ENGINE_JOBS = Object.freeze({ notes: 'search' });
|
|
414
|
+
|
|
415
|
+
function jobTakesAnyEngine(key) {
|
|
416
|
+
return Object.prototype.hasOwnProperty.call(ANY_ENGINE_JOBS, String(key || ''));
|
|
417
|
+
}
|
|
405
418
|
|
|
406
419
|
function rosterJob(name) {
|
|
407
420
|
const job = String(name || '').trim().toLowerCase();
|
|
@@ -472,10 +485,10 @@ function customRosterJobKeys(roster) {
|
|
|
472
485
|
// an engine and model that can run, and that engine (or its backup) is ready.
|
|
473
486
|
// A pick that cannot run never sets the job's kind, so it cannot block a
|
|
474
487
|
// ready all-projects pick of another kind.
|
|
475
|
-
function rosterPickLive(pick, now, registry) {
|
|
488
|
+
function rosterPickLive(pick, now, registry, key) {
|
|
476
489
|
const role = customPickRole(pick);
|
|
477
490
|
if (!role || !canonicalEngineName(pick.engine)) return false;
|
|
478
|
-
return Boolean(rosterChoiceForPick(pick, role, registry, { label: rosterJobLabel(role), now }));
|
|
491
|
+
return Boolean(rosterChoiceForPick(pick, role, registry, { label: rosterJobLabel(role), now, anyEngine: jobTakesAnyEngine(key) }));
|
|
479
492
|
}
|
|
480
493
|
|
|
481
494
|
// The role a job name resolves as: a built-in job's own role, else the kind
|
|
@@ -491,7 +504,7 @@ function rosterJobRole(name, root = process.cwd(), options = {}) {
|
|
|
491
504
|
const machine = options.machineRosterPicks || readMachineRoster(options);
|
|
492
505
|
const picks = [session && session[key], project && project[key], machine && machine[key]].map(rosterPickObject).filter(Boolean);
|
|
493
506
|
const registry = options.registry || readEngineRegistry(root, { persist: false });
|
|
494
|
-
const live = picks.find((pick) => rosterPickLive(pick, options.now, registry));
|
|
507
|
+
const live = picks.find((pick) => rosterPickLive(pick, options.now, registry, key));
|
|
495
508
|
if (live) return customPickRole(live);
|
|
496
509
|
const kind = inferJobKind(name);
|
|
497
510
|
if (kind) return ENGINE_JOBS[kind];
|
|
@@ -612,7 +625,7 @@ function routerSkipsRole(engine, role) {
|
|
|
612
625
|
function machineRosterFile(options = {}) {
|
|
613
626
|
return options.machineRosterFile
|
|
614
627
|
|| process.env.ATRIS_MACHINE_ROSTER_PATH
|
|
615
|
-
|| path.join(
|
|
628
|
+
|| path.join(stateHome(), '.atris', 'roster.json');
|
|
616
629
|
}
|
|
617
630
|
|
|
618
631
|
// The all-projects ROSTER.md. A scratch roster.json path (tests, sandboxes)
|
|
@@ -624,7 +637,7 @@ function machineRosterMarkdownFile(options = {}) {
|
|
|
624
637
|
if (options.machineRosterFile || process.env.ATRIS_MACHINE_ROSTER_PATH) {
|
|
625
638
|
return path.join(path.dirname(machineRosterFile(options)), 'ROSTER.md');
|
|
626
639
|
}
|
|
627
|
-
return path.join(
|
|
640
|
+
return path.join(stateHome(), '.atris', 'ROSTER.md');
|
|
628
641
|
}
|
|
629
642
|
|
|
630
643
|
function projectRosterFile(root = process.cwd()) {
|
|
@@ -902,7 +915,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
902
915
|
extras = [inlineMax[1], ...extras];
|
|
903
916
|
}
|
|
904
917
|
const primary = resolveRosterWords(main);
|
|
905
|
-
if (!engineCanTake(primary.engine, role)) throw new Error(`${primary.engine} cannot do ${kind} work`);
|
|
918
|
+
if (!options.anyEngine && !engineCanTake(primary.engine, role)) throw new Error(`${primary.engine} cannot do ${kind} work`);
|
|
906
919
|
let backup = null;
|
|
907
920
|
let until = '';
|
|
908
921
|
let untilYearless = '';
|
|
@@ -946,7 +959,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
946
959
|
throw new Error('a worker line takes no backup; put the backup on the next line');
|
|
947
960
|
} else if (backupMatch && !backup) {
|
|
948
961
|
backup = resolveRosterWords(backupMatch[1]);
|
|
949
|
-
if (!engineCanTake(backup.engine, role)) throw new Error(`the backup ${backup.engine} cannot do ${kind} work`);
|
|
962
|
+
if (!options.anyEngine && !engineCanTake(backup.engine, role)) throw new Error(`the backup ${backup.engine} cannot do ${kind} work`);
|
|
950
963
|
} else if (untilMatch && !until) {
|
|
951
964
|
const words = untilMatch[1].trim();
|
|
952
965
|
until = parseRosterUntilWords(words);
|
|
@@ -1112,7 +1125,7 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1112
1125
|
const workers = [];
|
|
1113
1126
|
for (const entry of lines) {
|
|
1114
1127
|
try {
|
|
1115
|
-
const worker = rosterWorkerFromValue(entry.value, ENGINE_JOBS[kind], options);
|
|
1128
|
+
const worker = rosterWorkerFromValue(entry.value, ENGINE_JOBS[kind], { ...options, anyEngine: jobTakesAnyEngine(key) });
|
|
1116
1129
|
const selfPrep = selfPrepWarning(worker, key, label);
|
|
1117
1130
|
if (selfPrep) warn(entry, selfPrep);
|
|
1118
1131
|
workers.push({ ...worker, line: entry.lineNumber, text: entry.raw });
|
|
@@ -1184,7 +1197,7 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1184
1197
|
continue;
|
|
1185
1198
|
}
|
|
1186
1199
|
try {
|
|
1187
|
-
const pick = rosterPickFromValue(entry.value, ENGINE_JOBS[kind], options);
|
|
1200
|
+
const pick = rosterPickFromValue(entry.value, ENGINE_JOBS[kind], { ...options, anyEngine: jobTakesAnyEngine(key) });
|
|
1188
1201
|
const selfPrep = selfPrepWarning(pick, key, label);
|
|
1189
1202
|
if (selfPrep) warn(entry, selfPrep);
|
|
1190
1203
|
picks[key] = { ...pick, ...(builtIn ? {} : { like: kind }), file: display, line: entry.lineNumber };
|
|
@@ -1264,7 +1277,7 @@ function sessionRosterDir(options = {}) {
|
|
|
1264
1277
|
if (options.sessionRosterDir) return options.sessionRosterDir;
|
|
1265
1278
|
if (process.env.ATRIS_ROSTER_SESSIONS_DIR) return process.env.ATRIS_ROSTER_SESSIONS_DIR;
|
|
1266
1279
|
if (options.machineRosterFile || process.env.ATRIS_MACHINE_ROSTER_PATH) return path.join(path.dirname(machineRosterFile(options)), 'sessions');
|
|
1267
|
-
return path.join(
|
|
1280
|
+
return path.join(stateHome(), '.atris', 'sessions');
|
|
1268
1281
|
}
|
|
1269
1282
|
|
|
1270
1283
|
// Like the all-projects roster: off in test runs unless a test points the
|
|
@@ -1531,13 +1544,14 @@ function assignedWorkerLines(key, engineName, options, context) {
|
|
|
1531
1544
|
if (likeName && !likeKind) throw new Error(`unknown kind "${likeName}". use --like search, build, or review`);
|
|
1532
1545
|
if (builtIn && likeKind && ENGINE_JOBS[likeKind] !== key) throw new Error(`${label} is a built-in job; --like is only for your own jobs`);
|
|
1533
1546
|
const savedKind = builtIn ? '' : Object.keys(ENGINE_JOBS).find((kind) => ENGINE_JOBS[kind] === customPickRole(savedPick)) || '';
|
|
1534
|
-
const kind = builtIn ? label : likeKind || inferJobKind(jobName) || savedKind;
|
|
1547
|
+
const kind = builtIn ? label : likeKind || inferJobKind(jobName) || savedKind || ANY_ENGINE_JOBS[key] || '';
|
|
1535
1548
|
if (!kind) throw new Error(`say what kind of job "${label}" is: add --like search, --like build, or --like review`);
|
|
1536
1549
|
const role = ENGINE_JOBS[kind];
|
|
1537
1550
|
const engineId = rosterToolName(engineName);
|
|
1538
1551
|
const engine = registry.engines.find((entry) => entry.id === engineId);
|
|
1539
1552
|
if (!engine) throw new Error(`unknown engine "${engineName}"`);
|
|
1540
|
-
|
|
1553
|
+
const anyEngine = jobTakesAnyEngine(key);
|
|
1554
|
+
if (!anyEngine && !engineHasRole(engine, role)) throw new Error(`${engine.id} cannot do ${builtIn ? label : `${kind} work, so it cannot take ${label}`}`);
|
|
1541
1555
|
// --backup takes engine words with an optional model and effort, the
|
|
1542
1556
|
// same as a written line: --backup "claude opus 5.5".
|
|
1543
1557
|
const backupName = String(options.backup || '').trim();
|
|
@@ -1552,7 +1566,7 @@ function assignedWorkerLines(key, engineName, options, context) {
|
|
|
1552
1566
|
}
|
|
1553
1567
|
const backup = backupRead ? registry.engines.find((entry) => entry.id === backupRead.engine) : null;
|
|
1554
1568
|
if (backupName && !backup) throw new Error(`unknown backup engine "${backupName}"`);
|
|
1555
|
-
if (backup && !engineHasRole(backup, role)) throw new Error(`${backup.id} cannot do ${builtIn ? label : `${kind} work, so it cannot back up ${label}`}`);
|
|
1569
|
+
if (backup && !anyEngine && !engineHasRole(backup, role)) throw new Error(`${backup.id} cannot do ${builtIn ? label : `${kind} work, so it cannot back up ${label}`}`);
|
|
1556
1570
|
// No --days means the worker holds until someone changes the line.
|
|
1557
1571
|
const days = options.days === undefined ? null : Number(options.days);
|
|
1558
1572
|
if (days !== null && (!Number.isSafeInteger(days) || days < 1)) throw new Error('days must be a positive whole number');
|
|
@@ -1745,11 +1759,12 @@ const ROSTER_LAYERS = Object.freeze(['session', 'project', 'machine']);
|
|
|
1745
1759
|
|
|
1746
1760
|
// A pick's workers, each with the engine it would run (model, effort, and
|
|
1747
1761
|
// time cap pinned) or the reason it is skipped: bad line, expired, bad model,
|
|
1748
|
-
// or not ready (down, or it cannot do this kind of work).
|
|
1749
|
-
|
|
1762
|
+
// or not ready (down, or it cannot do this kind of work). anyEngine drops
|
|
1763
|
+
// the kind check for a job any engine may fill (see ANY_ENGINE_JOBS).
|
|
1764
|
+
function rosterWorkerWalk(pick, role, registry, now, { anyEngine = false } = {}) {
|
|
1750
1765
|
const ready = registry.engines
|
|
1751
1766
|
.filter((engine) => engineReadyAt(engine, now))
|
|
1752
|
-
.filter((engine) => engineHasRole(engine, role));
|
|
1767
|
+
.filter((engine) => anyEngine || engineHasRole(engine, role));
|
|
1753
1768
|
return pickWorkers(pick).map((worker) => {
|
|
1754
1769
|
if (worker.error) return { worker, skip: 'bad line', engine: null, raw: null };
|
|
1755
1770
|
if (rosterPickExpired(worker, now)) return { worker, skip: 'expired', engine: null, raw: null };
|
|
@@ -1784,10 +1799,10 @@ function rosterWorkerWalk(pick, role, registry, now) {
|
|
|
1784
1799
|
// leads; null means the next pick in line decides. A custom pick only counts
|
|
1785
1800
|
// when its kind matches the role being resolved. team is every worker that
|
|
1786
1801
|
// can run, lead first.
|
|
1787
|
-
function rosterChoiceForPick(pick, role, registry, { key, label, source, scopeText = '', now } = {}) {
|
|
1802
|
+
function rosterChoiceForPick(pick, role, registry, { key, label, source, scopeText = '', now, anyEngine } = {}) {
|
|
1788
1803
|
if (!pick || !pick.engine) return null;
|
|
1789
1804
|
if (key !== undefined && pickRoleForKey(key, pick) !== role) return null;
|
|
1790
|
-
const walk = rosterWorkerWalk(pick, role, registry, now);
|
|
1805
|
+
const walk = rosterWorkerWalk(pick, role, registry, now, { anyEngine: anyEngine === undefined ? jobTakesAnyEngine(key) : anyEngine });
|
|
1791
1806
|
const leadIndex = walk.findIndex((step) => !step.skip);
|
|
1792
1807
|
if (leadIndex === -1) return null;
|
|
1793
1808
|
const lead = walk[leadIndex];
|
|
@@ -1942,6 +1957,55 @@ function resolveJobTeam(job, root = process.cwd(), options = {}) {
|
|
|
1942
1957
|
return { job: label, key, role, source: resolved.source, lead: resolved.engine || null, team, reason: resolved.reason };
|
|
1943
1958
|
}
|
|
1944
1959
|
|
|
1960
|
+
// Who writes YouTube notes when no roster layer names a notes job: agy on
|
|
1961
|
+
// its plain flash model at low effort, then haiku. Low effort is what keeps a
|
|
1962
|
+
// notes run near 8 seconds of writer time. Each model is a name from that
|
|
1963
|
+
// engine's seed catalog above. Change the default here and only here.
|
|
1964
|
+
const NOTES_DEFAULT_WORKERS = Object.freeze([
|
|
1965
|
+
Object.freeze({ engine: 'agy', model: 'gemini-3.8-flash', effort: 'low' }),
|
|
1966
|
+
Object.freeze({ engine: 'haiku', model: 'haiku', effort: null }),
|
|
1967
|
+
]);
|
|
1968
|
+
|
|
1969
|
+
// The notes job answered from saved state only: the roster layers in order
|
|
1970
|
+
// (this session, this project, all projects), each job's workers in order
|
|
1971
|
+
// with the roster's own skip rules (expired, cooling, not ready), else the
|
|
1972
|
+
// built-in default. installed comes from the registry's saved policy, so
|
|
1973
|
+
// this never probes the machine on a settled registry. A roster pick with
|
|
1974
|
+
// no usable worker falls through to the next layer, then to the default.
|
|
1975
|
+
function resolveNotesJob(root = process.cwd(), options = {}) {
|
|
1976
|
+
const key = 'notes';
|
|
1977
|
+
const registry = readEngineRegistry(root, { now: options.now });
|
|
1978
|
+
const state = options.rosterState || readRosterState(root, { now: options.now, registry });
|
|
1979
|
+
const installedOf = (id) => {
|
|
1980
|
+
const engine = registry.engines.find((entry) => entry.id === id);
|
|
1981
|
+
return Boolean(engine && engine.installed === true);
|
|
1982
|
+
};
|
|
1983
|
+
const named = [];
|
|
1984
|
+
for (const scope of ROSTER_LAYERS) {
|
|
1985
|
+
const pick = rosterPickObject(state[scope] && state[scope].picks && state[scope].picks[key]);
|
|
1986
|
+
if (!pick) continue;
|
|
1987
|
+
named.push(scope);
|
|
1988
|
+
const role = customPickRole(pick) || ENGINE_JOBS[ANY_ENGINE_JOBS[key]];
|
|
1989
|
+
const usable = rosterWorkerWalk(pick, role, registry, options.now, { anyEngine: true }).filter((step) => !step.skip);
|
|
1990
|
+
if (!usable.length) continue;
|
|
1991
|
+
const [lead, ...rest] = usable.map((step) => ({
|
|
1992
|
+
engine: step.engine.id,
|
|
1993
|
+
model: step.engine.roster_model || null,
|
|
1994
|
+
effort: step.engine.roster_effort || null,
|
|
1995
|
+
installed: step.raw.installed === true,
|
|
1996
|
+
}));
|
|
1997
|
+
return { job: key, ...lead, backup: rest, source: 'roster', from: ROSTER_SOURCES[scope] };
|
|
1998
|
+
}
|
|
1999
|
+
const [lead, ...rest] = NOTES_DEFAULT_WORKERS.map((worker) => ({ ...worker, installed: installedOf(worker.engine) }));
|
|
2000
|
+
return {
|
|
2001
|
+
job: key,
|
|
2002
|
+
...lead,
|
|
2003
|
+
backup: rest,
|
|
2004
|
+
source: 'default',
|
|
2005
|
+
...(named.length ? { skipped: `the notes roster pick (${named.map((scope) => ROSTER_SOURCES[scope]).join(', ')}) has no worker ready now` } : {}),
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1945
2009
|
// True when a roster line (not the router) decided.
|
|
1946
2010
|
function rosterDecided(source) {
|
|
1947
2011
|
return ROSTER_LAYERS.includes(source);
|
|
@@ -2136,6 +2200,9 @@ module.exports = {
|
|
|
2136
2200
|
rosterToolLabel,
|
|
2137
2201
|
rosterWorkerWalk,
|
|
2138
2202
|
resolveJobTeam,
|
|
2203
|
+
resolveNotesJob,
|
|
2204
|
+
NOTES_DEFAULT_WORKERS,
|
|
2205
|
+
jobTakesAnyEngine,
|
|
2139
2206
|
rosterSessionKey,
|
|
2140
2207
|
clearSessionRoster,
|
|
2141
2208
|
NO_SESSION_MESSAGE,
|
package/lib/first-minute.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { stateHome } = require('./state-home');
|
|
6
7
|
const { spawnSync } = require('child_process');
|
|
7
8
|
const { isGenericScratchRoot } = require('./scratch-root');
|
|
8
9
|
|
|
@@ -57,8 +58,7 @@ function unixUser(env = process.env) {
|
|
|
57
58
|
|
|
58
59
|
function loadSavedAccount() {
|
|
59
60
|
try {
|
|
60
|
-
const
|
|
61
|
-
const file = path.join(home, '.atris', 'credentials.json');
|
|
61
|
+
const file = path.join(stateHome(), '.atris', 'credentials.json');
|
|
62
62
|
if (!fs.existsSync(file)) return null;
|
|
63
63
|
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
64
64
|
return parsed && typeof parsed === 'object' ? parsed : null;
|
package/lib/fleet.js
CHANGED
|
@@ -36,11 +36,13 @@ const { listWorktrees, defaultMainlineBase } = require('../commands/worktree');
|
|
|
36
36
|
const { isConductorStatusLine } = require('./conductor-artifacts');
|
|
37
37
|
const { matchLessons } = require('./lesson-preflight');
|
|
38
38
|
const { sleepSync } = require('./sleep-sync');
|
|
39
|
+
const { trackedSandboxPidsSync } = require('./sandbox-pids');
|
|
39
40
|
const { ownerIdentity } = require('../utils/owner-identity');
|
|
40
41
|
const { matchTaste } = require('./taste-lessons');
|
|
41
42
|
const { buildVerifiedScoutPack, appendVerifiedScoutPack } = require('./dispatch-scout');
|
|
42
43
|
const { runPrepPass, withPrepBrief, prepRecordFields } = require('./roster-prep');
|
|
43
44
|
const { mapNotesPath } = require('./map-refs');
|
|
45
|
+
const { SHARED_BRIEF_RULES } = require('./brief-rules');
|
|
44
46
|
const {
|
|
45
47
|
applySecretGrantEnvironment,
|
|
46
48
|
buildGatewaySupervisorScript,
|
|
@@ -117,7 +119,7 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
|
|
|
117
119
|
mapNotesPath(worktreePath || process.cwd())
|
|
118
120
|
? '- Read atris/MAP.md first to locate the code, then atris/refs/MAP-NOTES.md for exact file:line refs; never guess file locations.'
|
|
119
121
|
: '- Read atris/MAP.md first to locate the code; never guess file locations.',
|
|
120
|
-
|
|
122
|
+
...SHARED_BRIEF_RULES,
|
|
121
123
|
'- Run git status first. Stage ONLY files you changed. Never revert or touch files another agent modified.',
|
|
122
124
|
'- Include or update a focused regression test; run it with npm run test:fast && node --test <focused files> before committing.',
|
|
123
125
|
`- ${commitRule}`,
|
|
@@ -284,31 +286,11 @@ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = fal
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
function trackedSandboxPids(stateFile, leaseFile) {
|
|
287
|
-
const pids = new Set();
|
|
288
289
|
let state = {};
|
|
289
290
|
try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch {}
|
|
290
291
|
const pgid = Number(state.pgid);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
for (const value of String(grouped.stdout || '').trim().split(/\s+/)) {
|
|
294
|
-
const pid = Number(value);
|
|
295
|
-
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
const leased = spawnSync('/usr/sbin/lsof', ['-t', leaseFile], { encoding: 'utf8' });
|
|
299
|
-
for (const value of String(leased.stdout || '').trim().split(/\s+/)) {
|
|
300
|
-
const pid = Number(value);
|
|
301
|
-
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
302
|
-
}
|
|
303
|
-
if (state.cwd) {
|
|
304
|
-
const rooted = spawnSync('/usr/sbin/lsof', ['-a', '-d', 'cwd', '+D', String(state.cwd), '-t'], { encoding: 'utf8' });
|
|
305
|
-
for (const value of String(rooted.stdout || '').trim().split(/\s+/)) {
|
|
306
|
-
const pid = Number(value);
|
|
307
|
-
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
pids.delete(process.pid);
|
|
311
|
-
return { pgid, pids: [...pids] };
|
|
292
|
+
const pids = trackedSandboxPidsSync({ pgid, leaseFile, cwd: state.cwd ? String(state.cwd) : '' });
|
|
293
|
+
return { pgid, pids };
|
|
312
294
|
}
|
|
313
295
|
|
|
314
296
|
function terminateTrackedSandbox(stateFile, leaseFile) {
|
|
@@ -480,6 +462,7 @@ function runInReapedProcessGroup(executable, args, options, controlDir, statusFi
|
|
|
480
462
|
"'use strict';",
|
|
481
463
|
"const fs = require('node:fs');",
|
|
482
464
|
"const { spawn, spawnSync } = require('node:child_process');",
|
|
465
|
+
`const sandboxPids = require(${JSON.stringify(require.resolve('./sandbox-pids'))});`,
|
|
483
466
|
"const [stateFile, leaseFile, statusFile, executable, ...args] = process.argv.slice(2);",
|
|
484
467
|
"const leaseFd = fs.openSync(leaseFile, 'w', 0o600);",
|
|
485
468
|
"const statusFd = fs.openSync(statusFile, 'w', 0o600);",
|
|
@@ -489,18 +472,7 @@ function runInReapedProcessGroup(executable, args, options, controlDir, statusFi
|
|
|
489
472
|
"fs.writeFileSync(stateFile, JSON.stringify({ pgid: child.pid, cwd: process.cwd() }) + '\\n', { mode: 0o600 });",
|
|
490
473
|
"let stopping = false;",
|
|
491
474
|
"const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
|
|
492
|
-
"
|
|
493
|
-
" const pids = new Set();",
|
|
494
|
-
" for (const [bin, argv] of [['/usr/bin/pgrep', ['-g', String(child.pid)]], ['/usr/sbin/lsof', ['-t', leaseFile]], ['/usr/sbin/lsof', ['-a', '-d', 'cwd', '+D', process.cwd(), '-t']]]) {",
|
|
495
|
-
" const found = spawnSync(bin, argv, { encoding: 'utf8' });",
|
|
496
|
-
" for (const value of String(found.stdout || '').trim().split(/\\s+/)) {",
|
|
497
|
-
" const pid = Number(value);",
|
|
498
|
-
" if (Number.isInteger(pid) && pid > 0) pids.add(pid);",
|
|
499
|
-
" }",
|
|
500
|
-
" }",
|
|
501
|
-
" pids.delete(process.pid);",
|
|
502
|
-
" return [...pids];",
|
|
503
|
-
"}",
|
|
475
|
+
"const trackedPids = () => sandboxPids.trackedSandboxPids({ pgid: child.pid, leaseFile, cwd: process.cwd() });",
|
|
504
476
|
"async function stop(code) {",
|
|
505
477
|
" if (stopping) return;",
|
|
506
478
|
" stopping = true;",
|
|
@@ -509,7 +481,7 @@ function runInReapedProcessGroup(executable, args, options, controlDir, statusFi
|
|
|
509
481
|
" }",
|
|
510
482
|
" for (const [signal, delay] of [['SIGTERM', 100], ['SIGKILL', 100], ['SIGKILL', 100]]) {",
|
|
511
483
|
" try { process.kill(-child.pid, signal); } catch {}",
|
|
512
|
-
" for (const pid of trackedPids()) { try { process.kill(pid, signal); } catch {} }",
|
|
484
|
+
" for (const pid of await trackedPids()) { try { process.kill(pid, signal); } catch {} }",
|
|
513
485
|
" await wait(delay);",
|
|
514
486
|
" }",
|
|
515
487
|
" let exitCode = Number.isInteger(code) ? code : 128;",
|
package/lib/git-spawn.js
CHANGED
|
@@ -4,8 +4,11 @@ const { spawnSync } = require('child_process');
|
|
|
4
4
|
|
|
5
5
|
// One synchronous git runner for commands that shell out to git.
|
|
6
6
|
// check: true throws on a non-zero exit; check: false hands back the raw result.
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// input, when given, is written to git's stdin (for --stdin batch reads).
|
|
8
|
+
function runGit(args, { cwd = process.cwd(), check = true, maxBuffer, input } = {}) {
|
|
9
|
+
const result = spawnSync('git', args, {
|
|
10
|
+
cwd, encoding: 'utf8', ...(maxBuffer ? { maxBuffer } : {}), ...(input !== undefined ? { input } : {}),
|
|
11
|
+
});
|
|
9
12
|
if (check && result.status !== 0) {
|
|
10
13
|
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
|
11
14
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Speed caches that must never be committed. They live in .atris/cache, a
|
|
4
|
+
// folder that carries its own .gitignore of `*`, so a project with no ignore
|
|
5
|
+
// rule for .atris still never stages them with `git add -A`. Caches are only
|
|
6
|
+
// kept in folders that already have .atris; elsewhere nothing is written.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const CACHE_DIR = path.join('.atris', 'cache');
|
|
12
|
+
|
|
13
|
+
function cacheEnabled(root) {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(path.join(root, '.atris')).isDirectory();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cachePath(root, name) {
|
|
22
|
+
return path.join(root, CACHE_DIR, name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The parsed cache file, or null when it is missing or unreadable.
|
|
26
|
+
function readCacheJson(root, name) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(cachePath(root, name), 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Returns false when the write failed; a cache is only a speedup.
|
|
35
|
+
function writeCacheJson(root, name, value) {
|
|
36
|
+
const dir = path.join(root, CACHE_DIR);
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
const ignore = path.join(dir, '.gitignore');
|
|
40
|
+
if (!fs.existsSync(ignore)) fs.writeFileSync(ignore, '*\n');
|
|
41
|
+
const file = cachePath(root, name);
|
|
42
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
43
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value)}\n`);
|
|
44
|
+
fs.renameSync(tmp, file);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { CACHE_DIR, cacheEnabled, cachePath, readCacheJson, writeCacheJson };
|
package/lib/manifest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const
|
|
4
|
+
const { stateHome } = require('./state-home');
|
|
5
5
|
const { computeContentHash } = require('./journal');
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -9,7 +9,7 @@ const { computeContentHash } = require('./journal');
|
|
|
9
9
|
* Stored at ~/.atris/businesses/{slug}/manifest.json
|
|
10
10
|
*/
|
|
11
11
|
function getManifestPath(slug) {
|
|
12
|
-
return path.join(
|
|
12
|
+
return path.join(stateHome(), '.atris', 'businesses', slug, 'manifest.json');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|