atris 3.38.0 → 3.41.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/AGENTS.md +25 -6
- package/atris/PERSONA.md +8 -4
- package/atris.md +7 -0
- package/ax +2 -1
- package/bin/atris.js +31 -6
- package/commands/agent-spawn.js +13 -11
- package/commands/autoland.js +28 -77
- package/commands/bench.js +10 -12
- package/commands/business.js +345 -0
- package/commands/chat-scan.js +5 -7
- package/commands/codex-goal.js +8 -10
- package/commands/computer.js +20 -0
- package/commands/console.js +19 -3
- package/commands/decide.js +166 -0
- package/commands/deck.js +1 -4
- package/commands/drill.js +14 -24
- package/commands/engine.js +196 -8
- package/commands/gm.js +8 -6
- package/commands/harvest.js +1 -4
- package/commands/init.js +23 -3
- package/commands/land.js +8 -14
- package/commands/launchpad.js +1 -14
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/member.js +558 -574
- package/commands/mission.js +456 -237
- package/commands/pack.js +2746 -164
- package/commands/play.js +6 -4
- package/commands/probe.js +2 -2
- package/commands/pulse.js +15 -16
- package/commands/release.js +10 -9
- package/commands/router.js +5 -4
- package/commands/site-deploy.js +885 -0
- package/commands/site.js +11 -2
- package/commands/slop.js +14 -2
- package/commands/stream.js +4 -18
- package/commands/task.js +880 -558
- package/commands/taste.js +101 -0
- package/commands/team.js +176 -3
- package/commands/vercel.js +4 -2
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -22
- package/commands/wiki.js +1 -4
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +1 -14
- package/commands/xp.js +27 -24
- package/lib/accept-verify-gate.js +5 -1
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +116 -1
- package/lib/autoland.js +66 -0
- package/lib/bench/runner.js +19 -1
- package/lib/context-gatherer.js +7 -1
- package/lib/engine-registry.js +141 -20
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +65 -15
- package/lib/git-spawn.js +15 -0
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +0 -2
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +4 -1
- package/lib/official-cli-integration.js +47 -2
- package/lib/orb-context.js +8 -1
- package/lib/pack-capabilities.js +685 -0
- package/lib/router-brain.js +51 -1
- package/lib/runner-command.js +0 -6
- package/lib/self-drive.js +44 -13
- package/lib/task-db.js +137 -3
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +66 -0
- package/lib/wish-audit.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/zip.js +95 -7
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
package/commands/xp.js
CHANGED
|
@@ -5,6 +5,7 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
|
+
const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
|
|
8
9
|
|
|
9
10
|
const DEFAULT_GRAPH_DAYS = 365;
|
|
10
11
|
const MAX_SYNC_GRAPH_DAYS = 370;
|
|
@@ -277,7 +278,8 @@ function currentForm(payload) {
|
|
|
277
278
|
};
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
|
|
281
|
+
// Preserve the existing rule that any inline value wins over a split value.
|
|
282
|
+
function readInlineFirstFlag(args, name, fallback = null) {
|
|
281
283
|
const inline = args.find(arg => arg.startsWith(`${name}=`));
|
|
282
284
|
if (inline) {
|
|
283
285
|
return inline.slice(name.length + 1);
|
|
@@ -291,7 +293,7 @@ function readFlag(args, name, fallback = null) {
|
|
|
291
293
|
|
|
292
294
|
function readFirstFlag(args, names, fallback = null) {
|
|
293
295
|
for (const name of names) {
|
|
294
|
-
const value =
|
|
296
|
+
const value = readInlineFirstFlag(args, name, null);
|
|
295
297
|
if (value !== null && value !== undefined && value !== '') return value;
|
|
296
298
|
}
|
|
297
299
|
return fallback;
|
|
@@ -313,8 +315,9 @@ function readFlagValues(args, names) {
|
|
|
313
315
|
return values.filter(Boolean);
|
|
314
316
|
}
|
|
315
317
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
+
// This command treats --name=value as flag presence as well as a standalone flag.
|
|
319
|
+
function hasFlagOrValue(args, name) {
|
|
320
|
+
return hasExactFlag(args, name) || args.some(arg => arg.startsWith(`${name}=`));
|
|
318
321
|
}
|
|
319
322
|
|
|
320
323
|
function levelFromXp(careerXp) {
|
|
@@ -1040,7 +1043,7 @@ function buildCareerXpProjection(receipts, workspace, integrity = {}) {
|
|
|
1040
1043
|
}
|
|
1041
1044
|
|
|
1042
1045
|
function collectLocalXpProjectionState(args = [], { write = true } = {}) {
|
|
1043
|
-
const workspace = path.resolve(
|
|
1046
|
+
const workspace = path.resolve(readInlineFirstFlag(args, '--workspace', defaultXpWorkspace()));
|
|
1044
1047
|
const episodePath = path.join(workspace, TASK_EPISODES_FILE);
|
|
1045
1048
|
const receiptsPath = path.join(workspace, CAREER_XP_RECEIPTS_FILE);
|
|
1046
1049
|
const projectionPath = path.join(workspace, CAREER_XP_PROJECTION_FILE);
|
|
@@ -1170,7 +1173,7 @@ function defaultAllSearchRoots(args = []) {
|
|
|
1170
1173
|
if (explicitRoots.length) return uniquePaths(explicitRoots);
|
|
1171
1174
|
|
|
1172
1175
|
const roots = [];
|
|
1173
|
-
const workspace =
|
|
1176
|
+
const workspace = readInlineFirstFlag(args, '--workspace', null);
|
|
1174
1177
|
if (workspace) roots.push(workspace);
|
|
1175
1178
|
roots.push(process.cwd());
|
|
1176
1179
|
roots.push(path.join(os.homedir(), 'arena'));
|
|
@@ -1575,7 +1578,7 @@ function codexHasThreadsTable(dbPath) {
|
|
|
1575
1578
|
}
|
|
1576
1579
|
|
|
1577
1580
|
function readCodexGoalsForSession(args, workspace, sinceMs, untilMs) {
|
|
1578
|
-
const explicitDb =
|
|
1581
|
+
const explicitDb = readInlineFirstFlag(args, '--codex-state', readInlineFirstFlag(args, '--state', process.env.CODEX_STATE_DB || ''));
|
|
1579
1582
|
const dbPath = explicitDb
|
|
1580
1583
|
? path.resolve(expandHome(explicitDb))
|
|
1581
1584
|
: path.resolve(fs.existsSync(CODEX_GOALS_FILE) ? CODEX_GOALS_FILE : CODEX_STATE_FILE);
|
|
@@ -1592,7 +1595,7 @@ function readCodexGoalsForSession(args, workspace, sinceMs, untilMs) {
|
|
|
1592
1595
|
}
|
|
1593
1596
|
}
|
|
1594
1597
|
|
|
1595
|
-
const threadId =
|
|
1598
|
+
const threadId = readInlineFirstFlag(args, '--thread', process.env.CODEX_THREAD_ID || '');
|
|
1596
1599
|
const clauses = [];
|
|
1597
1600
|
if (threadId) clauses.push(`tg.thread_id = ${sqlString(threadId)}`);
|
|
1598
1601
|
const workspaceCandidates = workspacePathCandidates(workspace);
|
|
@@ -1632,9 +1635,9 @@ LIMIT 25
|
|
|
1632
1635
|
}
|
|
1633
1636
|
|
|
1634
1637
|
function buildCareerXpSessionCapsule(args = []) {
|
|
1635
|
-
const workspace = path.resolve(
|
|
1636
|
-
const sinceInput =
|
|
1637
|
-
const untilInput =
|
|
1638
|
+
const workspace = path.resolve(readInlineFirstFlag(args, '--workspace', defaultXpWorkspace()));
|
|
1639
|
+
const sinceInput = readInlineFirstFlag(args, '--since', 'today');
|
|
1640
|
+
const untilInput = readInlineFirstFlag(args, '--until', null);
|
|
1638
1641
|
const since = parseSessionBoundary(sinceInput, startOfToday());
|
|
1639
1642
|
const until = parseSessionBoundary(untilInput, new Date());
|
|
1640
1643
|
const sinceMs = since.getTime();
|
|
@@ -1643,7 +1646,7 @@ function buildCareerXpSessionCapsule(args = []) {
|
|
|
1643
1646
|
throw new Error('--since must be before --until');
|
|
1644
1647
|
}
|
|
1645
1648
|
|
|
1646
|
-
const writeEnabled = !
|
|
1649
|
+
const writeEnabled = !hasFlagOrValue(args, '--no-write') && !hasFlagOrValue(args, '--dry-run');
|
|
1647
1650
|
const projectionState = collectLocalXpProjectionState(['--workspace', workspace], { write: writeEnabled });
|
|
1648
1651
|
const projection = projectionState.projection;
|
|
1649
1652
|
const receiptsPath = path.join(workspace, CAREER_XP_RECEIPTS_FILE);
|
|
@@ -1675,7 +1678,7 @@ function buildCareerXpSessionCapsule(args = []) {
|
|
|
1675
1678
|
});
|
|
1676
1679
|
const windowXp = acceptedReceipts.reduce((sum, receipt) => sum + asNumber(receipt.xp), 0);
|
|
1677
1680
|
const afterTotalXp = asNumber(projection.total_xp);
|
|
1678
|
-
const missionFlag =
|
|
1681
|
+
const missionFlag = readInlineFirstFlag(args, '--mission', null);
|
|
1679
1682
|
const episodeGoals = readJsonl(path.join(workspace, TASK_EPISODES_FILE), { allowPartialTail: true })
|
|
1680
1683
|
.filter(episode => inWindow(episode?.created_at, sinceMs, untilMs))
|
|
1681
1684
|
.map(episode => episode.goal);
|
|
@@ -1890,8 +1893,8 @@ function syncScopeFields(attribution = {}) {
|
|
|
1890
1893
|
}
|
|
1891
1894
|
|
|
1892
1895
|
function publicAgentXpOverride(args = []) {
|
|
1893
|
-
if (
|
|
1894
|
-
if (
|
|
1896
|
+
if (hasFlagOrValue(args, '--public')) return true;
|
|
1897
|
+
if (hasFlagOrValue(args, '--private') || hasFlagOrValue(args, '--internal')) return false;
|
|
1895
1898
|
return null;
|
|
1896
1899
|
}
|
|
1897
1900
|
|
|
@@ -1946,9 +1949,9 @@ function syncPlayer(args, projection) {
|
|
|
1946
1949
|
}
|
|
1947
1950
|
|
|
1948
1951
|
function buildAgentXpSyncPacket(args = []) {
|
|
1949
|
-
const localMode =
|
|
1952
|
+
const localMode = hasFlagOrValue(args, '--local') || hasFlagOrValue(args, '--workspace') || hasFlagOrValue(args, '--operator');
|
|
1950
1953
|
const projectionArgs = args.filter(arg => !['--dry-run', '--no-post', '--packet', '--public', '--private', '--internal'].includes(arg));
|
|
1951
|
-
const projection =
|
|
1954
|
+
const projection = hasFlagOrValue(args, '--all') || !localMode
|
|
1952
1955
|
? collectAllLocalXpProjection(projectionArgs)
|
|
1953
1956
|
: collectLocalXpProjection(projectionArgs);
|
|
1954
1957
|
const player = syncPlayer(args, projection);
|
|
@@ -2086,10 +2089,10 @@ function buildAgentXpSyncPacket(args = []) {
|
|
|
2086
2089
|
|
|
2087
2090
|
async function syncAgentXp(args = []) {
|
|
2088
2091
|
const preview = buildAgentXpSyncPacket(args);
|
|
2089
|
-
const dryRun =
|
|
2092
|
+
const dryRun = hasFlagOrValue(args, '--dry-run') || hasFlagOrValue(args, '--no-post') || hasFlagOrValue(args, '--packet');
|
|
2090
2093
|
if (dryRun) return preview;
|
|
2091
2094
|
|
|
2092
|
-
const token =
|
|
2095
|
+
const token = readInlineFirstFlag(args, '--token', process.env.ATRIS_AGENTXP_SYNC_TOKEN || process.env.AGENTXP_SYNC_TOKEN || '');
|
|
2093
2096
|
const envToken = process.env.ATRIS_TOKEN && process.env.ATRIS_TOKEN.trim() ? process.env.ATRIS_TOKEN.trim() : '';
|
|
2094
2097
|
const options = {
|
|
2095
2098
|
method: 'POST',
|
|
@@ -2298,11 +2301,11 @@ async function xpCommand(...args) {
|
|
|
2298
2301
|
const commandArgs = args.slice(1);
|
|
2299
2302
|
let payload;
|
|
2300
2303
|
try {
|
|
2301
|
-
const explicitLocal =
|
|
2302
|
-
||
|
|
2303
|
-
||
|
|
2304
|
+
const explicitLocal = hasFlagOrValue(commandArgs, '--local')
|
|
2305
|
+
|| hasFlagOrValue(commandArgs, '--workspace')
|
|
2306
|
+
|| hasFlagOrValue(commandArgs, '--operator');
|
|
2304
2307
|
const accountStatus = (subcommand === 'status' || subcommand === 'card') && !explicitLocal;
|
|
2305
|
-
payload =
|
|
2308
|
+
payload = hasFlagOrValue(commandArgs, '--all') || accountStatus
|
|
2306
2309
|
? collectAllLocalXpProjection(commandArgs)
|
|
2307
2310
|
: collectLocalXpProjection(commandArgs);
|
|
2308
2311
|
} catch (error) {
|
|
@@ -2324,7 +2327,7 @@ async function xpCommand(...args) {
|
|
|
2324
2327
|
}
|
|
2325
2328
|
|
|
2326
2329
|
const jsonMode = args.includes('--json');
|
|
2327
|
-
const localMode =
|
|
2330
|
+
const localMode = hasFlagOrValue(args, '--local') || hasFlagOrValue(args, '--workspace') || hasFlagOrValue(args, '--operator');
|
|
2328
2331
|
if (localMode) {
|
|
2329
2332
|
let payload;
|
|
2330
2333
|
try {
|
|
@@ -101,12 +101,16 @@ function evaluateAcceptVerify(task, workspaceRoot, { cache = null } = {}) {
|
|
|
101
101
|
|
|
102
102
|
const result = runVerifyCommandCached(command, workspaceRoot, cache);
|
|
103
103
|
if (!result.ok) {
|
|
104
|
+
const diffCheck = /^git\s+diff\s+--check\b/i.test(command.trim());
|
|
105
|
+
const fixHint = diffCheck
|
|
106
|
+
? '; trailing whitespace in markdown is auto-fixable: npm run audit:markdown-whitespace -- --fix'
|
|
107
|
+
: '';
|
|
104
108
|
return {
|
|
105
109
|
ok: false,
|
|
106
110
|
reason: result.reason || 'verify_failed',
|
|
107
111
|
detail: result.reason === 'verify_workdir_missing' || result.reason === 'verify_worktree_missing'
|
|
108
112
|
? 'the stored verify command points at a directory that no longer exists'
|
|
109
|
-
: `the stored verify command did not pass (${result.reason || 'nonzero exit'})`,
|
|
113
|
+
: `the stored verify command did not pass (${result.reason || 'nonzero exit'})${fixHint}`,
|
|
110
114
|
command,
|
|
111
115
|
ran: true,
|
|
112
116
|
exit_code: typeof result.status === 'number' ? result.status : null,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function hasFlag(args, name) {
|
|
4
|
+
return args.includes(name);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function unquote(value) {
|
|
8
|
+
const text = String(value);
|
|
9
|
+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
10
|
+
return text.slice(1, -1);
|
|
11
|
+
}
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readFlag(args, name, fallback = '') {
|
|
16
|
+
const prefix = `${name}=`;
|
|
17
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
18
|
+
const arg = String(args[i]);
|
|
19
|
+
if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) {
|
|
20
|
+
return unquote(args[i + 1]);
|
|
21
|
+
}
|
|
22
|
+
if (arg.startsWith(prefix)) return unquote(arg.slice(prefix.length));
|
|
23
|
+
}
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readIntFlag(args, name, fallback = null) {
|
|
28
|
+
const raw = readFlag(args, name, '');
|
|
29
|
+
if (!raw) return fallback;
|
|
30
|
+
const value = Number.parseInt(raw, 10);
|
|
31
|
+
return Number.isFinite(value) ? value : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readNumberFlag(args, name, fallback = null) {
|
|
35
|
+
const raw = readFlag(args, name, '');
|
|
36
|
+
if (!raw) return fallback;
|
|
37
|
+
const value = Number(raw);
|
|
38
|
+
return Number.isFinite(value) ? value : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { hasFlag, readFlag, readIntFlag, readNumberFlag };
|
|
@@ -13,7 +13,7 @@ const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
|
|
|
13
13
|
// once powered is gone. Passes alone never land work, an independent
|
|
14
14
|
// reviewer does.
|
|
15
15
|
const AUTO_ACCEPT_HIGH_CONFIDENCE_PASSES = 3;
|
|
16
|
-
const DENIED_TAGS = new Set(['billing', 'deploy', 'feedback', 'security', 'customer', 'external']);
|
|
16
|
+
const DENIED_TAGS = new Set(['billing', 'money', 'payments', 'deploy', 'feedback', 'security', 'customer', 'external']);
|
|
17
17
|
|
|
18
18
|
const SIMPLE_VERIFY_TOKEN_RE = /^[a-zA-Z0-9_./:@=+-]+$/;
|
|
19
19
|
const GIT_WORKTREE_PATH_RE = /^[a-zA-Z0-9_./@=+-]+$/;
|
|
@@ -447,7 +447,41 @@ function isAutoCertifyVerifyCommandAllowed(verify) {
|
|
|
447
447
|
return parseVerifyCommand(verify).ok;
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
// Re-entrancy guard: every verify child carries ATRIS_VERIFY_IN_PROGRESS=1
|
|
451
|
+
// (see verifyCommandEnv). A verify that shells back into the CLI and reaches
|
|
452
|
+
// another verify would otherwise recurse without bound — that cycle
|
|
453
|
+
// fork-bombed the fleet on 2026-07-29 when a stored pytest called
|
|
454
|
+
// `atris task status` and the read path re-ran the verify. Refuse loudly.
|
|
455
|
+
const VERIFY_IN_PROGRESS_ENV = 'ATRIS_VERIFY_IN_PROGRESS';
|
|
456
|
+
|
|
457
|
+
// Spawn cap: a single CLI process has no legitimate reason to run dozens of
|
|
458
|
+
// verifies. Beyond the cap something is looping; refuse and report instead
|
|
459
|
+
// of silently bounding.
|
|
460
|
+
const VERIFY_SPAWN_CAP = 25;
|
|
461
|
+
let verifySpawnCount = 0;
|
|
462
|
+
|
|
463
|
+
function verifyRefusal(unrunnableCause, extra) {
|
|
464
|
+
return {
|
|
465
|
+
ok: false,
|
|
466
|
+
reason: 'verify_unrunnable',
|
|
467
|
+
unrunnable_cause: unrunnableCause,
|
|
468
|
+
alarm: true,
|
|
469
|
+
...(extra || {}),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
450
473
|
function runVerifyCommand(verify, workspaceRoot) {
|
|
474
|
+
if (process.env[VERIFY_IN_PROGRESS_ENV]) {
|
|
475
|
+
return verifyRefusal('verify_reentrant', {
|
|
476
|
+
detail: `${VERIFY_IN_PROGRESS_ENV} is set: a verify is already running up the call chain; refusing to nest another one`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
if (verifySpawnCount >= VERIFY_SPAWN_CAP) {
|
|
480
|
+
return verifyRefusal('verify_spawn_cap', {
|
|
481
|
+
detail: `this process already ran ${verifySpawnCount} verifies (cap ${VERIFY_SPAWN_CAP}); refusing more`,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
verifySpawnCount += 1;
|
|
451
485
|
const parsed = parseVerifyCommand(verify);
|
|
452
486
|
if (!parsed.ok) return parsed;
|
|
453
487
|
const cwdCheck = validateCommandCwd(parsed, workspaceRoot);
|
|
@@ -503,6 +537,7 @@ function verifyCommandEnv(extraEnv) {
|
|
|
503
537
|
if (binDir && !alreadyPresent) {
|
|
504
538
|
base.PATH = current ? `${binDir}${path.delimiter}${current}` : binDir;
|
|
505
539
|
}
|
|
540
|
+
base[VERIFY_IN_PROGRESS_ENV] = '1';
|
|
506
541
|
return base;
|
|
507
542
|
}
|
|
508
543
|
|
|
@@ -528,6 +563,63 @@ function runVerifyCommandCached(verify, workspaceRoot, cache = null) {
|
|
|
528
563
|
return { ...result, reused: false };
|
|
529
564
|
}
|
|
530
565
|
|
|
566
|
+
// Pre-land hygiene: dead exports used to surface only AFTER landing, when the
|
|
567
|
+
// full suite's repo-hygiene ratchet went red on master (lesson:
|
|
568
|
+
// engine-dead-exports — engines export every internal helper, the task's own
|
|
569
|
+
// verify command stays green, and the breakage lands). In repos that carry the
|
|
570
|
+
// ratchet (test/repo-hygiene.test.js), run the same detector before landing so
|
|
571
|
+
// the gate refuses the work instead of master discovering it. Memoized per
|
|
572
|
+
// root: one scan covers every task in a sweep.
|
|
573
|
+
const repoHygieneCache = new Map();
|
|
574
|
+
|
|
575
|
+
function repoHygieneGate(workspaceRoot) {
|
|
576
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
577
|
+
if (repoHygieneCache.has(root)) return repoHygieneCache.get(root);
|
|
578
|
+
let result = { ok: true, skipped: true };
|
|
579
|
+
if (fs.existsSync(path.join(root, 'test', 'repo-hygiene.test.js'))) {
|
|
580
|
+
try {
|
|
581
|
+
const { findDeadCode, findOrphanedExports, listJsFiles } = require('../commands/slop');
|
|
582
|
+
const dead = findDeadCode(root).dead;
|
|
583
|
+
const files = ['commands', 'lib'].flatMap((d) => listJsFiles(path.join(root, d)));
|
|
584
|
+
const orphans = findOrphanedExports(root, files, listJsFiles(root));
|
|
585
|
+
const offenders = [
|
|
586
|
+
...dead.map((f) => path.relative(root, f)),
|
|
587
|
+
...orphans.map((o) => `${path.relative(root, o.file)} → ${o.name}`),
|
|
588
|
+
];
|
|
589
|
+
result = offenders.length
|
|
590
|
+
? {
|
|
591
|
+
ok: false,
|
|
592
|
+
reason: 'dead_exports',
|
|
593
|
+
offenders: offenders.slice(0, 12),
|
|
594
|
+
message: 'this work leaves code or exports nothing uses, so the full test suite fails right after landing; delete the unused pieces and land again.',
|
|
595
|
+
}
|
|
596
|
+
: { ok: true };
|
|
597
|
+
} catch (err) {
|
|
598
|
+
// "I could not run the detector" is not a verdict on the work — same
|
|
599
|
+
// rule as verify_unrunnable, but hygiene is a repo-wide ratchet the
|
|
600
|
+
// suite still enforces, so failing open here only delays the red.
|
|
601
|
+
result = { ok: true, skipped: true, error: String((err && err.message) || err).slice(0, 200) };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
repoHygieneCache.set(root, result);
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Shared by both landing lanes so the check cannot ship in one dispatch branch
|
|
609
|
+
// and silently skip the sibling (lesson: parallel-paths-drift).
|
|
610
|
+
function hygieneBlockResult(task, ref) {
|
|
611
|
+
const hygiene = repoHygieneGate(task.workspace_root || process.cwd());
|
|
612
|
+
if (hygiene.ok) return null;
|
|
613
|
+
return {
|
|
614
|
+
eligible: false,
|
|
615
|
+
ref,
|
|
616
|
+
reason: hygiene.reason,
|
|
617
|
+
message: hygiene.message,
|
|
618
|
+
offenders: hygiene.offenders,
|
|
619
|
+
next_action: 'run `node --test test/repo-hygiene.test.js` in the workspace, delete what it names, then re-certify',
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
531
623
|
function strictVerifyMissingResult(ref) {
|
|
532
624
|
return {
|
|
533
625
|
eligible: false,
|
|
@@ -721,6 +813,19 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
721
813
|
if (trustTier === 'probation' && !reviewIntegrity.hasIndependentReview(task)) {
|
|
722
814
|
return { eligible: false, ref, reason: 'probation_needs_review' };
|
|
723
815
|
}
|
|
816
|
+
// The read-only status path (executeVerify:false) must apply the same
|
|
817
|
+
// command allowlist the landing gate applies, or status promises a landing
|
|
818
|
+
// the gate will refuse every hour: five rows read "lands itself" for a
|
|
819
|
+
// whole night while every tick skipped them as verify_command_not_allowed.
|
|
820
|
+
// Sits after the probation gate so it reports the gate's own refusal
|
|
821
|
+
// order, not a new one.
|
|
822
|
+
if (verify && !executeVerify && !isAutoCertifyVerifyCommandAllowed(verify)) {
|
|
823
|
+
return { eligible: false, ref, reason: 'verify_command_not_allowed', verify };
|
|
824
|
+
}
|
|
825
|
+
if (executeVerify) {
|
|
826
|
+
const hygieneBlock = hygieneBlockResult(task, ref);
|
|
827
|
+
if (hygieneBlock) return hygieneBlock;
|
|
828
|
+
}
|
|
724
829
|
return {
|
|
725
830
|
eligible: true,
|
|
726
831
|
ref,
|
|
@@ -788,9 +893,18 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
788
893
|
if (!verifyResult.ok) {
|
|
789
894
|
return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
|
|
790
895
|
}
|
|
896
|
+
} else if (!isAutoCertifyVerifyCommandAllowed(verify)) {
|
|
897
|
+
// Same truth rule as above: the read-only path may not promise a
|
|
898
|
+
// landing the allowlist will refuse.
|
|
899
|
+
return { eligible: false, ref, reason: 'verify_command_not_allowed', verify };
|
|
791
900
|
}
|
|
792
901
|
}
|
|
793
902
|
|
|
903
|
+
if (executeVerify) {
|
|
904
|
+
const hygieneBlock = hygieneBlockResult(task, ref);
|
|
905
|
+
if (hygieneBlock) return hygieneBlock;
|
|
906
|
+
}
|
|
907
|
+
|
|
794
908
|
return {
|
|
795
909
|
eligible: true,
|
|
796
910
|
ref,
|
|
@@ -812,6 +926,7 @@ module.exports = {
|
|
|
812
926
|
isAutoCertifyVerifyCommandAllowed,
|
|
813
927
|
isAgentCertified,
|
|
814
928
|
parseVerifyCommand,
|
|
929
|
+
repoHygieneGate,
|
|
815
930
|
runVerifyCommand,
|
|
816
931
|
runVerifyCommandCached,
|
|
817
932
|
};
|
package/lib/autoland.js
CHANGED
|
@@ -120,6 +120,66 @@ function liveAcceptAuthorization(root = process.cwd()) {
|
|
|
120
120
|
return { ok: true, actor, policy: 'autoland', strictVerify: policy.strict_verify !== false };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Tick receipts are local file reads and prove the loop ran. A receipt from
|
|
124
|
+
// the last couple of hours means the hourly pass is alive; older or missing
|
|
125
|
+
// means surfaces must not promise "next tick" landing.
|
|
126
|
+
const HEARTBEAT_LIVE_HOURS = 2;
|
|
127
|
+
const STALE_HEARTBEAT_LANDING = 'once the hourly heartbeat runs (start one with atris autoland tick)';
|
|
128
|
+
|
|
129
|
+
function lastTickAgeHours(root) {
|
|
130
|
+
try {
|
|
131
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
132
|
+
const newest = fs.readdirSync(runsDir)
|
|
133
|
+
.filter((f) => f.startsWith('autoland-tick-') && f.endsWith('.json'))
|
|
134
|
+
.sort()
|
|
135
|
+
.pop();
|
|
136
|
+
if (!newest) return null;
|
|
137
|
+
const stamp = fs.statSync(path.join(runsDir, newest)).mtimeMs;
|
|
138
|
+
return (Date.now() - stamp) / 3_600_000;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function heartbeatIsLive(root) {
|
|
145
|
+
const ageHours = lastTickAgeHours(root);
|
|
146
|
+
return ageHours !== null && ageHours <= HEARTBEAT_LIVE_HOURS;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function heartbeatLiveness(root, policy) {
|
|
150
|
+
return typeof policy?.heartbeat_installed === 'boolean' ? policy.heartbeat_installed : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function heartbeatStatusText(root, policy) {
|
|
154
|
+
const ageHours = lastTickAgeHours(root);
|
|
155
|
+
const ageText = ageHours === null
|
|
156
|
+
? null
|
|
157
|
+
: ageHours < 1 ? 'under an hour' : `${Math.floor(ageHours)}h`;
|
|
158
|
+
// Evidence first: a receipt from the last couple of hours proves the loop is
|
|
159
|
+
// alive no matter what the policy file remembers. The hourly cron makes two
|
|
160
|
+
// missed hours an outage rather than jitter — and a heartbeat that has gone
|
|
161
|
+
// quiet is the single most useful thing this line can say, because that is
|
|
162
|
+
// the state nobody notices.
|
|
163
|
+
if (ageHours !== null && ageHours <= HEARTBEAT_LIVE_HOURS) return `running hourly (last tick ${ageText} ago)`;
|
|
164
|
+
if (ageHours !== null) return `SILENT - last tick ${ageText} ago; run atris autoland tick`;
|
|
165
|
+
const installed = heartbeatLiveness(root, policy);
|
|
166
|
+
if (installed === false) return 'not installed - run atris autoland on';
|
|
167
|
+
if (installed === true) return 'installed, but no tick has ever run - run atris autoland tick';
|
|
168
|
+
return 'unknown - run atris autoland on to check and repair';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Operator-facing schedule: only promise "next tick" when a recent receipt
|
|
172
|
+
// proves the loop is alive. Otherwise name the honest gate.
|
|
173
|
+
function whenAutolandLands(root) {
|
|
174
|
+
return heartbeatIsLive(root) ? 'on the next tick' : STALE_HEARTBEAT_LANDING;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function certifiedWorkLandsPhrase(root) {
|
|
178
|
+
return heartbeatIsLive(root)
|
|
179
|
+
? 'certified work lands itself'
|
|
180
|
+
: `certified work lands ${STALE_HEARTBEAT_LANDING}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
123
183
|
function cronMarker(root) {
|
|
124
184
|
const slug = path.basename(root).replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase();
|
|
125
185
|
return `ATRIS_AUTOLAND_${slug}`;
|
|
@@ -654,8 +714,10 @@ function sendImessage(root, to, text) {
|
|
|
654
714
|
module.exports = {
|
|
655
715
|
DEFAULT_ALARM_HOURS,
|
|
656
716
|
DEFAULT_DIGEST_HOUR,
|
|
717
|
+
STALE_HEARTBEAT_LANDING,
|
|
657
718
|
acceptedInLastDay,
|
|
658
719
|
buildCronLine,
|
|
720
|
+
certifiedWorkLandsPhrase,
|
|
659
721
|
clarify,
|
|
660
722
|
composeAlarm,
|
|
661
723
|
composeDigest,
|
|
@@ -665,6 +727,9 @@ module.exports = {
|
|
|
665
727
|
digestLine,
|
|
666
728
|
dejargon,
|
|
667
729
|
explainResult,
|
|
730
|
+
heartbeatIsLive,
|
|
731
|
+
heartbeatLiveness,
|
|
732
|
+
heartbeatStatusText,
|
|
668
733
|
installCron,
|
|
669
734
|
liveAcceptAuthorization,
|
|
670
735
|
markAlerted,
|
|
@@ -684,6 +749,7 @@ module.exports = {
|
|
|
684
749
|
statePath,
|
|
685
750
|
uninstallCron,
|
|
686
751
|
waitingOnHuman,
|
|
752
|
+
whenAutolandLands,
|
|
687
753
|
writePolicy,
|
|
688
754
|
writeState,
|
|
689
755
|
};
|
package/lib/bench/runner.js
CHANGED
|
@@ -219,6 +219,22 @@ async function runSetup(spec, ctx) {
|
|
|
219
219
|
await setup(ctx);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// The one live prompt gate (lesson: benchmark-prompt-paths): receipt tests run
|
|
223
|
+
// the solution/null engines, which never read the prompt, so a malformed
|
|
224
|
+
// prompt.md sails through green dry-runs and only a paid real-engine run finds
|
|
225
|
+
// it. Every prompt an engine receives flows through here, and the tests assert
|
|
226
|
+
// real task prompts against the same function.
|
|
227
|
+
function readTaskPrompt(spec) {
|
|
228
|
+
const text = fs.readFileSync(spec.promptPath, 'utf8');
|
|
229
|
+
if (!text.trim() || text.trim().length < 20) {
|
|
230
|
+
throw new BenchInfraError(`${spec.id}: prompt.md is empty or too short to brief an engine`);
|
|
231
|
+
}
|
|
232
|
+
if (/\{\{[^}]*\}\}|<%|\uFFFD|\u0000/.test(text)) {
|
|
233
|
+
throw new BenchInfraError(`${spec.id}: prompt.md carries unresolved template or encoding artifacts`);
|
|
234
|
+
}
|
|
235
|
+
return text;
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
function formatEngineFailure(engineName, result) {
|
|
223
239
|
const stdout = String(result && result.stdout ? result.stdout : '').trim();
|
|
224
240
|
const stderr = String(result && result.stderr ? result.stderr : '').trim();
|
|
@@ -256,7 +272,7 @@ async function runAgentTaskSpec(spec, options = {}) {
|
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
await runSetup(spec, ctx);
|
|
259
|
-
const promptText =
|
|
275
|
+
const promptText = readTaskPrompt(spec);
|
|
260
276
|
const engineResult = await withTimeout(Promise.resolve(engine.run(promptText, ctx.workspace, timeoutMs)), spec, timeoutMs);
|
|
261
277
|
if (!engineResult || engineResult.status !== 0) {
|
|
262
278
|
throw new Error(formatEngineFailure(options.engine, engineResult));
|
|
@@ -493,8 +509,10 @@ module.exports = {
|
|
|
493
509
|
BenchInfraError,
|
|
494
510
|
DEFAULT_PACK,
|
|
495
511
|
findPython,
|
|
512
|
+
loadTaskSpecs,
|
|
496
513
|
packMetadata,
|
|
497
514
|
readResultRecords,
|
|
515
|
+
readTaskPrompt,
|
|
498
516
|
runBench,
|
|
499
517
|
summarizeTasks,
|
|
500
518
|
taskMetadata,
|
package/lib/context-gatherer.js
CHANGED
|
@@ -28,7 +28,13 @@ function hasContextProfile(root = process.cwd()) {
|
|
|
28
28
|
function compactText(value, max = 160) {
|
|
29
29
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
30
30
|
if (!text) return '';
|
|
31
|
-
|
|
31
|
+
if (text.length <= max) return text;
|
|
32
|
+
const slice = text.slice(0, Math.max(0, max - 3));
|
|
33
|
+
const lastSpace = slice.lastIndexOf(' ');
|
|
34
|
+
// Cut on a word boundary so a title never ends on half a word ("...an autonomo...").
|
|
35
|
+
// Fall back to the hard slice only when a single word already fills the limit.
|
|
36
|
+
const body = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
|
|
37
|
+
return `${body.replace(/[\s,;:.!?-]+$/, '')}...`;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
function inferDomain(answer) {
|