atris 3.62.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/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +120 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +22 -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 +242 -0
- 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 +82 -15
- package/commands/mission.js +4 -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 +91 -2
- package/commands/transactions.js +115 -0
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +24 -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 +90 -0
- 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 +102 -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/known-commands.js +1 -1
- package/lib/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/member-engine.js +15 -1
- package/lib/member-scaffold.js +2 -0
- package/lib/member-type.js +27 -0
- 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 +54 -3
- 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 +122 -16
- package/scripts/det/ytrail-bench.js +215 -0
- package/scripts/det/ytrail-eval.js +128 -56
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
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
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared helpers for `atris buy` and `atris transactions`.
|
|
4
|
+
// Backend: /api/commerce/* and /api/transactions (commerce_router.py).
|
|
5
|
+
|
|
6
|
+
const { ROLLOUT_MESSAGE, oneLine, tokenFrom } = require('./developer-api');
|
|
7
|
+
const { decodeJwtClaims } = require('../utils/auth');
|
|
8
|
+
|
|
9
|
+
// Flags whose values the backend reads as numbers.
|
|
10
|
+
const NUMERIC_KEYS = new Set(['quantity', 'dollars', 'limit_price', 'min_each_usd', 'max_each_usd', 'limit']);
|
|
11
|
+
|
|
12
|
+
function snake(key) {
|
|
13
|
+
return String(key).replace(/-/g, '_');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function coerce(key, value) {
|
|
17
|
+
if (value === true) return true;
|
|
18
|
+
const text = String(value);
|
|
19
|
+
if (/^[[{]/.test(text.trim())) {
|
|
20
|
+
try { return JSON.parse(text); } catch { /* keep as text */ }
|
|
21
|
+
}
|
|
22
|
+
if (NUMERIC_KEYS.has(key) && text.trim() !== '' && Number.isFinite(Number(text))) return Number(text);
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --event-url x --quantity 2 --accept-extras -> { event_url: 'x', quantity: 2, accept_extras: true }
|
|
27
|
+
function parseFlags(argv = []) {
|
|
28
|
+
const flags = {};
|
|
29
|
+
const pos = [];
|
|
30
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
31
|
+
const arg = String(argv[i]);
|
|
32
|
+
if (arg === '-h') { flags.help = true; continue; }
|
|
33
|
+
if (arg.startsWith('--')) {
|
|
34
|
+
const eq = arg.indexOf('=');
|
|
35
|
+
const rawKey = eq > 0 ? arg.slice(2, eq) : arg.slice(2);
|
|
36
|
+
const key = snake(rawKey);
|
|
37
|
+
if (eq > 0) { flags[key] = coerce(key, arg.slice(eq + 1)); continue; }
|
|
38
|
+
const next = argv[i + 1];
|
|
39
|
+
if (next != null && !String(next).startsWith('--')) { flags[key] = coerce(key, next); i += 1; } else flags[key] = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
pos.push(arg);
|
|
43
|
+
}
|
|
44
|
+
return { flags, pos };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatCents(cents, currency = 'usd') {
|
|
48
|
+
if (cents == null || cents === '' || !Number.isFinite(Number(cents))) return '-';
|
|
49
|
+
const n = Number(cents) / 100;
|
|
50
|
+
const sign = n < 0 ? '-' : '';
|
|
51
|
+
const body = `$${Math.abs(n).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
|
|
52
|
+
const code = String(currency || 'usd').toLowerCase();
|
|
53
|
+
return code === 'usd' ? `${sign}${body}` : `${sign}${body} ${code.toUpperCase()}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// "2026-09-29T18:04:00Z" -> "in 30 min" / "expired"
|
|
57
|
+
function untilText(iso, now = Date.now()) {
|
|
58
|
+
const at = Date.parse(iso);
|
|
59
|
+
if (!Number.isFinite(at)) return '';
|
|
60
|
+
const mins = Math.round((at - now) / 60000);
|
|
61
|
+
if (mins <= 0) return 'expired';
|
|
62
|
+
if (mins < 90) return `in ${mins} min`;
|
|
63
|
+
return `in ${Math.round(mins / 60)} h`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The backend's error envelope is {"error": {"message", "type"}, "atris": {...}}.
|
|
67
|
+
// A bare 404 with no envelope means the route itself is not deployed yet.
|
|
68
|
+
function errorFrom(result) {
|
|
69
|
+
const data = result && result.data;
|
|
70
|
+
const envelope = data && typeof data === 'object' && data.error && typeof data.error === 'object';
|
|
71
|
+
if (result && Number(result.status) === 404 && !envelope) return ROLLOUT_MESSAGE;
|
|
72
|
+
if (envelope) return oneLine(data.error.message || data.error);
|
|
73
|
+
return oneLine(result && result.error);
|
|
74
|
+
}
|
|
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()) {
|
|
@@ -860,6 +873,22 @@ function resolveEngineModelWords(text) {
|
|
|
860
873
|
throw new Error(`"${clean}" is not an engine or a model atris knows`);
|
|
861
874
|
}
|
|
862
875
|
|
|
876
|
+
// The engine a model name belongs to, read the same way a roster line is,
|
|
877
|
+
// or '' when the name is not one atris can place. The claude cli's
|
|
878
|
+
// own aliases ("default", "opusplan", with or without [1m]) count as claude.
|
|
879
|
+
function modelEngineName(model) {
|
|
880
|
+
const base = String(model || '').trim().replace(CLAUDE_LONG_CONTEXT, '').trim();
|
|
881
|
+
try {
|
|
882
|
+
return resolveEngineModelWords(base).engine || '';
|
|
883
|
+
} catch {
|
|
884
|
+
return CLAUDE_MODEL_ALIASES.includes(base.toLowerCase()) ? 'claude' : '';
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function isClaudeFamilyModel(model) {
|
|
889
|
+
return CLAUDE_FAMILY_ENGINES.includes(modelEngineName(model));
|
|
890
|
+
}
|
|
891
|
+
|
|
863
892
|
// What an engine can do by its seed, used to check hand-written lines before
|
|
864
893
|
// any registry is read.
|
|
865
894
|
function engineCanTake(engineId, role) {
|
|
@@ -886,7 +915,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
886
915
|
extras = [inlineMax[1], ...extras];
|
|
887
916
|
}
|
|
888
917
|
const primary = resolveRosterWords(main);
|
|
889
|
-
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`);
|
|
890
919
|
let backup = null;
|
|
891
920
|
let until = '';
|
|
892
921
|
let untilYearless = '';
|
|
@@ -930,7 +959,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
930
959
|
throw new Error('a worker line takes no backup; put the backup on the next line');
|
|
931
960
|
} else if (backupMatch && !backup) {
|
|
932
961
|
backup = resolveRosterWords(backupMatch[1]);
|
|
933
|
-
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`);
|
|
934
963
|
} else if (untilMatch && !until) {
|
|
935
964
|
const words = untilMatch[1].trim();
|
|
936
965
|
until = parseRosterUntilWords(words);
|
|
@@ -1096,7 +1125,7 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1096
1125
|
const workers = [];
|
|
1097
1126
|
for (const entry of lines) {
|
|
1098
1127
|
try {
|
|
1099
|
-
const worker = rosterWorkerFromValue(entry.value, ENGINE_JOBS[kind], options);
|
|
1128
|
+
const worker = rosterWorkerFromValue(entry.value, ENGINE_JOBS[kind], { ...options, anyEngine: jobTakesAnyEngine(key) });
|
|
1100
1129
|
const selfPrep = selfPrepWarning(worker, key, label);
|
|
1101
1130
|
if (selfPrep) warn(entry, selfPrep);
|
|
1102
1131
|
workers.push({ ...worker, line: entry.lineNumber, text: entry.raw });
|
|
@@ -1168,7 +1197,7 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1168
1197
|
continue;
|
|
1169
1198
|
}
|
|
1170
1199
|
try {
|
|
1171
|
-
const pick = rosterPickFromValue(entry.value, ENGINE_JOBS[kind], options);
|
|
1200
|
+
const pick = rosterPickFromValue(entry.value, ENGINE_JOBS[kind], { ...options, anyEngine: jobTakesAnyEngine(key) });
|
|
1172
1201
|
const selfPrep = selfPrepWarning(pick, key, label);
|
|
1173
1202
|
if (selfPrep) warn(entry, selfPrep);
|
|
1174
1203
|
picks[key] = { ...pick, ...(builtIn ? {} : { like: kind }), file: display, line: entry.lineNumber };
|
|
@@ -1248,7 +1277,7 @@ function sessionRosterDir(options = {}) {
|
|
|
1248
1277
|
if (options.sessionRosterDir) return options.sessionRosterDir;
|
|
1249
1278
|
if (process.env.ATRIS_ROSTER_SESSIONS_DIR) return process.env.ATRIS_ROSTER_SESSIONS_DIR;
|
|
1250
1279
|
if (options.machineRosterFile || process.env.ATRIS_MACHINE_ROSTER_PATH) return path.join(path.dirname(machineRosterFile(options)), 'sessions');
|
|
1251
|
-
return path.join(
|
|
1280
|
+
return path.join(stateHome(), '.atris', 'sessions');
|
|
1252
1281
|
}
|
|
1253
1282
|
|
|
1254
1283
|
// Like the all-projects roster: off in test runs unless a test points the
|
|
@@ -1515,13 +1544,14 @@ function assignedWorkerLines(key, engineName, options, context) {
|
|
|
1515
1544
|
if (likeName && !likeKind) throw new Error(`unknown kind "${likeName}". use --like search, build, or review`);
|
|
1516
1545
|
if (builtIn && likeKind && ENGINE_JOBS[likeKind] !== key) throw new Error(`${label} is a built-in job; --like is only for your own jobs`);
|
|
1517
1546
|
const savedKind = builtIn ? '' : Object.keys(ENGINE_JOBS).find((kind) => ENGINE_JOBS[kind] === customPickRole(savedPick)) || '';
|
|
1518
|
-
const kind = builtIn ? label : likeKind || inferJobKind(jobName) || savedKind;
|
|
1547
|
+
const kind = builtIn ? label : likeKind || inferJobKind(jobName) || savedKind || ANY_ENGINE_JOBS[key] || '';
|
|
1519
1548
|
if (!kind) throw new Error(`say what kind of job "${label}" is: add --like search, --like build, or --like review`);
|
|
1520
1549
|
const role = ENGINE_JOBS[kind];
|
|
1521
1550
|
const engineId = rosterToolName(engineName);
|
|
1522
1551
|
const engine = registry.engines.find((entry) => entry.id === engineId);
|
|
1523
1552
|
if (!engine) throw new Error(`unknown engine "${engineName}"`);
|
|
1524
|
-
|
|
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}`}`);
|
|
1525
1555
|
// --backup takes engine words with an optional model and effort, the
|
|
1526
1556
|
// same as a written line: --backup "claude opus 5.5".
|
|
1527
1557
|
const backupName = String(options.backup || '').trim();
|
|
@@ -1536,7 +1566,7 @@ function assignedWorkerLines(key, engineName, options, context) {
|
|
|
1536
1566
|
}
|
|
1537
1567
|
const backup = backupRead ? registry.engines.find((entry) => entry.id === backupRead.engine) : null;
|
|
1538
1568
|
if (backupName && !backup) throw new Error(`unknown backup engine "${backupName}"`);
|
|
1539
|
-
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}`}`);
|
|
1540
1570
|
// No --days means the worker holds until someone changes the line.
|
|
1541
1571
|
const days = options.days === undefined ? null : Number(options.days);
|
|
1542
1572
|
if (days !== null && (!Number.isSafeInteger(days) || days < 1)) throw new Error('days must be a positive whole number');
|
|
@@ -1729,11 +1759,12 @@ const ROSTER_LAYERS = Object.freeze(['session', 'project', 'machine']);
|
|
|
1729
1759
|
|
|
1730
1760
|
// A pick's workers, each with the engine it would run (model, effort, and
|
|
1731
1761
|
// time cap pinned) or the reason it is skipped: bad line, expired, bad model,
|
|
1732
|
-
// or not ready (down, or it cannot do this kind of work).
|
|
1733
|
-
|
|
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 } = {}) {
|
|
1734
1765
|
const ready = registry.engines
|
|
1735
1766
|
.filter((engine) => engineReadyAt(engine, now))
|
|
1736
|
-
.filter((engine) => engineHasRole(engine, role));
|
|
1767
|
+
.filter((engine) => anyEngine || engineHasRole(engine, role));
|
|
1737
1768
|
return pickWorkers(pick).map((worker) => {
|
|
1738
1769
|
if (worker.error) return { worker, skip: 'bad line', engine: null, raw: null };
|
|
1739
1770
|
if (rosterPickExpired(worker, now)) return { worker, skip: 'expired', engine: null, raw: null };
|
|
@@ -1768,10 +1799,10 @@ function rosterWorkerWalk(pick, role, registry, now) {
|
|
|
1768
1799
|
// leads; null means the next pick in line decides. A custom pick only counts
|
|
1769
1800
|
// when its kind matches the role being resolved. team is every worker that
|
|
1770
1801
|
// can run, lead first.
|
|
1771
|
-
function rosterChoiceForPick(pick, role, registry, { key, label, source, scopeText = '', now } = {}) {
|
|
1802
|
+
function rosterChoiceForPick(pick, role, registry, { key, label, source, scopeText = '', now, anyEngine } = {}) {
|
|
1772
1803
|
if (!pick || !pick.engine) return null;
|
|
1773
1804
|
if (key !== undefined && pickRoleForKey(key, pick) !== role) return null;
|
|
1774
|
-
const walk = rosterWorkerWalk(pick, role, registry, now);
|
|
1805
|
+
const walk = rosterWorkerWalk(pick, role, registry, now, { anyEngine: anyEngine === undefined ? jobTakesAnyEngine(key) : anyEngine });
|
|
1775
1806
|
const leadIndex = walk.findIndex((step) => !step.skip);
|
|
1776
1807
|
if (leadIndex === -1) return null;
|
|
1777
1808
|
const lead = walk[leadIndex];
|
|
@@ -1926,6 +1957,55 @@ function resolveJobTeam(job, root = process.cwd(), options = {}) {
|
|
|
1926
1957
|
return { job: label, key, role, source: resolved.source, lead: resolved.engine || null, team, reason: resolved.reason };
|
|
1927
1958
|
}
|
|
1928
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
|
+
|
|
1929
2009
|
// True when a roster line (not the router) decided.
|
|
1930
2010
|
function rosterDecided(source) {
|
|
1931
2011
|
return ROSTER_LAYERS.includes(source);
|
|
@@ -2107,6 +2187,7 @@ module.exports = {
|
|
|
2107
2187
|
setRosterPick,
|
|
2108
2188
|
confirmRoster,
|
|
2109
2189
|
normalizeRosterModel,
|
|
2190
|
+
isClaudeFamilyModel,
|
|
2110
2191
|
rosterModelLabel,
|
|
2111
2192
|
parseRosterUntil,
|
|
2112
2193
|
rosterPickExpired,
|
|
@@ -2119,6 +2200,9 @@ module.exports = {
|
|
|
2119
2200
|
rosterToolLabel,
|
|
2120
2201
|
rosterWorkerWalk,
|
|
2121
2202
|
resolveJobTeam,
|
|
2203
|
+
resolveNotesJob,
|
|
2204
|
+
NOTES_DEFAULT_WORKERS,
|
|
2205
|
+
jobTakesAnyEngine,
|
|
2122
2206
|
rosterSessionKey,
|
|
2123
2207
|
clearSessionRoster,
|
|
2124
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/known-commands.js
CHANGED
|
@@ -4,7 +4,7 @@ const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'work
|
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
5
|
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
6
|
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
|
|
7
|
-
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
7
|
+
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'buy', 'transactions', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi'];
|
|
9
9
|
|
|
10
10
|
// Damerau-Levenshtein edit distance between two short strings. Counts an
|
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
|
/**
|