atris 3.63.0 → 3.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- 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 +28 -2
- package/cli/atris_code.py +25 -1
- package/commands/agent-spawn.js +1 -0
- 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 +292 -65
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/hire.js +189 -0
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/mail.js +163 -0
- package/commands/member.js +47 -9
- package/commands/mission.js +4 -3
- 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/stream.js +22 -1
- package/commands/task.js +84 -2
- package/commands/team.js +355 -67
- 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/auto-accept-certified.js +83 -9
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/business-target.js +131 -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-processes.js +430 -0
- package/lib/engine-registry.js +101 -21
- 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-identity.js +56 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-models.js +11 -0
- package/lib/roster-runs.js +13 -2
- package/lib/roster-stale.js +241 -0
- 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/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -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
package/commands/x-search.js
CHANGED
|
@@ -4,7 +4,9 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { apiRequestJson } = require('../utils/api');
|
|
6
6
|
const { ensureBilledCommandAuth } = require('./auth');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
7
8
|
const applyGate = require('../lib/apply-gate');
|
|
9
|
+
const { checkXPosts, annotateContent, formatTally } = require('../lib/x-post-check');
|
|
8
10
|
const {
|
|
9
11
|
fileTeachExperiment,
|
|
10
12
|
extractTeachNumbers,
|
|
@@ -23,9 +25,10 @@ const KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the f
|
|
|
23
25
|
|
|
24
26
|
function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
25
27
|
output('');
|
|
26
|
-
output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--save] [--json]`);
|
|
27
|
-
output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--save] [--json]`);
|
|
28
|
+
output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--save] [--json] [--no-check]`);
|
|
29
|
+
output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--save] [--json] [--no-check]`);
|
|
28
30
|
output(` ${commandName} unsave <query-or-source>`);
|
|
31
|
+
output(` ${commandName} bench [--quick|--free]`);
|
|
29
32
|
output('');
|
|
30
33
|
output(`Search X/Twitter via Atris (${COST_HINT}).`);
|
|
31
34
|
output('Requires login. Same auth path as atris youtube process.');
|
|
@@ -33,13 +36,15 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
|
33
36
|
output('--save files a brief only when the result is rich.');
|
|
34
37
|
output('unsave deletes the filed brief, apply stub, and matching experiment pack (no paid calls).');
|
|
35
38
|
output('Empty or failed search prints credits refunded only when the server marks a refund.');
|
|
39
|
+
output('Each quoted post is looked up on X for free and marked checked, unverified, or unknown.');
|
|
36
40
|
output('');
|
|
37
41
|
output('Options:');
|
|
38
42
|
output(' --limit <n> Max results hint (search only)');
|
|
39
43
|
output(' --days <n> Only tweets from the last N days (search only)');
|
|
40
44
|
output(' --save File brief, journal, apply; rich results mint a keep/revert experiment');
|
|
41
45
|
output(' --unsave Delete filed brief, apply stub, and matching experiment pack (no paid calls)');
|
|
42
|
-
output(' --json Print the raw JSON response');
|
|
46
|
+
output(' --json Print the raw JSON response (plus a checks list)');
|
|
47
|
+
output(' --no-check Skip looking up the quoted posts on X');
|
|
43
48
|
output(' -h, --help This help');
|
|
44
49
|
output('');
|
|
45
50
|
output('Person options:');
|
|
@@ -55,6 +60,7 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
|
55
60
|
output(` ${commandName} person --name "Leah Bonvissuto" --handle leahbon`);
|
|
56
61
|
output(` ${commandName} unsave "MCP agents"`);
|
|
57
62
|
output(` ${commandName} --unsave "MCP agents"`);
|
|
63
|
+
output(` ${commandName} bench --free`);
|
|
58
64
|
output('');
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -81,6 +87,7 @@ function parseSearchArgs(argv = []) {
|
|
|
81
87
|
json: false,
|
|
82
88
|
save: false,
|
|
83
89
|
unsave: false,
|
|
90
|
+
check: true,
|
|
84
91
|
query: null,
|
|
85
92
|
limit: null,
|
|
86
93
|
daysBack: null,
|
|
@@ -100,6 +107,8 @@ function parseSearchArgs(argv = []) {
|
|
|
100
107
|
options.json = true;
|
|
101
108
|
} else if (arg === '--save') {
|
|
102
109
|
options.save = true;
|
|
110
|
+
} else if (arg === '--no-check') {
|
|
111
|
+
options.check = false;
|
|
103
112
|
} else if (arg === '--unsave') {
|
|
104
113
|
options.unsave = true;
|
|
105
114
|
} else if (arg === '--limit') {
|
|
@@ -153,6 +162,7 @@ function parsePersonArgs(argv = []) {
|
|
|
153
162
|
help: false,
|
|
154
163
|
json: false,
|
|
155
164
|
save: false,
|
|
165
|
+
check: true,
|
|
156
166
|
name: null,
|
|
157
167
|
handle: null,
|
|
158
168
|
company: null,
|
|
@@ -173,6 +183,8 @@ function parsePersonArgs(argv = []) {
|
|
|
173
183
|
options.json = true;
|
|
174
184
|
} else if (arg === '--save') {
|
|
175
185
|
options.save = true;
|
|
186
|
+
} else if (arg === '--no-check') {
|
|
187
|
+
options.check = false;
|
|
176
188
|
} else if (arg === '--name') {
|
|
177
189
|
options.name = readValue(args, i, arg);
|
|
178
190
|
i++;
|
|
@@ -592,23 +604,26 @@ function ensureXSearchApply({ cwd, source, packRel, now, output } = {}) {
|
|
|
592
604
|
});
|
|
593
605
|
}
|
|
594
606
|
|
|
595
|
-
|
|
607
|
+
// check is the result of checkXPosts. With it, each post carries its own
|
|
608
|
+
// link or warning, and citations no post uses are listed as other sources.
|
|
609
|
+
function formatXSearchResult(data, check = null) {
|
|
596
610
|
const lines = [];
|
|
597
611
|
const content = xSearchContent(data);
|
|
598
612
|
const citations = xSearchCitations(data);
|
|
599
613
|
|
|
600
614
|
if (content) {
|
|
601
|
-
lines.push(content);
|
|
615
|
+
lines.push(check ? annotateContent(content, check) : content);
|
|
602
616
|
} else if (data?.message) {
|
|
603
617
|
lines.push(String(data.message).trim());
|
|
604
618
|
} else {
|
|
605
619
|
lines.push('X search completed');
|
|
606
620
|
}
|
|
607
621
|
|
|
608
|
-
|
|
622
|
+
const listed = check ? check.otherSources : citations;
|
|
623
|
+
if (listed.length) {
|
|
609
624
|
lines.push('');
|
|
610
|
-
lines.push('Citations:');
|
|
611
|
-
for (const cite of
|
|
625
|
+
lines.push(check ? 'Other sources:' : 'Citations:');
|
|
626
|
+
for (const cite of listed) {
|
|
612
627
|
lines.push(` ${cite}`);
|
|
613
628
|
}
|
|
614
629
|
}
|
|
@@ -619,9 +634,54 @@ function formatXSearchResult(data) {
|
|
|
619
634
|
lines.push(...creditLines);
|
|
620
635
|
}
|
|
621
636
|
|
|
637
|
+
if (check) {
|
|
638
|
+
lines.push('');
|
|
639
|
+
lines.push(formatTally(check.tally));
|
|
640
|
+
}
|
|
641
|
+
|
|
622
642
|
return lines.join('\n');
|
|
623
643
|
}
|
|
624
644
|
|
|
645
|
+
// Look the quoted posts up on X. Free, and never fatal: any failure means no
|
|
646
|
+
// check, and the original output prints unchanged.
|
|
647
|
+
async function checkXSearchPosts(data, options, deps = {}) {
|
|
648
|
+
if (options.check === false) return null;
|
|
649
|
+
try {
|
|
650
|
+
return await (deps.checkXPosts || checkXPosts)({
|
|
651
|
+
content: xSearchContent(data),
|
|
652
|
+
citations: xSearchCitations(data),
|
|
653
|
+
fetchPost: deps.fetchPost,
|
|
654
|
+
fetch: deps.fetch,
|
|
655
|
+
});
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function renderXSearchResult(data, check) {
|
|
662
|
+
if (check) {
|
|
663
|
+
try {
|
|
664
|
+
return formatXSearchResult(data, check);
|
|
665
|
+
} catch {
|
|
666
|
+
// fall through to the original output
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return formatXSearchResult(data);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function runXSearchBench(argv, deps = {}) {
|
|
673
|
+
const run = (deps.spawnSync || spawnSync)(
|
|
674
|
+
process.execPath,
|
|
675
|
+
[path.join(__dirname, '..', 'scripts', 'det', 'xsearch-bench.js'), ...argv],
|
|
676
|
+
{ stdio: 'inherit' },
|
|
677
|
+
);
|
|
678
|
+
if (run.error) {
|
|
679
|
+
(deps.output || console.error)(`bench did not start: ${run.error.message}`);
|
|
680
|
+
return 1;
|
|
681
|
+
}
|
|
682
|
+
return typeof run.status === 'number' ? run.status : 1;
|
|
683
|
+
}
|
|
684
|
+
|
|
625
685
|
function formatEmptyXSearchResult(data) {
|
|
626
686
|
const lines = ['no results'];
|
|
627
687
|
const creditLines = formatCreditsLines(xSearchCredits(data));
|
|
@@ -634,6 +694,11 @@ function formatEmptyXSearchResult(data) {
|
|
|
634
694
|
|
|
635
695
|
async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
|
|
636
696
|
const output = deps.output || ((line = '') => console.log(line));
|
|
697
|
+
if (argv[0] === 'bench') {
|
|
698
|
+
const code = runXSearchBench(argv.slice(1), deps);
|
|
699
|
+
if (!deps.output && !deps.spawnSync) process.exit(code);
|
|
700
|
+
return code;
|
|
701
|
+
}
|
|
637
702
|
let options;
|
|
638
703
|
try {
|
|
639
704
|
options = parseXSearchArgs(argv);
|
|
@@ -655,10 +720,14 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
655
720
|
try {
|
|
656
721
|
const data = await runXSearch(options, deps);
|
|
657
722
|
const hasResults = xSearchHasResults(data);
|
|
723
|
+
const check = hasResults ? await checkXSearchPosts(data, options, deps) : null;
|
|
658
724
|
if (options.json) {
|
|
659
|
-
|
|
725
|
+
const withChecks = check && data && typeof data === 'object' && !Array.isArray(data)
|
|
726
|
+
? { ...data, checks: check.checks }
|
|
727
|
+
: data;
|
|
728
|
+
output(JSON.stringify(withChecks, null, 2));
|
|
660
729
|
} else {
|
|
661
|
-
output(hasResults ?
|
|
730
|
+
output(hasResults ? renderXSearchResult(data, check) : formatEmptyXSearchResult(data));
|
|
662
731
|
}
|
|
663
732
|
if (!hasResults) {
|
|
664
733
|
if (
|
|
@@ -734,5 +803,7 @@ module.exports = {
|
|
|
734
803
|
xSearchExperimentSlug,
|
|
735
804
|
xSearchExperimentRel,
|
|
736
805
|
unsaveXSearch,
|
|
806
|
+
formatXSearchResult,
|
|
807
|
+
runXSearchBench,
|
|
737
808
|
xSearchCommand,
|
|
738
809
|
};
|
package/commands/youtube.js
CHANGED
|
@@ -5,6 +5,7 @@ const { spawnSync } = require('child_process');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { stateHome } = require('../lib/state-home');
|
|
8
9
|
const https = require('https');
|
|
9
10
|
|
|
10
11
|
const YTNOTES_USAGE = 'usage: ytnotes <youtube-url> [youtube-url-or-playlist...] [auto|gemini|haiku|atris-fast|grok|codex|cursor]';
|
|
@@ -2256,7 +2257,7 @@ function searchCacheTtlMs(deps = {}) {
|
|
|
2256
2257
|
|
|
2257
2258
|
function resolveSearchCachePath(deps = {}) {
|
|
2258
2259
|
if (deps.searchCachePath) return deps.searchCachePath;
|
|
2259
|
-
const homeDir = deps.homeDir ||
|
|
2260
|
+
const homeDir = deps.homeDir || stateHome();
|
|
2260
2261
|
return path.join(homeDir, '.atris', LOCAL_SEARCH_CACHE_FILE);
|
|
2261
2262
|
}
|
|
2262
2263
|
|
|
@@ -457,8 +457,61 @@ function parseVerifyCommand(verify) {
|
|
|
457
457
|
};
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// Members record checks as one command or several joined with `&&`
|
|
461
|
+
// ("npm run test:a && node scripts/test-b.mjs"). Each step must pass the
|
|
462
|
+
// same allowlist on its own; `&&` only runs them in order and stops at the
|
|
463
|
+
// first failure, still with shell:false, so a chain can do nothing its steps
|
|
464
|
+
// could not do one at a time. One leading `cd` applies to every step.
|
|
465
|
+
const MAX_VERIFY_CHAIN_STEPS = 8;
|
|
466
|
+
|
|
467
|
+
function splitTopLevelAnd(command) {
|
|
468
|
+
const parts = [];
|
|
469
|
+
let current = '';
|
|
470
|
+
let quote = null;
|
|
471
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
472
|
+
const char = command[i];
|
|
473
|
+
if (quote) {
|
|
474
|
+
if (char === quote) quote = null;
|
|
475
|
+
current += char;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (char === "'" || char === '"') {
|
|
479
|
+
quote = char;
|
|
480
|
+
current += char;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (char === '&' && command[i + 1] === '&') {
|
|
484
|
+
parts.push(current.trim());
|
|
485
|
+
current = '';
|
|
486
|
+
i += 1;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
current += char;
|
|
490
|
+
}
|
|
491
|
+
parts.push(current.trim());
|
|
492
|
+
return parts;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function parseVerifyChain(verify) {
|
|
496
|
+
const cmd = String(verify || '').trim();
|
|
497
|
+
if (!cmd) return { ok: false, reason: 'no_verify_command' };
|
|
498
|
+
const prefixed = parseCdPrefix(cmd);
|
|
499
|
+
if (!prefixed.ok) return prefixed;
|
|
500
|
+
const parts = splitTopLevelAnd(prefixed.command);
|
|
501
|
+
if (parts.length > MAX_VERIFY_CHAIN_STEPS || parts.some((part) => !part || /^cd(\s|$)/.test(part))) {
|
|
502
|
+
return { ok: false, reason: 'verify_command_not_allowed' };
|
|
503
|
+
}
|
|
504
|
+
const steps = [];
|
|
505
|
+
for (const part of parts) {
|
|
506
|
+
const parsed = parseVerifyCommand(part);
|
|
507
|
+
if (!parsed.ok) return parsed;
|
|
508
|
+
steps.push({ ...parsed, ...(prefixed.cwd ? { cwd: prefixed.cwd } : {}) });
|
|
509
|
+
}
|
|
510
|
+
return { ok: true, steps, ...(prefixed.cwd ? { cwd: prefixed.cwd } : {}) };
|
|
511
|
+
}
|
|
512
|
+
|
|
460
513
|
function isAutoCertifyVerifyCommandAllowed(verify) {
|
|
461
|
-
return
|
|
514
|
+
return parseVerifyChain(verify).ok;
|
|
462
515
|
}
|
|
463
516
|
|
|
464
517
|
// Re-entrancy guard: every verify child carries ATRIS_VERIFY_IN_PROGRESS=1
|
|
@@ -468,12 +521,20 @@ function isAutoCertifyVerifyCommandAllowed(verify) {
|
|
|
468
521
|
// `atris task status` and the read path re-ran the verify. Refuse loudly.
|
|
469
522
|
const VERIFY_IN_PROGRESS_ENV = 'ATRIS_VERIFY_IN_PROGRESS';
|
|
470
523
|
|
|
471
|
-
// Spawn cap: a
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
|
|
524
|
+
// Spawn cap: bounds a runaway loop inside one process. The recursion that
|
|
525
|
+
// fork-bombed the fleet is stopped by VERIFY_IN_PROGRESS_ENV above; this cap
|
|
526
|
+
// is the backstop. It was 25, which starved the review queue: every hourly
|
|
527
|
+
// tick re-checked the same first 25 rows and the other 141 waiting rows were
|
|
528
|
+
// refused as verify_spawn_cap forever (measured 2026-10-01). Override with
|
|
529
|
+
// ATRIS_VERIFY_SPAWN_CAP.
|
|
530
|
+
const DEFAULT_VERIFY_SPAWN_CAP = 150;
|
|
475
531
|
let verifySpawnCount = 0;
|
|
476
532
|
|
|
533
|
+
function verifySpawnCap() {
|
|
534
|
+
const raw = Number(process.env.ATRIS_VERIFY_SPAWN_CAP);
|
|
535
|
+
return Number.isInteger(raw) && raw > 0 ? Math.min(raw, 1000) : DEFAULT_VERIFY_SPAWN_CAP;
|
|
536
|
+
}
|
|
537
|
+
|
|
477
538
|
function verifyRefusal(unrunnableCause, extra) {
|
|
478
539
|
return {
|
|
479
540
|
ok: false,
|
|
@@ -490,14 +551,26 @@ function runVerifyCommand(verify, workspaceRoot) {
|
|
|
490
551
|
detail: `${VERIFY_IN_PROGRESS_ENV} is set: a verify is already running up the call chain; refusing to nest another one`,
|
|
491
552
|
});
|
|
492
553
|
}
|
|
493
|
-
|
|
554
|
+
const chain = parseVerifyChain(verify);
|
|
555
|
+
if (!chain.ok) return chain;
|
|
556
|
+
let result = null;
|
|
557
|
+
for (const step of chain.steps) {
|
|
558
|
+
result = runVerifyStep(step, workspaceRoot);
|
|
559
|
+
if (!result.ok) {
|
|
560
|
+
return chain.steps.length > 1 ? { ...result, failed_step: step.argv.join(' ') } : result;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function runVerifyStep(parsed, workspaceRoot) {
|
|
567
|
+
const cap = verifySpawnCap();
|
|
568
|
+
if (verifySpawnCount >= cap) {
|
|
494
569
|
return verifyRefusal('verify_spawn_cap', {
|
|
495
|
-
detail: `this process already ran ${verifySpawnCount} verifies (cap ${
|
|
570
|
+
detail: `this process already ran ${verifySpawnCount} verifies (cap ${cap}); refusing more`,
|
|
496
571
|
});
|
|
497
572
|
}
|
|
498
573
|
verifySpawnCount += 1;
|
|
499
|
-
const parsed = parseVerifyCommand(verify);
|
|
500
|
-
if (!parsed.ok) return parsed;
|
|
501
574
|
const cwdCheck = validateCommandCwd(parsed, workspaceRoot);
|
|
502
575
|
if (!cwdCheck.ok) return cwdCheck;
|
|
503
576
|
const gitPathCheck = validateGitWorktreePath(parsed.argv, workspaceRoot, cwdCheck.cwd);
|
|
@@ -1250,6 +1323,7 @@ module.exports = {
|
|
|
1250
1323
|
sniffedProtectedLane,
|
|
1251
1324
|
evaluateAutoAccept,
|
|
1252
1325
|
isAutoCertifyVerifyCommandAllowed,
|
|
1326
|
+
parseVerifyChain,
|
|
1253
1327
|
isAgentCertified,
|
|
1254
1328
|
parseVerifyCommand,
|
|
1255
1329
|
repoHygieneGate,
|
package/lib/autoland.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const { stateHome } = require('./state-home');
|
|
10
11
|
const os = require('os');
|
|
11
12
|
const { spawnSync } = require('child_process');
|
|
12
13
|
const { computeTrustTier } = require('./trust-tiers');
|
|
@@ -189,7 +190,7 @@ function buildCronLine(root) {
|
|
|
189
190
|
const node = process.execPath;
|
|
190
191
|
const bin = path.join(root, 'bin', 'atris.js');
|
|
191
192
|
const cliBin = fs.existsSync(bin) ? bin : 'atris';
|
|
192
|
-
const logDir = path.join(
|
|
193
|
+
const logDir = path.join(stateHome(), '.atris', 'overnight');
|
|
193
194
|
const logFile = path.join(logDir, `autoland-${path.basename(root)}.log`);
|
|
194
195
|
const invoke = cliBin.endsWith('.js') ? `${node} ${cliBin}` : cliBin;
|
|
195
196
|
return `14 * * * * cd ${root} && ${invoke} autoland tick >> ${logFile} 2>&1 # ${cronMarker(root)}`;
|
|
@@ -210,7 +211,7 @@ function cronInstalled(root) {
|
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
function installCron(root) {
|
|
213
|
-
fs.mkdirSync(path.join(
|
|
214
|
+
fs.mkdirSync(path.join(stateHome(), '.atris', 'overnight'), { recursive: true });
|
|
214
215
|
const marker = cronMarker(root);
|
|
215
216
|
const kept = readCrontab().split(/\r?\n/).filter((line) => line.trim() && !line.includes(marker));
|
|
216
217
|
const next = `${kept.join('\n')}${kept.length ? '\n' : ''}${buildCronLine(root)}\n`;
|
package/lib/ax-auto-lane.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
4
|
const path = require('node:path');
|
|
5
|
+
const { stateHome } = require('./state-home');
|
|
6
6
|
|
|
7
7
|
const SHORT_LOOKUP_MAX_CHARS = 140;
|
|
8
8
|
const LONG_INPUT_MIN_CHARS = 1200;
|
|
@@ -75,7 +75,7 @@ function overrideMatches(text, override) {
|
|
|
75
75
|
function loadOverrides(filePath) {
|
|
76
76
|
const resolved = filePath
|
|
77
77
|
|| process.env.ATRIS_ROUTER_OVERRIDES_PATH
|
|
78
|
-
|| path.join(
|
|
78
|
+
|| path.join(stateHome(), '.atris', 'router-overrides.json');
|
|
79
79
|
try {
|
|
80
80
|
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
81
81
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -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 };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared plumbing for business-scoped HTTP commands (`atris hire`, `atris hires`,
|
|
4
|
+
// `atris mail`): parse flags, find which business to act on, get a login token.
|
|
5
|
+
// These commands are HTTP clients of /api/business/{id}/...; no backend code here.
|
|
6
|
+
|
|
7
|
+
const { apiRequestJson } = require('../utils/api');
|
|
8
|
+
const { ensureValidCredentials } = require('../utils/auth');
|
|
9
|
+
const { findBusiness } = require('../commands/feed');
|
|
10
|
+
const { oneLine } = require('./developer-api');
|
|
11
|
+
|
|
12
|
+
const NO_BUSINESS = 'no business here. run this inside a business folder, or add --business <slug>';
|
|
13
|
+
|
|
14
|
+
// --help, --json and --business are allowed on every command.
|
|
15
|
+
const COMMON_SWITCHES = ['help', 'json'];
|
|
16
|
+
const COMMON_VALUES = ['business'];
|
|
17
|
+
|
|
18
|
+
// --key value, --key=value, and yes/no switches that never take a value.
|
|
19
|
+
// `spec` is { switches: [...], values: [...] }: the only flags the command
|
|
20
|
+
// takes, beyond the common ones. Anything else comes back as `error`, and
|
|
21
|
+
// the command must stop before making any request:
|
|
22
|
+
// an unknown flag (--dry-run on a command without it),
|
|
23
|
+
// a value on a switch (--yes=false is refused, never read as yes),
|
|
24
|
+
// a value flag with nothing after it (--business ""),
|
|
25
|
+
// the same value flag twice.
|
|
26
|
+
// A plain array is the old lenient form (switch names only), kept for callers
|
|
27
|
+
// that want it; it never reports errors.
|
|
28
|
+
function parseArgs(argv = [], spec = []) {
|
|
29
|
+
const strict = !Array.isArray(spec);
|
|
30
|
+
const switchNames = strict ? (spec.switches || []) : spec;
|
|
31
|
+
const valueNames = strict ? (spec.values || []) : [];
|
|
32
|
+
const switches = new Set([...switchNames, ...COMMON_SWITCHES, 'yes']);
|
|
33
|
+
const allowed = new Set([...switchNames, ...valueNames, ...COMMON_SWITCHES, ...COMMON_VALUES]);
|
|
34
|
+
const flags = {};
|
|
35
|
+
const pos = [];
|
|
36
|
+
let error = '';
|
|
37
|
+
const fail = (message) => { if (!error) error = message; };
|
|
38
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
39
|
+
const arg = String(argv[i]);
|
|
40
|
+
if (arg === '-h') { flags.help = true; continue; }
|
|
41
|
+
if (arg === '-y') {
|
|
42
|
+
if (strict && !allowed.has('yes')) fail('unknown option -y');
|
|
43
|
+
else flags.yes = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg.startsWith('--')) {
|
|
47
|
+
const eq = arg.indexOf('=');
|
|
48
|
+
const raw = eq > 0 ? arg.slice(0, eq) : arg;
|
|
49
|
+
const key = raw.slice(2).replace(/-/g, '_');
|
|
50
|
+
if (strict && !allowed.has(key)) { fail(`unknown option ${raw}`); continue; }
|
|
51
|
+
if (switches.has(key)) {
|
|
52
|
+
if (eq < 0) flags[key] = true;
|
|
53
|
+
else if (strict) fail(`${raw} is a switch and takes no value: use ${raw} on its own, or leave it off`);
|
|
54
|
+
else flags[key] = arg.slice(eq + 1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
let value = true;
|
|
58
|
+
if (eq > 0) value = arg.slice(eq + 1);
|
|
59
|
+
else if (argv[i + 1] != null && !String(argv[i + 1]).startsWith('--')) { value = String(argv[i + 1]); i += 1; }
|
|
60
|
+
if (strict && (typeof value !== 'string' || !value.trim())) { fail(`${raw} needs a value`); continue; }
|
|
61
|
+
if (strict && Object.prototype.hasOwnProperty.call(flags, key)) { fail(`${raw} was given twice`); continue; }
|
|
62
|
+
flags[key] = value;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (strict && arg.length > 1 && arg.startsWith('-')) { fail(`unknown option ${arg}`); continue; }
|
|
66
|
+
pos.push(arg);
|
|
67
|
+
}
|
|
68
|
+
return strict ? { flags, pos, error } : { flags, pos };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A --business that was given must name a business. An empty or missing
|
|
72
|
+
// value is an error and never falls back to the current folder's business.
|
|
73
|
+
// Without --business: the nearest .atris/business.json above the current
|
|
74
|
+
// folder. Returns { id, name }, { error }, or null when none is found.
|
|
75
|
+
function resolveBusiness(flags = {}, deps = {}) {
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(flags, 'business')) {
|
|
77
|
+
const given = typeof flags.business === 'string' ? flags.business.trim() : '';
|
|
78
|
+
if (!given) return { error: '--business needs a business slug or id' };
|
|
79
|
+
return { id: given, name: given };
|
|
80
|
+
}
|
|
81
|
+
const found = (deps.findBusiness || findBusiness)(deps.cwd || process.cwd());
|
|
82
|
+
return found && found.businessId ? { id: found.businessId, name: found.name || 'this business' } : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The business to act on, or says why not (through `err`) and returns null.
|
|
86
|
+
function businessOrSay(flags, deps, err) {
|
|
87
|
+
const business = resolveBusiness(flags, deps);
|
|
88
|
+
if (!business) { err(NO_BUSINESS); return null; }
|
|
89
|
+
if (business.error) { err(business.error); return null; }
|
|
90
|
+
return business;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loginToken(deps = {}) {
|
|
94
|
+
if (deps.token) return deps.token;
|
|
95
|
+
const ensure = deps.ensureCredentials || (() => ensureValidCredentials(apiRequestJson));
|
|
96
|
+
const auth = await ensure();
|
|
97
|
+
return auth && !auth.error && auth.credentials && auth.credentials.token ? auth.credentials.token : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// FastAPI errors arrive as {"detail": "..."}; keep them to one plain line.
|
|
101
|
+
function errorText(result) {
|
|
102
|
+
if (!result) return 'request failed';
|
|
103
|
+
if (Number(result.status) === 404 && !(result.data && result.data.detail)) {
|
|
104
|
+
return 'backend rolling out, try again shortly';
|
|
105
|
+
}
|
|
106
|
+
return oneLine((result.data && (result.data.detail || result.data.error)) || result.error || `request failed (${result.status})`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// "2026-10-02T09:00:00+00:00" -> "in 9 h" / "6 h ago"
|
|
110
|
+
function relativeTime(iso, now = Date.now()) {
|
|
111
|
+
const at = Date.parse(iso || '');
|
|
112
|
+
if (!Number.isFinite(at)) return '';
|
|
113
|
+
const mins = Math.round((at - now) / 60000);
|
|
114
|
+
const abs = Math.abs(mins);
|
|
115
|
+
const span = abs < 1 ? 'now' : abs < 90 ? `${abs} min` : abs < 48 * 60 ? `${Math.round(abs / 60)} h` : `${Math.round(abs / 1440)} days`;
|
|
116
|
+
if (span === 'now') return 'just now';
|
|
117
|
+
return mins >= 0 ? `in ${span}` : `${span} ago`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function dollars(value) {
|
|
121
|
+
if (value == null || !Number.isFinite(Number(value))) return '-';
|
|
122
|
+
const n = Number(value);
|
|
123
|
+
return Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function clip(text, width) {
|
|
127
|
+
const s = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
|
|
128
|
+
return s.length > width ? `${s.slice(0, width - 1)}…` : s;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { NO_BUSINESS, parseArgs, resolveBusiness, businessOrSay, loginToken, errorText, relativeTime, dollars, clip };
|
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',
|