atris 3.16.0 → 3.17.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 +33 -7
- package/atris/skills/atris/SKILL.md +15 -2
- package/atris/skills/atris-feedback/SKILL.md +7 -0
- package/atris/skills/design/SKILL.md +29 -2
- package/atris/skills/engines/SKILL.md +44 -0
- package/atris/skills/flow/SKILL.md +1 -1
- package/atris/skills/wake/SKILL.md +37 -0
- package/atris/skills/youtube/SKILL.md +13 -39
- package/atris/team/validator/MEMBER.md +1 -0
- package/atris/wiki/concepts/agent-activation-contract.md +3 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
- package/atris/wiki/index.md +1 -0
- package/atris.md +43 -19
- package/bin/atris.js +446 -43
- package/commands/agent-spawn.js +480 -0
- package/commands/analytics.js +6 -3
- package/commands/apps.js +11 -0
- package/commands/autopilot.js +466 -20
- package/commands/brain.js +74 -7
- package/commands/brainstorm.js +9 -58
- package/commands/clean.js +1 -4
- package/commands/compile.js +574 -0
- package/commands/console.js +8 -3
- package/commands/deck.js +135 -0
- package/commands/init.js +22 -11
- package/commands/lesson.js +76 -0
- package/commands/member.js +252 -48
- package/commands/mission.js +405 -13
- package/commands/now.js +4 -2
- package/commands/probe.js +444 -0
- package/commands/pulse.js +504 -0
- package/commands/radar.js +1 -0
- package/commands/recap.js +233 -0
- package/commands/run.js +615 -22
- package/commands/skill.js +6 -2
- package/commands/slop.js +173 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +458 -43
- package/commands/verify.js +7 -3
- package/lib/activity-stream.js +166 -0
- package/lib/auto-accept-certified.js +23 -1
- package/lib/context-gatherer.js +170 -0
- package/lib/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/slides-deck.js +236 -0
- package/lib/state-detection.js +40 -3
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- package/lib/todo-fallback.js +2 -1
- package/lib/todo-sections.js +33 -0
- package/package.json +1 -2
- package/utils/api.js +14 -2
- package/atris/atrisDev.md +0 -717
package/commands/autopilot.js
CHANGED
|
@@ -12,6 +12,11 @@ const { execSync, execFileSync, spawnSync } = require('child_process');
|
|
|
12
12
|
const readline = require('readline');
|
|
13
13
|
const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
|
|
14
14
|
const { parseTodo } = require('../lib/todo');
|
|
15
|
+
const {
|
|
16
|
+
buildRunnerCommand,
|
|
17
|
+
buildRunnerAvailabilityCommand,
|
|
18
|
+
resolveClaudeRunnerBin,
|
|
19
|
+
} = require('../lib/runner-command');
|
|
15
20
|
const { findStalePages, findStaleTasks, healBrokenMapRefs } = require('./clean');
|
|
16
21
|
const {
|
|
17
22
|
buildScorecardData,
|
|
@@ -194,7 +199,7 @@ async function suggestNextTask(cwd, skipped = new Set(), { auto = false } = {})
|
|
|
194
199
|
const { logFile } = getLogPath();
|
|
195
200
|
if (fs.existsSync(logFile)) {
|
|
196
201
|
const content = fs.readFileSync(logFile, 'utf8');
|
|
197
|
-
const inboxMatch = content.match(/## Inbox\n([\s\S]*?)(?=\n##|$)/);
|
|
202
|
+
const inboxMatch = content.match(/## Inbox\r?\n([\s\S]*?)(?=\r?\n##|$)/);
|
|
198
203
|
if (inboxMatch && inboxMatch[1].trim()) {
|
|
199
204
|
const items = inboxMatch[1].trim().split('\n').filter(l => {
|
|
200
205
|
const t = l.trim();
|
|
@@ -393,7 +398,53 @@ function askHuman(taskTitle) {
|
|
|
393
398
|
}
|
|
394
399
|
|
|
395
400
|
/**
|
|
396
|
-
*
|
|
401
|
+
* Type-check a child_process error as a real wall-clock timeout. Node's
|
|
402
|
+
* execSync attaches `code: 'ETIMEDOUT'` (plus `signal`) on timeout — it does
|
|
403
|
+
* NOT set `killed`, so a `killed`-only guard is dead code on the exact error
|
|
404
|
+
* it was written for (lesson: etimedout-error-shape, 2026-06-10). A bare
|
|
405
|
+
* `signal` without ETIMEDOUT is NOT a timeout: it's an OOM SIGKILL or an
|
|
406
|
+
* external SIGTERM, and calling it a timeout misdiagnoses the cause.
|
|
407
|
+
*/
|
|
408
|
+
function isPhaseTimeoutError(err) {
|
|
409
|
+
return Boolean(err && err.code === 'ETIMEDOUT');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Any abnormal child death — timeout or signal kill. The group sweep in
|
|
414
|
+
* execPhaseCommandSync uses this wider net (orphans need sweeping either
|
|
415
|
+
* way); the thrown message uses the narrow predicate to name the cause.
|
|
416
|
+
*/
|
|
417
|
+
function isPhaseKillError(err) {
|
|
418
|
+
return Boolean(err && (err.killed || err.code === 'ETIMEDOUT' || err.signal));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* execSync with the phase-timeout orphan fix. Node's sync-exec timeout signals
|
|
423
|
+
* only the direct child pid — the `/bin/sh -c` wrapper — so the `claude` it
|
|
424
|
+
* spawned kept committing 160–296s past the 600s wall (lesson:
|
|
425
|
+
* etimedout-error-shape, 2026-06-10). `detached: true` makes the wrapper a
|
|
426
|
+
* process-group leader; on timeout we sweep the whole group via
|
|
427
|
+
* `process.kill(-pid, 'SIGKILL')`. ESRCH on the sweep means the group already
|
|
428
|
+
* died — fine. The original error is rethrown untouched so every call site
|
|
429
|
+
* keeps its existing catch contract (err.stdout passthrough included).
|
|
430
|
+
*/
|
|
431
|
+
function execPhaseCommandSync(cmd, opts = {}) {
|
|
432
|
+
try {
|
|
433
|
+
return execSync(cmd, { ...opts, detached: true });
|
|
434
|
+
} catch (err) {
|
|
435
|
+
if (isPhaseKillError(err) && err.pid) {
|
|
436
|
+
try {
|
|
437
|
+
process.kill(-err.pid, 'SIGKILL');
|
|
438
|
+
} catch (sweepErr) {
|
|
439
|
+
if (sweepErr.code !== 'ESRCH') throw sweepErr;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Run a phase via the configured runner subprocess.
|
|
397
448
|
*/
|
|
398
449
|
function executePhaseDetailed(phase, context, options = {}) {
|
|
399
450
|
const { verbose = false, timeout = PHASE_TIMEOUT } = options;
|
|
@@ -403,10 +454,11 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
403
454
|
fs.writeFileSync(tmpFile, prompt);
|
|
404
455
|
|
|
405
456
|
try {
|
|
406
|
-
const cmd =
|
|
457
|
+
const cmd = options.cmdOverride
|
|
458
|
+
|| buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Bash,Read,Write,Edit,Glob,Grep' });
|
|
407
459
|
const env = { ...process.env };
|
|
408
460
|
delete env.CLAUDECODE;
|
|
409
|
-
const output =
|
|
461
|
+
const output = execPhaseCommandSync(cmd, {
|
|
410
462
|
cwd: process.cwd(),
|
|
411
463
|
encoding: 'utf8',
|
|
412
464
|
timeout,
|
|
@@ -419,7 +471,12 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
419
471
|
return { prompt, output: output || '' };
|
|
420
472
|
} catch (err) {
|
|
421
473
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
422
|
-
if (err
|
|
474
|
+
if (isPhaseTimeoutError(err)) {
|
|
475
|
+
throw new Error(`${phase} phase timed out after ${timeout / 1000}s (configured runner hit the wall; any work it committed survives — reconcile from pre-tick HEADs)`);
|
|
476
|
+
}
|
|
477
|
+
if (isPhaseKillError(err)) {
|
|
478
|
+
throw new Error(`${phase} phase killed by ${err.signal || 'a signal'} before the ${timeout / 1000}s wall — not a timeout; check memory pressure or an external supervisor`);
|
|
479
|
+
}
|
|
423
480
|
if (err.stdout) {
|
|
424
481
|
return { prompt, output: err.stdout };
|
|
425
482
|
}
|
|
@@ -452,6 +509,16 @@ function getContextFiles(phase, options = {}) {
|
|
|
452
509
|
return [...new Set(files.filter(Boolean))].map((f) => `- ${f}`).join('\n');
|
|
453
510
|
}
|
|
454
511
|
|
|
512
|
+
// T35a (endgame loop-self-repair): shared-checkout git-safety contract.
|
|
513
|
+
// Lesson 39: a concurrent tick's `git reset` destroyed a sibling repo's
|
|
514
|
+
// uncommitted work. Sibling-repo edits ride per-tick worktrees (the same
|
|
515
|
+
// ../repo siblings snapshotRepoHeads tracks); destructive git on the shared
|
|
516
|
+
// checkout is forbidden (COORDINATION.md Rule 4). Interpolated into the
|
|
517
|
+
// default and self-heal do prompts — never the benchmark prompt (it never
|
|
518
|
+
// commits).
|
|
519
|
+
const SHARED_CHECKOUT_GIT_CONTRACT = `- Shared-checkout git safety (COORDINATION.md Rule 4): edits to any repo OTHER than this tick's cwd (../atrisos-backend-style sibling repos) go through a per-tick worktree — start with \`atris worktree start --member <member> --task "<task>"\`, land with \`atris worktree ship --message "<msg>" --verify "<cmd>"\`. Never edit a sibling repo's shared checkout directly.
|
|
520
|
+
- On a shared checkout, \`git reset\`, \`git checkout --\`, \`git clean\`, and stashing other agents' work are FORBIDDEN — concurrent ticks' uncommitted work lives there.`;
|
|
521
|
+
|
|
455
522
|
/**
|
|
456
523
|
* Build the right prompt for each phase, adapting to the kind of work.
|
|
457
524
|
*/
|
|
@@ -648,6 +715,7 @@ Rules:
|
|
|
648
715
|
- Execute ONE step at a time. Verify each step before moving on.
|
|
649
716
|
- Check MAP.md for file locations before grepping.
|
|
650
717
|
- Stay in scope. Only fix the bug described in the lesson — no side quests.
|
|
718
|
+
${SHARED_CHECKOUT_GIT_CONTRACT}
|
|
651
719
|
|
|
652
720
|
Read these files first:
|
|
653
721
|
${readFiles}
|
|
@@ -672,6 +740,7 @@ Rules:
|
|
|
672
740
|
- Check MAP.md for file locations before grepping.
|
|
673
741
|
- If you hit two errors on the same step, stop and flag for re-scope.
|
|
674
742
|
- Stay in scope. Don't touch files outside the task boundary.
|
|
743
|
+
${SHARED_CHECKOUT_GIT_CONTRACT}
|
|
675
744
|
|
|
676
745
|
Read these files first:
|
|
677
746
|
${readFiles}
|
|
@@ -742,6 +811,27 @@ If broken beyond quick fix, reply: failed — [reason].`;
|
|
|
742
811
|
return '';
|
|
743
812
|
}
|
|
744
813
|
|
|
814
|
+
/**
|
|
815
|
+
* Build a clean kebab-case lesson slug from free text. Strips non-alphanumerics
|
|
816
|
+
* (em-dashes were leaking into slugs verbatim) and truncates at a word boundary
|
|
817
|
+
* instead of mid-word (e.g. the old `.slice(0, 40)` produced
|
|
818
|
+
* `verify-fail-per-member-model-selection-—-the-member-`).
|
|
819
|
+
*/
|
|
820
|
+
function lessonSlug(text, maxLen = 40) {
|
|
821
|
+
const base = String(text || 'unknown')
|
|
822
|
+
.toLowerCase()
|
|
823
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
824
|
+
.replace(/^-+|-+$/g, '');
|
|
825
|
+
if (!base) return 'unknown';
|
|
826
|
+
if (base.length <= maxLen) return base;
|
|
827
|
+
const cut = base.slice(0, maxLen);
|
|
828
|
+
const lastDash = cut.lastIndexOf('-');
|
|
829
|
+
// base[maxLen] continues a word — back up to the last full word.
|
|
830
|
+
const atBoundary = base[maxLen] === '-';
|
|
831
|
+
const trimmed = atBoundary ? cut : (lastDash > 0 ? cut.slice(0, lastDash) : cut);
|
|
832
|
+
return trimmed.replace(/-+$/g, '') || 'unknown';
|
|
833
|
+
}
|
|
834
|
+
|
|
745
835
|
/**
|
|
746
836
|
* Write a lesson to atris/lessons.md
|
|
747
837
|
* Appends a line in format: - **[YYYY-MM-DD] slug** — pass/fail — explanation
|
|
@@ -884,6 +974,38 @@ function shouldAdoptPlannedVerify(kind) {
|
|
|
884
974
|
return ['staleness', 'docs', 'review', 'inbox', 'cleanup', 'feature', 'lessons', 'imagined'].includes(kind);
|
|
885
975
|
}
|
|
886
976
|
|
|
977
|
+
// Task-plane status vocabulary lint. `atris task list/queue/current --status <s>`
|
|
978
|
+
// only matches raw stored statuses (commands/task.js); `ready` is a TRANSITION
|
|
979
|
+
// (`atris task ready` moves a task to review), so `--status ready` always
|
|
980
|
+
// returns "(no tasks)" — a verify built on it is an unreachable gate; the
|
|
981
|
+
// matching listable form is --status review (lessons.md
|
|
982
|
+
// verify-status-vocabulary, 3rd occurrence 2026-06-10).
|
|
983
|
+
const LISTABLE_TASK_STATUSES = ['open', 'claimed', 'review', 'done', 'failed'];
|
|
984
|
+
const STATUS_CORRECTIONS = { ready: 'review' };
|
|
985
|
+
|
|
986
|
+
function lintVerifyTaskStatusVocabulary(text) {
|
|
987
|
+
// Scan every `atris task list|queue|current` segment (compound verifies
|
|
988
|
+
// chain with && / || / ;), then pull its --status value if present.
|
|
989
|
+
const segmentRe = /\batris\s+task\s+(?:list|queue|current)\b([^|&;]*)/g;
|
|
990
|
+
let segment;
|
|
991
|
+
while ((segment = segmentRe.exec(text)) !== null) {
|
|
992
|
+
const statusMatch = /--status[=\s]+["']?([A-Za-z0-9_-]+)["']?/.exec(segment[1]);
|
|
993
|
+
if (!statusMatch) continue;
|
|
994
|
+
const status = statusMatch[1];
|
|
995
|
+
if (LISTABLE_TASK_STATUSES.includes(status)) continue;
|
|
996
|
+
const vocabulary = LISTABLE_TASK_STATUSES.join('|');
|
|
997
|
+
const corrected = STATUS_CORRECTIONS[status];
|
|
998
|
+
const suggestion = corrected
|
|
999
|
+
? `use --status ${corrected} instead (atris task ${status} is a transition that lands tasks in ${corrected}, so --status ${status} never matches)`
|
|
1000
|
+
: `use one of --status ${vocabulary}`;
|
|
1001
|
+
return {
|
|
1002
|
+
ok: false,
|
|
1003
|
+
reason: `Verify uses unlistable task status "--status ${status}" — the listable vocabulary is ${vocabulary}; ${suggestion}`,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
887
1009
|
function validateVerifyCommandShape(cmd) {
|
|
888
1010
|
const text = String(cmd || '').trim();
|
|
889
1011
|
if (!text) return { ok: true };
|
|
@@ -893,6 +1015,8 @@ function validateVerifyCommandShape(cmd) {
|
|
|
893
1015
|
if (/\b(returns?|shows?|equals?|should|must)\b/i.test(text)) {
|
|
894
1016
|
return { ok: false, reason: 'Verify contains prose expectations instead of shell operators/assertions' };
|
|
895
1017
|
}
|
|
1018
|
+
const statusLint = lintVerifyTaskStatusVocabulary(text);
|
|
1019
|
+
if (statusLint) return statusLint;
|
|
896
1020
|
return { ok: true };
|
|
897
1021
|
}
|
|
898
1022
|
|
|
@@ -1073,17 +1197,17 @@ function parseProposedBlock(lines) {
|
|
|
1073
1197
|
}
|
|
1074
1198
|
|
|
1075
1199
|
/**
|
|
1076
|
-
* Default executor for plan-review: spawn a fresh
|
|
1200
|
+
* Default executor for plan-review: spawn a fresh configured runner call.
|
|
1077
1201
|
* Kept thin so tests can inject a stub via options.planReviewExec.
|
|
1078
1202
|
*/
|
|
1079
1203
|
function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
1080
1204
|
const tmpFile = path.join(cwd, '.autopilot-plan-review.tmp');
|
|
1081
1205
|
fs.writeFileSync(tmpFile, prompt);
|
|
1082
1206
|
try {
|
|
1083
|
-
const cmd =
|
|
1207
|
+
const cmd = buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Bash,Read,Grep,Glob' });
|
|
1084
1208
|
const env = { ...process.env };
|
|
1085
1209
|
delete env.CLAUDECODE;
|
|
1086
|
-
const output =
|
|
1210
|
+
const output = execPhaseCommandSync(cmd, {
|
|
1087
1211
|
cwd,
|
|
1088
1212
|
encoding: 'utf8',
|
|
1089
1213
|
timeout,
|
|
@@ -1112,7 +1236,18 @@ function defaultCodexExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
|
1112
1236
|
timeout,
|
|
1113
1237
|
stdio: 'pipe',
|
|
1114
1238
|
maxBuffer: 10 * 1024 * 1024,
|
|
1239
|
+
detached: true,
|
|
1115
1240
|
});
|
|
1241
|
+
// No sh wrapper here, but codex spawns its own children — sweep the group
|
|
1242
|
+
// on timeout so they cannot outlive the wall (same orphan class as the
|
|
1243
|
+
// claude sites; ESRCH means the tree is already dead).
|
|
1244
|
+
if (proc.pid && ((proc.error && proc.error.code === 'ETIMEDOUT') || proc.signal)) {
|
|
1245
|
+
try {
|
|
1246
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
1247
|
+
} catch (sweepErr) {
|
|
1248
|
+
if (sweepErr.code !== 'ESRCH') throw sweepErr;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1116
1251
|
if (proc.status !== 0 && !proc.stdout) {
|
|
1117
1252
|
throw new Error(`codex exited with status ${proc.status}: ${proc.stderr || 'no output'}`);
|
|
1118
1253
|
}
|
|
@@ -1258,6 +1393,216 @@ function appendPlanRejection(cwd, context, review) {
|
|
|
1258
1393
|
}
|
|
1259
1394
|
}
|
|
1260
1395
|
|
|
1396
|
+
// ── Timeout reconciliation (T33, endgame loop-self-repair) ─────────────────
|
|
1397
|
+
// A do-phase wall-clock timeout kills the reporter, not the work: 12 of 13
|
|
1398
|
+
// ETIMEDOUT halts in the 2026-06-10 RSI audit had real commits landed with no
|
|
1399
|
+
// receipt, no checked bullet, and a human halt (lessons: executor-timeout-wall,
|
|
1400
|
+
// tick-must-mark-own-bullet). These helpers let the tick reconcile from
|
|
1401
|
+
// pre-tick HEADs instead of halting when work provably landed.
|
|
1402
|
+
|
|
1403
|
+
function todayJournalPath(cwd) {
|
|
1404
|
+
const now = new Date();
|
|
1405
|
+
const yyyy = now.getFullYear();
|
|
1406
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
1407
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
1408
|
+
return {
|
|
1409
|
+
logFile: path.join(cwd, 'atris', 'logs', String(yyyy), `${yyyy}-${mm}-${dd}.md`),
|
|
1410
|
+
dateFormatted: `${yyyy}-${mm}-${dd}`,
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Normalize text for fuzzy task-title matching: lowercase, strip code spans,
|
|
1416
|
+
* tags, and markdown punctuation down to single-spaced words.
|
|
1417
|
+
*/
|
|
1418
|
+
function normalizeForMatch(text) {
|
|
1419
|
+
return String(text || '')
|
|
1420
|
+
.toLowerCase()
|
|
1421
|
+
.replace(/`[^`]*`/g, ' ')
|
|
1422
|
+
.replace(/\[[\w-]+\]/g, ' ')
|
|
1423
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
1424
|
+
.trim()
|
|
1425
|
+
.replace(/\s+/g, ' ');
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* A word-boundary-truncated normalized prefix of the task title, used to find
|
|
1430
|
+
* the task's TODO bullet and journal receipts without exact-string fragility.
|
|
1431
|
+
*/
|
|
1432
|
+
function taskMatchNeedle(taskTitle, maxLen = 60) {
|
|
1433
|
+
const norm = normalizeForMatch(taskTitle);
|
|
1434
|
+
if (!norm) return '';
|
|
1435
|
+
if (norm.length <= maxLen) return norm;
|
|
1436
|
+
return norm.slice(0, maxLen).replace(/\s+\S*$/, '');
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function gitHeadAt(dir) {
|
|
1440
|
+
try {
|
|
1441
|
+
return execSync('git rev-parse HEAD', { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
|
|
1442
|
+
} catch {
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Snapshot HEAD of the workspace repo plus any sibling repos named in the
|
|
1449
|
+
* task text — both explicit `../atris-cli`-style refs (the journal convention)
|
|
1450
|
+
* and bare sibling-directory names like `atris-cli` that resolve to a git
|
|
1451
|
+
* repo next to cwd. Returns [{ label, dir, head }].
|
|
1452
|
+
*/
|
|
1453
|
+
function snapshotRepoHeads(cwd, taskText = '') {
|
|
1454
|
+
const root = path.resolve(cwd);
|
|
1455
|
+
const repos = new Map([[root, '.']]);
|
|
1456
|
+
const text = String(taskText || '');
|
|
1457
|
+
for (const ref of text.match(/\.\.\/[A-Za-z0-9._-]+/g) || []) {
|
|
1458
|
+
const dir = path.resolve(cwd, ref);
|
|
1459
|
+
if (dir !== root && fs.existsSync(path.join(dir, '.git'))) repos.set(dir, ref);
|
|
1460
|
+
}
|
|
1461
|
+
for (const tok of text.match(/[A-Za-z][A-Za-z0-9._-]{2,}/g) || []) {
|
|
1462
|
+
const dir = path.resolve(cwd, '..', tok);
|
|
1463
|
+
if (dir !== root && !repos.has(dir) && fs.existsSync(path.join(dir, '.git'))) {
|
|
1464
|
+
repos.set(dir, `../${tok}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return [...repos].map(([dir, label]) => ({ label, dir, head: gitHeadAt(dir) }));
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Re-read HEADs for a prior snapshot; return the repos whose HEAD advanced
|
|
1472
|
+
* as [{ label, dir, before, after }].
|
|
1473
|
+
*/
|
|
1474
|
+
function diffAdvancedRepoHeads(snapshot) {
|
|
1475
|
+
const advanced = [];
|
|
1476
|
+
for (const repo of snapshot || []) {
|
|
1477
|
+
if (!repo || !repo.head) continue;
|
|
1478
|
+
const after = gitHeadAt(repo.dir);
|
|
1479
|
+
if (after && after !== repo.head) {
|
|
1480
|
+
advanced.push({ label: repo.label, dir: repo.dir, before: repo.head, after });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return advanced;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* The T31-typed do-phase timeout message thrown by executePhaseDetailed.
|
|
1488
|
+
* Plan/review timeouts stay human halts — only the do phase commits work
|
|
1489
|
+
* worth reconciling.
|
|
1490
|
+
*/
|
|
1491
|
+
function isDoPhaseTimeoutMessage(message) {
|
|
1492
|
+
return /\bdo phase timed out after\b/.test(String(message || ''));
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Mark the task's TODO bullet `[x]`. Matches the first un-checked,
|
|
1497
|
+
* un-struck bullet whose normalized text contains the normalized title
|
|
1498
|
+
* prefix; `- **T33:** …` becomes `- [x] **T33:** …`, `- [ ]` becomes `- [x]`.
|
|
1499
|
+
* Returns true if a bullet was marked.
|
|
1500
|
+
*/
|
|
1501
|
+
function markTodoBulletDone(cwd, taskTitle) {
|
|
1502
|
+
const needle = taskMatchNeedle(taskTitle);
|
|
1503
|
+
if (!needle) return false;
|
|
1504
|
+
for (const name of ['TODO.md', 'todo.md']) {
|
|
1505
|
+
const todoPath = path.join(cwd, 'atris', name);
|
|
1506
|
+
if (!fs.existsSync(todoPath)) continue;
|
|
1507
|
+
const lines = fs.readFileSync(todoPath, 'utf8').split('\n');
|
|
1508
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1509
|
+
const bullet = lines[i].match(/^(\s*)- (?:\[( |x)\]\s+)?(.*)$/);
|
|
1510
|
+
if (!bullet) continue;
|
|
1511
|
+
if (bullet[2] === 'x') continue;
|
|
1512
|
+
if (bullet[3].startsWith('~~')) continue;
|
|
1513
|
+
if (!normalizeForMatch(lines[i]).includes(needle)) continue;
|
|
1514
|
+
lines[i] = `${bullet[1]}- [x] ${bullet[3]}`;
|
|
1515
|
+
fs.writeFileSync(todoPath, lines.join('\n'));
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Append a block under today's journal `## Notes`, creating the journal file
|
|
1525
|
+
* if the tick dies before any other writer got to it. Never throws.
|
|
1526
|
+
*/
|
|
1527
|
+
function appendUnderNotes(cwd, block) {
|
|
1528
|
+
try {
|
|
1529
|
+
const { logFile, dateFormatted } = todayJournalPath(cwd);
|
|
1530
|
+
if (!fs.existsSync(logFile)) {
|
|
1531
|
+
const dir = path.dirname(logFile);
|
|
1532
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1533
|
+
createLogFile(logFile, dateFormatted);
|
|
1534
|
+
}
|
|
1535
|
+
let content = fs.readFileSync(logFile, 'utf8');
|
|
1536
|
+
const notesIdx = content.indexOf('## Notes');
|
|
1537
|
+
if (notesIdx === -1) {
|
|
1538
|
+
content = content.replace(/\s*$/, '') + `\n\n## Notes\n${block}\n`;
|
|
1539
|
+
} else {
|
|
1540
|
+
const eol = content.indexOf('\n', notesIdx);
|
|
1541
|
+
content = content.slice(0, eol + 1) + block + content.slice(eol + 1);
|
|
1542
|
+
}
|
|
1543
|
+
fs.writeFileSync(logFile, content);
|
|
1544
|
+
return true;
|
|
1545
|
+
} catch {
|
|
1546
|
+
return false;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function appendTimeoutReconciliation(cwd, { task, advanced }) {
|
|
1551
|
+
const now = new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
1552
|
+
const repoLines = (advanced || [])
|
|
1553
|
+
.map((r) => `- ${r.label}: ${String(r.before).slice(0, 7)} → ${String(r.after).slice(0, 7)}`)
|
|
1554
|
+
.join('\n');
|
|
1555
|
+
const block =
|
|
1556
|
+
`\n### Timeout reconciliation — ${now} — work-landed-receipt-died\n\n` +
|
|
1557
|
+
`**Task:** ${task}\n` +
|
|
1558
|
+
`**What happened:** the do-phase wall killed the reporter, but commits landed:\n` +
|
|
1559
|
+
`${repoLines}\n` +
|
|
1560
|
+
`Receipt auto-written and the TODO bullet marked; no human halt required.\n`;
|
|
1561
|
+
return appendUnderNotes(cwd, block);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function appendCheckAndAdvance(cwd, task, receiptLine) {
|
|
1565
|
+
const now = new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
1566
|
+
const block =
|
|
1567
|
+
`\n### Check-and-advance — ${now} — advanced-already-done\n\n` +
|
|
1568
|
+
`**Task:** ${task}\n` +
|
|
1569
|
+
`**What happened:** verify passed before work started AND today's journal already carries a completion receipt — the work shipped on a prior tick whose reporter died before bookkeeping. Bullet marked, picker advanced.\n` +
|
|
1570
|
+
`**Receipt:** ${receiptLine}\n`;
|
|
1571
|
+
return appendUnderNotes(cwd, block);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Scan today's journal for a completion receipt naming the task: a `C#`
|
|
1576
|
+
* completed line, a timeout-reconciliation entry, or a `**Task:**` line.
|
|
1577
|
+
* Returns the matching line, or null.
|
|
1578
|
+
*/
|
|
1579
|
+
function findCompletionReceipt(cwd, taskTitle) {
|
|
1580
|
+
const { logFile } = todayJournalPath(cwd);
|
|
1581
|
+
if (!fs.existsSync(logFile)) return null;
|
|
1582
|
+
const needle = taskMatchNeedle(taskTitle);
|
|
1583
|
+
if (!needle) return null;
|
|
1584
|
+
for (const line of fs.readFileSync(logFile, 'utf8').split('\n')) {
|
|
1585
|
+
const receiptShaped =
|
|
1586
|
+
/\*\*C\d+:\*\*/.test(line) || /\*\*Task:\*\*/.test(line) || /reconciliation/i.test(line);
|
|
1587
|
+
if (receiptShaped && normalizeForMatch(line).includes(needle)) return line.trim();
|
|
1588
|
+
}
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* After a do-phase timeout: diff the pre-tick HEAD snapshot. If commits
|
|
1594
|
+
* landed, write the journal reconciliation receipt, mark the TODO bullet, and
|
|
1595
|
+
* report outcome `work-landed-receipt-died`. If nothing landed, the caller
|
|
1596
|
+
* halts exactly as before.
|
|
1597
|
+
*/
|
|
1598
|
+
function reconcileTimedOutTick(cwd, snapshot, taskTitle) {
|
|
1599
|
+
const advanced = diffAdvancedRepoHeads(snapshot);
|
|
1600
|
+
if (advanced.length === 0) return { reconciled: false, advanced: [] };
|
|
1601
|
+
appendTimeoutReconciliation(cwd, { task: taskTitle, advanced });
|
|
1602
|
+
const bulletMarked = markTodoBulletDone(cwd, taskTitle);
|
|
1603
|
+
return { reconciled: true, outcome: 'work-landed-receipt-died', advanced, bulletMarked };
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1261
1606
|
function runTaskOnce(context, options = {}) {
|
|
1262
1607
|
const { verbose = false, cwd = process.cwd() } = options;
|
|
1263
1608
|
|
|
@@ -1318,6 +1663,25 @@ function runTaskOnce(context, options = {}) {
|
|
|
1318
1663
|
if (!skipFalsifiability && verifyResult.explicit && context.kind === 'endgame' && verifyCmd) {
|
|
1319
1664
|
try {
|
|
1320
1665
|
execSync(verifyCmd, { cwd, stdio: 'pipe', timeout: 300000 });
|
|
1666
|
+
// T33b (lesson: tick-must-mark-own-bullet): a pre-work verify pass WITH
|
|
1667
|
+
// a completion receipt already in today's journal means the work shipped
|
|
1668
|
+
// but the reporter died before bookkeeping. Check the bullet and advance
|
|
1669
|
+
// instead of wedging the picker on verify-not-falsifiable.
|
|
1670
|
+
const receipt = findCompletionReceipt(cwd, context.task);
|
|
1671
|
+
if (receipt) {
|
|
1672
|
+
const bulletMarked = markTodoBulletDone(cwd, context.task);
|
|
1673
|
+
appendCheckAndAdvance(cwd, context.task, receipt);
|
|
1674
|
+
return {
|
|
1675
|
+
outcome: 'advanced-already-done',
|
|
1676
|
+
reason: 'advanced-already-done',
|
|
1677
|
+
receipt,
|
|
1678
|
+
bulletMarked,
|
|
1679
|
+
phaseResults: {},
|
|
1680
|
+
elapsedSeconds: 0,
|
|
1681
|
+
verifyRan: true,
|
|
1682
|
+
verifyPass: true,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1321
1685
|
writeLesson(cwd, 'verify-not-falsifiable', 'fail',
|
|
1322
1686
|
`Verify \`${verifyCmd}\` passed before work started on "${context.task}". Either the rubric is trivial or the task is already done. Tick halted.`);
|
|
1323
1687
|
return {
|
|
@@ -1436,7 +1800,7 @@ function runTaskOnce(context, options = {}) {
|
|
|
1436
1800
|
elapsedSeconds: verifyTime,
|
|
1437
1801
|
};
|
|
1438
1802
|
try {
|
|
1439
|
-
const slug = (context.task
|
|
1803
|
+
const slug = lessonSlug(context.task);
|
|
1440
1804
|
writeLesson(cwd, `verify-fail-${slug}`, 'fail', `Verify command \`${verifyCmd}\` failed: ${e.message.split('\n')[0]}`);
|
|
1441
1805
|
} catch { /* lesson write must not crash the tick */ }
|
|
1442
1806
|
}
|
|
@@ -2482,7 +2846,7 @@ function getLessonVerdict(lessonLine) {
|
|
|
2482
2846
|
/**
|
|
2483
2847
|
* Propose 3 candidate next horizons for the autopilot loop. Combines
|
|
2484
2848
|
* `getIdleTickCount` + `getRecentSignals` into a prompt asking the LLM
|
|
2485
|
-
* to imagine what to work on next,
|
|
2849
|
+
* to imagine what to work on next, uses the shared runner command, and parses the
|
|
2486
2850
|
* JSON response into `[{ title, confidence, rationale }]`.
|
|
2487
2851
|
*
|
|
2488
2852
|
* Filters out candidates derived from resolved lessons (bug pattern no
|
|
@@ -2537,10 +2901,10 @@ Reply with the JSON array and nothing else.`;
|
|
|
2537
2901
|
|
|
2538
2902
|
let output = '';
|
|
2539
2903
|
try {
|
|
2540
|
-
const cmd =
|
|
2904
|
+
const cmd = buildRunnerCommand({ promptFile: tmpFile });
|
|
2541
2905
|
const env = { ...process.env };
|
|
2542
2906
|
delete env.CLAUDECODE;
|
|
2543
|
-
output =
|
|
2907
|
+
output = execPhaseCommandSync(cmd, {
|
|
2544
2908
|
cwd,
|
|
2545
2909
|
encoding: 'utf8',
|
|
2546
2910
|
timeout: PHASE_TIMEOUT,
|
|
@@ -2548,6 +2912,14 @@ Reply with the JSON array and nothing else.`;
|
|
|
2548
2912
|
maxBuffer: 10 * 1024 * 1024,
|
|
2549
2913
|
env
|
|
2550
2914
|
}).toString();
|
|
2915
|
+
} catch (err) {
|
|
2916
|
+
if (isPhaseTimeoutError(err)) {
|
|
2917
|
+
throw new Error(`horizon-proposal phase timed out after ${PHASE_TIMEOUT / 1000}s`);
|
|
2918
|
+
}
|
|
2919
|
+
if (isPhaseKillError(err)) {
|
|
2920
|
+
throw new Error(`horizon-proposal phase killed by ${err.signal || 'a signal'} before the ${PHASE_TIMEOUT / 1000}s wall — not a timeout`);
|
|
2921
|
+
}
|
|
2922
|
+
throw err;
|
|
2551
2923
|
} finally {
|
|
2552
2924
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
2553
2925
|
}
|
|
@@ -2555,7 +2927,7 @@ Reply with the JSON array and nothing else.`;
|
|
|
2555
2927
|
const start = output.indexOf('[');
|
|
2556
2928
|
const end = output.lastIndexOf(']');
|
|
2557
2929
|
if (start === -1 || end === -1 || end <= start) {
|
|
2558
|
-
throw new Error('proposeCandidateHorizons:
|
|
2930
|
+
throw new Error('proposeCandidateHorizons: configured runner returned no JSON array');
|
|
2559
2931
|
}
|
|
2560
2932
|
const jsonText = output.slice(start, end + 1);
|
|
2561
2933
|
|
|
@@ -2650,8 +3022,8 @@ async function autopilotAtris(description, options = {}) {
|
|
|
2650
3022
|
process.exit(1);
|
|
2651
3023
|
}
|
|
2652
3024
|
|
|
2653
|
-
try { execSync(
|
|
2654
|
-
console.error(
|
|
3025
|
+
try { execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' }); } catch {
|
|
3026
|
+
console.error(`${resolveClaudeRunnerBin()} CLI not found. Set ATRIS_RUNNER_BIN (or legacy ATRIS_CLAUDE_BIN), or install the configured runner first.`);
|
|
2655
3027
|
process.exit(1);
|
|
2656
3028
|
}
|
|
2657
3029
|
|
|
@@ -2835,6 +3207,17 @@ async function autopilotAtris(description, options = {}) {
|
|
|
2835
3207
|
};
|
|
2836
3208
|
const startingEndgame = readEndgameState(cwd);
|
|
2837
3209
|
|
|
3210
|
+
// T33a: snapshot pre-tick HEADs (cwd + sibling repos named in the task)
|
|
3211
|
+
// so a do-phase timeout can be reconciled against what actually landed.
|
|
3212
|
+
let preTickHeads = null;
|
|
3213
|
+
try {
|
|
3214
|
+
const verifyHint = getVerifyCommand(cwd, suggestion.task).cmd || '';
|
|
3215
|
+
preTickHeads = snapshotRepoHeads(
|
|
3216
|
+
cwd,
|
|
3217
|
+
[suggestion.task, ...(suggestion.files || []), verifyHint].join(' ')
|
|
3218
|
+
);
|
|
3219
|
+
} catch { /* snapshot failure must not block the tick */ }
|
|
3220
|
+
|
|
2838
3221
|
try {
|
|
2839
3222
|
if (verbose) {
|
|
2840
3223
|
console.log('');
|
|
@@ -2868,6 +3251,26 @@ async function autopilotAtris(description, options = {}) {
|
|
|
2868
3251
|
break;
|
|
2869
3252
|
}
|
|
2870
3253
|
|
|
3254
|
+
// T33b: the falsifiability gate found a completion receipt — the work
|
|
3255
|
+
// already shipped, the bullet is checked, move straight to the next pick.
|
|
3256
|
+
if (execution.outcome === 'advanced-already-done') {
|
|
3257
|
+
completed++;
|
|
3258
|
+
tickOutcome = 'built';
|
|
3259
|
+
tickOutcomeText = `"${lastTaskTitle}" was already done — verify passed pre-work and today's journal carries its completion receipt, so I checked the bullet and advanced.`;
|
|
3260
|
+
tickNextStep = 'pick the next endgame task';
|
|
3261
|
+
if (verbose) {
|
|
3262
|
+
console.log(' already done (journal receipt found). bullet checked, advancing.');
|
|
3263
|
+
} else {
|
|
3264
|
+
printPlainBlock([
|
|
3265
|
+
'That task was already done — verify passed before work and a completion receipt exists in today\'s journal.',
|
|
3266
|
+
'I checked the bullet and advanced.',
|
|
3267
|
+
'',
|
|
3268
|
+
'Next I will look for the next task.'
|
|
3269
|
+
].join('\n'));
|
|
3270
|
+
}
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
2871
3274
|
const planTime = execution.phaseResults.plan.elapsedSeconds;
|
|
2872
3275
|
if (verbose) console.log(` planned (${planTime}s)`);
|
|
2873
3276
|
|
|
@@ -2929,7 +3332,7 @@ async function autopilotAtris(description, options = {}) {
|
|
|
2929
3332
|
// Record commit hash + verify command for retroactive regression checks
|
|
2930
3333
|
try {
|
|
2931
3334
|
const commitHash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
|
|
2932
|
-
const taskSlug = (suggestion.task
|
|
3335
|
+
const taskSlug = lessonSlug(suggestion.task);
|
|
2933
3336
|
recordTickCommit(cwd, commitHash, execution.verifyCmd || '', taskSlug);
|
|
2934
3337
|
|
|
2935
3338
|
// Every 10th tick, run retroactive regression check
|
|
@@ -2976,6 +3379,36 @@ async function autopilotAtris(description, options = {}) {
|
|
|
2976
3379
|
}
|
|
2977
3380
|
|
|
2978
3381
|
} catch (err) {
|
|
3382
|
+
// T33a: a do-phase timeout with commits landed is a dead reporter, not
|
|
3383
|
+
// dead work — write the reconciliation receipt, mark the bullet, and
|
|
3384
|
+
// record work-landed-receipt-died instead of halting for a human.
|
|
3385
|
+
let reconciliation = null;
|
|
3386
|
+
if (isDoPhaseTimeoutMessage(err.message)) {
|
|
3387
|
+
try {
|
|
3388
|
+
reconciliation = reconcileTimedOutTick(cwd, preTickHeads, lastTaskTitle || suggestion.task);
|
|
3389
|
+
} catch { reconciliation = null; }
|
|
3390
|
+
}
|
|
3391
|
+
if (reconciliation && reconciliation.reconciled) {
|
|
3392
|
+
completed++;
|
|
3393
|
+
const landed = reconciliation.advanced
|
|
3394
|
+
.map((r) => `${r.label} ${String(r.before).slice(0, 7)} → ${String(r.after).slice(0, 7)}`)
|
|
3395
|
+
.join(', ');
|
|
3396
|
+
tickOutcome = 'work-landed-receipt-died';
|
|
3397
|
+
tickOutcomeText = `"${lastTaskTitle}" hit the do-phase wall but commits landed (${landed}). I wrote the reconciliation receipt and marked the bullet — work-landed-receipt-died, no human halt.`;
|
|
3398
|
+
tickNextStep = 'pick the next task';
|
|
3399
|
+
if (verbose) {
|
|
3400
|
+
console.log(` do phase timed out, but work landed (${landed}). reconciled — no human halt.`);
|
|
3401
|
+
} else {
|
|
3402
|
+
printPlainBlock([
|
|
3403
|
+
'The do phase timed out, but commits landed before the wall.',
|
|
3404
|
+
`Landed: ${landed}.`,
|
|
3405
|
+
'I wrote the reconciliation receipt and marked the task bullet.',
|
|
3406
|
+
'',
|
|
3407
|
+
'Next tick will pick the next task.'
|
|
3408
|
+
].join('\n'));
|
|
3409
|
+
}
|
|
3410
|
+
break;
|
|
3411
|
+
}
|
|
2979
3412
|
tickOutcome = 'halted';
|
|
2980
3413
|
tickOutcomeText = `I hit an error while running "${lastTaskTitle || 'a task'}": ${err.message}`;
|
|
2981
3414
|
tickNextStep = 'stop until a human looks at the error';
|
|
@@ -3135,7 +3568,7 @@ function isStillTrue(fact, cwd) {
|
|
|
3135
3568
|
/**
|
|
3136
3569
|
* Ask a local model whether a task/fact is still relevant.
|
|
3137
3570
|
* Called when isStillTrue returns 'unverified' — the mechanical check
|
|
3138
|
-
* couldn't confirm or deny, so we ask
|
|
3571
|
+
* couldn't confirm or deny, so we ask the configured runner to inspect the codebase.
|
|
3139
3572
|
*
|
|
3140
3573
|
* @param {{ title: string, age: number, source?: string }} fact
|
|
3141
3574
|
* @param {string} cwd - workspace root
|
|
@@ -3158,8 +3591,8 @@ Search the codebase to verify. Reply: YES <reason> or NO <reason>`;
|
|
|
3158
3591
|
try {
|
|
3159
3592
|
const env = { ...process.env };
|
|
3160
3593
|
delete env.CLAUDECODE;
|
|
3161
|
-
const cmd =
|
|
3162
|
-
const output =
|
|
3594
|
+
const cmd = buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Bash,Read,Glob,Grep' });
|
|
3595
|
+
const output = execPhaseCommandSync(cmd, {
|
|
3163
3596
|
cwd,
|
|
3164
3597
|
encoding: 'utf8',
|
|
3165
3598
|
timeout: 60000,
|
|
@@ -3192,6 +3625,13 @@ async function autopilotFromTodo(options = {}) {
|
|
|
3192
3625
|
|
|
3193
3626
|
module.exports = {
|
|
3194
3627
|
appendTickSummary,
|
|
3628
|
+
snapshotRepoHeads,
|
|
3629
|
+
diffAdvancedRepoHeads,
|
|
3630
|
+
reconcileTimedOutTick,
|
|
3631
|
+
markTodoBulletDone,
|
|
3632
|
+
findCompletionReceipt,
|
|
3633
|
+
isDoPhaseTimeoutMessage,
|
|
3634
|
+
validateVerifyCommandShape,
|
|
3195
3635
|
askHuman,
|
|
3196
3636
|
askModel,
|
|
3197
3637
|
autopilotAtris,
|
|
@@ -3218,6 +3658,7 @@ module.exports = {
|
|
|
3218
3658
|
scanAnomalies,
|
|
3219
3659
|
verifyJudgeIntegrity,
|
|
3220
3660
|
maybeWriteCompletedEndgameScorecard,
|
|
3661
|
+
readEndgameState,
|
|
3221
3662
|
renderHumanSuggestion,
|
|
3222
3663
|
renderHumanTickIntro,
|
|
3223
3664
|
proposeCandidateHorizons,
|
|
@@ -3233,5 +3674,10 @@ module.exports = {
|
|
|
3233
3674
|
scoreEndgameCandidates,
|
|
3234
3675
|
suggestNextTask,
|
|
3235
3676
|
shouldSkipAutoHumanGate,
|
|
3236
|
-
writeLesson
|
|
3677
|
+
writeLesson,
|
|
3678
|
+
isPhaseTimeoutError,
|
|
3679
|
+
isPhaseKillError,
|
|
3680
|
+
execPhaseCommandSync,
|
|
3681
|
+
executePhaseDetailed,
|
|
3682
|
+
lessonSlug
|
|
3237
3683
|
};
|