atris 3.16.1 → 3.22.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 +32 -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 +413 -31
- package/commands/agent-spawn.js +480 -0
- package/commands/analytics.js +6 -3
- package/commands/apps.js +11 -0
- package/commands/autopilot.js +42 -18
- package/commands/brain.js +74 -7
- package/commands/brainstorm.js +9 -58
- package/commands/clean.js +1 -4
- package/commands/compile.js +9 -4
- package/commands/console.js +8 -3
- package/commands/deck.js +184 -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 +105 -27
- package/commands/pulse.js +504 -0
- package/commands/radar.js +1 -0
- package/commands/recap.js +71 -25
- package/commands/run.js +615 -22
- package/commands/site.js +48 -0
- package/commands/slop.js +307 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +429 -37
- package/commands/theme.js +217 -0
- 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/deck-from-md.js +110 -0
- package/lib/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/html-render.js +257 -0
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/memory-view.js +95 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/site.js +114 -0
- package/lib/slides-deck.js +237 -0
- package/lib/state-detection.js +1 -4
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- package/lib/theme.js +264 -0
- 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,12 +398,23 @@ function askHuman(taskTitle) {
|
|
|
393
398
|
}
|
|
394
399
|
|
|
395
400
|
/**
|
|
396
|
-
* Type-check a child_process error as a timeout
|
|
397
|
-
* `code: 'ETIMEDOUT'`
|
|
398
|
-
* `killed`-only guard is dead code on the exact error
|
|
399
|
-
* (lesson: etimedout-error-shape, 2026-06-10).
|
|
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.
|
|
400
407
|
*/
|
|
401
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) {
|
|
402
418
|
return Boolean(err && (err.killed || err.code === 'ETIMEDOUT' || err.signal));
|
|
403
419
|
}
|
|
404
420
|
|
|
@@ -416,7 +432,7 @@ function execPhaseCommandSync(cmd, opts = {}) {
|
|
|
416
432
|
try {
|
|
417
433
|
return execSync(cmd, { ...opts, detached: true });
|
|
418
434
|
} catch (err) {
|
|
419
|
-
if (
|
|
435
|
+
if (isPhaseKillError(err) && err.pid) {
|
|
420
436
|
try {
|
|
421
437
|
process.kill(-err.pid, 'SIGKILL');
|
|
422
438
|
} catch (sweepErr) {
|
|
@@ -428,7 +444,7 @@ function execPhaseCommandSync(cmd, opts = {}) {
|
|
|
428
444
|
}
|
|
429
445
|
|
|
430
446
|
/**
|
|
431
|
-
* Run a phase via
|
|
447
|
+
* Run a phase via the configured runner subprocess.
|
|
432
448
|
*/
|
|
433
449
|
function executePhaseDetailed(phase, context, options = {}) {
|
|
434
450
|
const { verbose = false, timeout = PHASE_TIMEOUT } = options;
|
|
@@ -439,7 +455,7 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
439
455
|
|
|
440
456
|
try {
|
|
441
457
|
const cmd = options.cmdOverride
|
|
442
|
-
||
|
|
458
|
+
|| buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Bash,Read,Write,Edit,Glob,Grep' });
|
|
443
459
|
const env = { ...process.env };
|
|
444
460
|
delete env.CLAUDECODE;
|
|
445
461
|
const output = execPhaseCommandSync(cmd, {
|
|
@@ -456,7 +472,10 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
456
472
|
} catch (err) {
|
|
457
473
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
458
474
|
if (isPhaseTimeoutError(err)) {
|
|
459
|
-
throw new Error(`${phase} phase timed out after ${timeout / 1000}s (
|
|
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`);
|
|
460
479
|
}
|
|
461
480
|
if (err.stdout) {
|
|
462
481
|
return { prompt, output: err.stdout };
|
|
@@ -1178,14 +1197,14 @@ function parseProposedBlock(lines) {
|
|
|
1178
1197
|
}
|
|
1179
1198
|
|
|
1180
1199
|
/**
|
|
1181
|
-
* Default executor for plan-review: spawn a fresh
|
|
1200
|
+
* Default executor for plan-review: spawn a fresh configured runner call.
|
|
1182
1201
|
* Kept thin so tests can inject a stub via options.planReviewExec.
|
|
1183
1202
|
*/
|
|
1184
1203
|
function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
1185
1204
|
const tmpFile = path.join(cwd, '.autopilot-plan-review.tmp');
|
|
1186
1205
|
fs.writeFileSync(tmpFile, prompt);
|
|
1187
1206
|
try {
|
|
1188
|
-
const cmd =
|
|
1207
|
+
const cmd = buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Bash,Read,Grep,Glob' });
|
|
1189
1208
|
const env = { ...process.env };
|
|
1190
1209
|
delete env.CLAUDECODE;
|
|
1191
1210
|
const output = execPhaseCommandSync(cmd, {
|
|
@@ -2827,7 +2846,7 @@ function getLessonVerdict(lessonLine) {
|
|
|
2827
2846
|
/**
|
|
2828
2847
|
* Propose 3 candidate next horizons for the autopilot loop. Combines
|
|
2829
2848
|
* `getIdleTickCount` + `getRecentSignals` into a prompt asking the LLM
|
|
2830
|
-
* to imagine what to work on next,
|
|
2849
|
+
* to imagine what to work on next, uses the shared runner command, and parses the
|
|
2831
2850
|
* JSON response into `[{ title, confidence, rationale }]`.
|
|
2832
2851
|
*
|
|
2833
2852
|
* Filters out candidates derived from resolved lessons (bug pattern no
|
|
@@ -2882,7 +2901,7 @@ Reply with the JSON array and nothing else.`;
|
|
|
2882
2901
|
|
|
2883
2902
|
let output = '';
|
|
2884
2903
|
try {
|
|
2885
|
-
const cmd =
|
|
2904
|
+
const cmd = buildRunnerCommand({ promptFile: tmpFile });
|
|
2886
2905
|
const env = { ...process.env };
|
|
2887
2906
|
delete env.CLAUDECODE;
|
|
2888
2907
|
output = execPhaseCommandSync(cmd, {
|
|
@@ -2897,6 +2916,9 @@ Reply with the JSON array and nothing else.`;
|
|
|
2897
2916
|
if (isPhaseTimeoutError(err)) {
|
|
2898
2917
|
throw new Error(`horizon-proposal phase timed out after ${PHASE_TIMEOUT / 1000}s`);
|
|
2899
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
|
+
}
|
|
2900
2922
|
throw err;
|
|
2901
2923
|
} finally {
|
|
2902
2924
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
@@ -2905,7 +2927,7 @@ Reply with the JSON array and nothing else.`;
|
|
|
2905
2927
|
const start = output.indexOf('[');
|
|
2906
2928
|
const end = output.lastIndexOf(']');
|
|
2907
2929
|
if (start === -1 || end === -1 || end <= start) {
|
|
2908
|
-
throw new Error('proposeCandidateHorizons:
|
|
2930
|
+
throw new Error('proposeCandidateHorizons: configured runner returned no JSON array');
|
|
2909
2931
|
}
|
|
2910
2932
|
const jsonText = output.slice(start, end + 1);
|
|
2911
2933
|
|
|
@@ -3000,8 +3022,8 @@ async function autopilotAtris(description, options = {}) {
|
|
|
3000
3022
|
process.exit(1);
|
|
3001
3023
|
}
|
|
3002
3024
|
|
|
3003
|
-
try { execSync(
|
|
3004
|
-
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.`);
|
|
3005
3027
|
process.exit(1);
|
|
3006
3028
|
}
|
|
3007
3029
|
|
|
@@ -3546,7 +3568,7 @@ function isStillTrue(fact, cwd) {
|
|
|
3546
3568
|
/**
|
|
3547
3569
|
* Ask a local model whether a task/fact is still relevant.
|
|
3548
3570
|
* Called when isStillTrue returns 'unverified' — the mechanical check
|
|
3549
|
-
* couldn't confirm or deny, so we ask
|
|
3571
|
+
* couldn't confirm or deny, so we ask the configured runner to inspect the codebase.
|
|
3550
3572
|
*
|
|
3551
3573
|
* @param {{ title: string, age: number, source?: string }} fact
|
|
3552
3574
|
* @param {string} cwd - workspace root
|
|
@@ -3569,7 +3591,7 @@ Search the codebase to verify. Reply: YES <reason> or NO <reason>`;
|
|
|
3569
3591
|
try {
|
|
3570
3592
|
const env = { ...process.env };
|
|
3571
3593
|
delete env.CLAUDECODE;
|
|
3572
|
-
const cmd =
|
|
3594
|
+
const cmd = buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Bash,Read,Glob,Grep' });
|
|
3573
3595
|
const output = execPhaseCommandSync(cmd, {
|
|
3574
3596
|
cwd,
|
|
3575
3597
|
encoding: 'utf8',
|
|
@@ -3636,6 +3658,7 @@ module.exports = {
|
|
|
3636
3658
|
scanAnomalies,
|
|
3637
3659
|
verifyJudgeIntegrity,
|
|
3638
3660
|
maybeWriteCompletedEndgameScorecard,
|
|
3661
|
+
readEndgameState,
|
|
3639
3662
|
renderHumanSuggestion,
|
|
3640
3663
|
renderHumanTickIntro,
|
|
3641
3664
|
proposeCandidateHorizons,
|
|
@@ -3653,6 +3676,7 @@ module.exports = {
|
|
|
3653
3676
|
shouldSkipAutoHumanGate,
|
|
3654
3677
|
writeLesson,
|
|
3655
3678
|
isPhaseTimeoutError,
|
|
3679
|
+
isPhaseKillError,
|
|
3656
3680
|
execPhaseCommandSync,
|
|
3657
3681
|
executePhaseDetailed,
|
|
3658
3682
|
lessonSlug
|
package/commands/brain.js
CHANGED
|
@@ -3,6 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { spawnSync } = require('child_process');
|
|
5
5
|
const { refreshNowFile } = require('./now');
|
|
6
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
7
|
+
const { hasRenderedSections, isOpenSection, isDoneSection } = require('../lib/todo-sections');
|
|
6
8
|
|
|
7
9
|
const GENERATED_START = '<!-- ATRIS_BRAIN_COMPILE:START -->';
|
|
8
10
|
const GENERATED_END = '<!-- ATRIS_BRAIN_COMPILE:END -->';
|
|
@@ -296,7 +298,8 @@ function resolveStateRoot(root) {
|
|
|
296
298
|
|
|
297
299
|
function countTodoItems(todoText) {
|
|
298
300
|
const text = String(todoText || '');
|
|
299
|
-
|
|
301
|
+
// Section classification (incl. emoji-decorated headings) lives in lib/todo-sections.
|
|
302
|
+
const rendered = hasRenderedSections(text);
|
|
300
303
|
let section = null;
|
|
301
304
|
let unchecked = 0;
|
|
302
305
|
let checked = 0;
|
|
@@ -317,22 +320,46 @@ function countTodoItems(todoText) {
|
|
|
317
320
|
const isTitled = /^\s*-\s+(?:\[[ xX]\]\s+)?\*\*[^*]+:?\*\*/.test(line);
|
|
318
321
|
if (isUnchecked) unchecked += 1;
|
|
319
322
|
if (isChecked) checked += 1;
|
|
320
|
-
if (!
|
|
323
|
+
if (!rendered && (isUnchecked || (isTitled && !isChecked))) legacyOpen += 1;
|
|
321
324
|
if (!isTitled) continue;
|
|
322
325
|
|
|
323
326
|
titled += 1;
|
|
324
|
-
if (
|
|
325
|
-
if (
|
|
327
|
+
if (rendered && isOpenSection(section)) renderedOpen += 1;
|
|
328
|
+
if (rendered && isDoneSection(section)) renderedDone += 1;
|
|
326
329
|
}
|
|
327
330
|
|
|
328
331
|
return {
|
|
329
|
-
open:
|
|
332
|
+
open: rendered ? renderedOpen : legacyOpen,
|
|
330
333
|
checked,
|
|
331
334
|
titled,
|
|
332
|
-
done:
|
|
335
|
+
done: rendered ? renderedDone : checked + (text.match(/~~|DONE|✅/g) || []).length,
|
|
333
336
|
};
|
|
334
337
|
}
|
|
335
338
|
|
|
339
|
+
function parseTodoEndgame(todoText) {
|
|
340
|
+
const text = String(todoText || '');
|
|
341
|
+
const fields = {};
|
|
342
|
+
let inEndgame = false;
|
|
343
|
+
|
|
344
|
+
for (const line of text.split(/\r?\n/)) {
|
|
345
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
346
|
+
if (heading) {
|
|
347
|
+
inEndgame = heading[1].trim().toLowerCase() === 'endgame';
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (!inEndgame) continue;
|
|
351
|
+
|
|
352
|
+
const field = line.match(/^\*\*(Slug|Horizon|Source):\*\*\s*(.*?)\s*$/i);
|
|
353
|
+
if (field) fields[field[1].toLowerCase()] = field[2].trim();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const slug = fields.slug || null;
|
|
357
|
+
const horizon = fields.horizon || null;
|
|
358
|
+
const source = fields.source || null;
|
|
359
|
+
if (!slug && !horizon && !source) return null;
|
|
360
|
+
return { slug, horizon, source };
|
|
361
|
+
}
|
|
362
|
+
|
|
336
363
|
const EXECUTABLE_TASK_STATUSES = new Set(['open', 'claimed']);
|
|
337
364
|
const COMPLETED_TASK_STATUSES = new Set(['done', 'completed', 'accepted']);
|
|
338
365
|
|
|
@@ -350,15 +377,33 @@ function isCertifiedReviewTask(task) {
|
|
|
350
377
|
return Boolean(metadata.agent_certified || review.agent_certified || passCount >= 2);
|
|
351
378
|
}
|
|
352
379
|
|
|
380
|
+
function isAgentNeededReviewTask(task) {
|
|
381
|
+
if (String(task?.status || '').toLowerCase() !== 'review') return false;
|
|
382
|
+
if (isCertifiedReviewTask(task)) return false;
|
|
383
|
+
const metadata = task.metadata || {};
|
|
384
|
+
const review = task.review || {};
|
|
385
|
+
const approvalStatus = String(metadata.approval_status || review.approval_status || 'pending').toLowerCase();
|
|
386
|
+
if (approvalStatus && approvalStatus !== 'pending') return false;
|
|
387
|
+
const passCount = Number(metadata.agent_review_pass_count || review.agent_review_pass_count || 0);
|
|
388
|
+
return passCount < 2;
|
|
389
|
+
}
|
|
390
|
+
|
|
353
391
|
function summarizeTaskProjection(root) {
|
|
354
392
|
const tasks = readTaskProjectionTasks(root);
|
|
355
393
|
if (!tasks) return null;
|
|
356
394
|
|
|
357
395
|
const counts = {};
|
|
358
396
|
const certifiedReviewTasks = [];
|
|
397
|
+
const agentNeededReviewTasks = [];
|
|
359
398
|
for (const task of tasks) {
|
|
360
399
|
const status = String(task?.status || '').toLowerCase();
|
|
361
400
|
counts[status] = (counts[status] || 0) + 1;
|
|
401
|
+
if (isAgentNeededReviewTask(task)) {
|
|
402
|
+
agentNeededReviewTasks.push({
|
|
403
|
+
ref: task.display_id || task.legacy_ref || task.id,
|
|
404
|
+
title: task.title || 'Untitled task',
|
|
405
|
+
});
|
|
406
|
+
}
|
|
362
407
|
if (isCertifiedReviewTask(task)) {
|
|
363
408
|
certifiedReviewTasks.push({
|
|
364
409
|
ref: task.display_id || task.legacy_ref || task.id,
|
|
@@ -370,6 +415,7 @@ function summarizeTaskProjection(root) {
|
|
|
370
415
|
return {
|
|
371
416
|
tasks,
|
|
372
417
|
counts,
|
|
418
|
+
agentNeededReviewTasks,
|
|
373
419
|
certifiedReviewTasks,
|
|
374
420
|
};
|
|
375
421
|
}
|
|
@@ -466,6 +512,7 @@ function collectState(root) {
|
|
|
466
512
|
slug: business.slug || path.basename(root),
|
|
467
513
|
business,
|
|
468
514
|
todo: countWorkItems(root, todoText),
|
|
515
|
+
endgame: parseTodoEndgame(todoText),
|
|
469
516
|
taskProjection: summarizeTaskProjection(root),
|
|
470
517
|
hasNow: nowText.length > 0,
|
|
471
518
|
nowHeading: firstHeading(nowText, null),
|
|
@@ -628,7 +675,7 @@ function parseContributionCard(text, member) {
|
|
|
628
675
|
if (!text || !member) return null;
|
|
629
676
|
const firstName = String(member.name || member.slug || '').split(/\s+/)[0].toLowerCase();
|
|
630
677
|
const sections = String(text).split(/\n(?=##\s+)/);
|
|
631
|
-
const memberSections = sections.filter(section => new RegExp(`^##\\s+${firstName}\\b`, 'i').test(section.trim()));
|
|
678
|
+
const memberSections = sections.filter(section => new RegExp(`^##\\s+${escapeRegExp(firstName)}\\b`, 'i').test(section.trim()));
|
|
632
679
|
const section = (
|
|
633
680
|
memberSections.find(candidate => /current_score_signal\s*:/i.test(candidate))
|
|
634
681
|
|| memberSections[0]
|
|
@@ -708,12 +755,25 @@ function memberNextMove(member, state = null) {
|
|
|
708
755
|
const name = member.name || member.slug;
|
|
709
756
|
const context = `${member.startHere}\n${member.goals}`;
|
|
710
757
|
const identity = `${member.slug}\n${member.name}`;
|
|
758
|
+
const agentNeededReview = state?.taskProjection?.agentNeededReviewTasks?.[0] || null;
|
|
759
|
+
const agentNeededReviewMove = agentNeededReview
|
|
760
|
+
? `${name}: run the agent-safe review lane for ${agentNeededReview.ref}: ` +
|
|
761
|
+
`\`atris task review-chat ${agentNeededReview.ref} --as codex-review\`, then run the verifier named in the review packet and certify or revise without accepting XP.`
|
|
762
|
+
: null;
|
|
711
763
|
const certifiedReview = state?.taskProjection?.certifiedReviewTasks?.[0] || null;
|
|
764
|
+
const certifiedReviewRefs = (state?.taskProjection?.certifiedReviewTasks || [])
|
|
765
|
+
.map(task => task.ref)
|
|
766
|
+
.filter(Boolean);
|
|
712
767
|
const certifiedReviewMove = certifiedReview
|
|
713
768
|
? `${name}: hand off certified review ${certifiedReview.ref} to the operator: run ` +
|
|
714
769
|
`\`atris task accept ${certifiedReview.ref}\` if approved or ` +
|
|
715
770
|
`\`atris task revise ${certifiedReview.ref} --note "<what must change>"\` if not; do not create new work until this checkpoint is clear.`
|
|
716
771
|
: null;
|
|
772
|
+
const codexEndgameMove = certifiedReviewRefs.length > 0 && member.slug === 'codex-executor'
|
|
773
|
+
? `${name}: certified reviews ${certifiedReviewRefs.slice(0, 3).join(', ')} are human-only; do not accept XP. ` +
|
|
774
|
+
`Create the next bounded Codex task from Endgame ${state?.endgame?.slug || 'current-horizon'}: ` +
|
|
775
|
+
`${state?.endgame?.horizon || 'the highest-leverage system gap'}; include files, verifier, and stop rule before editing.`
|
|
776
|
+
: null;
|
|
717
777
|
if (member.slug === 'justin' || /justin/i.test(member.name || '')) {
|
|
718
778
|
return `${name}: run one customer-moving GTM rep, update the relevant workspace state within 10 minutes, and leave a scorecard.`;
|
|
719
779
|
}
|
|
@@ -728,6 +788,7 @@ function memberNextMove(member, state = null) {
|
|
|
728
788
|
return `${name}: choose or create one bounded mission step, run its verifier, and close it with proof, a scorecard, and the next move.`;
|
|
729
789
|
}
|
|
730
790
|
if (/validator|reviewer/i.test(identity)) {
|
|
791
|
+
if (agentNeededReviewMove) return agentNeededReviewMove;
|
|
731
792
|
if (certifiedReviewMove) return certifiedReviewMove;
|
|
732
793
|
if ((state?.todo?.open || 0) === 0 && (state?.todo?.done || 0) === 0) {
|
|
733
794
|
return `${name}: wait for one concrete artifact or ask Navigator to create a reviewable task with verifier, proof target, and residual-risk checklist.`;
|
|
@@ -736,6 +797,8 @@ function memberNextMove(member, state = null) {
|
|
|
736
797
|
}
|
|
737
798
|
if (/executor|builder/i.test(identity)) {
|
|
738
799
|
if ((state?.todo?.open || 0) === 0) {
|
|
800
|
+
if (agentNeededReviewMove) return agentNeededReviewMove;
|
|
801
|
+
if (codexEndgameMove) return codexEndgameMove;
|
|
739
802
|
if (certifiedReviewMove) return certifiedReviewMove;
|
|
740
803
|
return `${name}: ask Navigator to create one bounded task with files, verifier, and stop rule before making a patch.`;
|
|
741
804
|
}
|
|
@@ -745,6 +808,7 @@ function memberNextMove(member, state = null) {
|
|
|
745
808
|
return `${name}: turn one messy or unclaimed intent into a MAP-backed plan with ASCII visualization, exact files, verifier, rollback, and a review-ready task.`;
|
|
746
809
|
}
|
|
747
810
|
if (/launcher|closer/i.test(identity)) {
|
|
811
|
+
if (agentNeededReviewMove) return agentNeededReviewMove;
|
|
748
812
|
if (certifiedReviewMove) return certifiedReviewMove;
|
|
749
813
|
if ((state?.todo?.done || 0) === 0) {
|
|
750
814
|
return `${name}: wait for one validated task receipt before closeout, or ask Validator to produce a review decision with proof.`;
|
|
@@ -1571,4 +1635,7 @@ module.exports = {
|
|
|
1571
1635
|
verifyActivationCard,
|
|
1572
1636
|
verifyActivationGallery,
|
|
1573
1637
|
verifyBrain,
|
|
1638
|
+
parseContributionCard,
|
|
1639
|
+
escapeRegExp,
|
|
1640
|
+
countTodoItems,
|
|
1574
1641
|
};
|
package/commands/brainstorm.js
CHANGED
|
@@ -2,6 +2,15 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
|
|
5
|
+
// Inbox helpers live canonically (and CRLF-tolerant) in lib/file-ops; brainstorm
|
|
6
|
+
// used to carry byte-identical local copies that silently missed CRLF journals.
|
|
7
|
+
const {
|
|
8
|
+
parseInboxItems,
|
|
9
|
+
replaceInboxSection,
|
|
10
|
+
addInboxItemToContent,
|
|
11
|
+
getNextInboxId,
|
|
12
|
+
addInboxIdea,
|
|
13
|
+
} = require('../lib/file-ops');
|
|
5
14
|
const { loadConfig } = require('../utils/config');
|
|
6
15
|
const { loadCredentials, ensureValidCredentials } = require('../utils/auth');
|
|
7
16
|
const { apiRequestJson } = require('../utils/api');
|
|
@@ -351,69 +360,11 @@ function brainstormAbortError() {
|
|
|
351
360
|
return error;
|
|
352
361
|
}
|
|
353
362
|
|
|
354
|
-
function addInboxIdea(logFile, summary) {
|
|
355
|
-
const content = fs.readFileSync(logFile, 'utf8');
|
|
356
|
-
const nextId = getNextInboxId(content);
|
|
357
|
-
const updated = addInboxItemToContent(content, nextId, summary);
|
|
358
|
-
fs.writeFileSync(logFile, updated);
|
|
359
|
-
return nextId;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function parseInboxItems(content) {
|
|
363
|
-
const match = content.match(/## Inbox\n([\s\S]*?)(?=\n##|\n---|$)/);
|
|
364
|
-
if (!match) {
|
|
365
|
-
return [];
|
|
366
|
-
}
|
|
367
|
-
const body = match[1];
|
|
368
|
-
const lines = body.split('\n');
|
|
369
|
-
const items = [];
|
|
370
|
-
lines.forEach((line) => {
|
|
371
|
-
const trimmed = line.trim();
|
|
372
|
-
if (!trimmed) return;
|
|
373
|
-
if (trimmed.startsWith('(Empty')) return;
|
|
374
|
-
const parsed = trimmed.match(/^- \*\*I(\d+):\*\*\s*(.+)$|^- \*\*I(\d+):\s+(.+)$/);
|
|
375
|
-
if (parsed) {
|
|
376
|
-
const id = parseInt(parsed[1] || parsed[3], 10);
|
|
377
|
-
const text = parsed[2] || parsed[4];
|
|
378
|
-
items.push({ id, text, line: trimmed });
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
return items;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function replaceInboxSection(content, items) {
|
|
385
|
-
const regex = /(## Inbox\n)([\s\S]*?)(\n---|\n##|$)/;
|
|
386
|
-
if (!regex.test(content)) {
|
|
387
|
-
const lines = items.length ? items.map((item) => item.line).join('\n') : '(Empty - inbox zero achieved)';
|
|
388
|
-
return `${content}\n\n## Inbox\n\n${lines}\n`;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return content.replace(regex, (match, header, body, suffix) => {
|
|
392
|
-
const inner = items.length
|
|
393
|
-
? `\n${items.map((item) => item.line).join('\n')}\n`
|
|
394
|
-
: '\n(Empty - inbox zero achieved)\n';
|
|
395
|
-
return `${header}${inner}${suffix}`;
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function addInboxItemToContent(content, id, summary) {
|
|
400
|
-
const items = parseInboxItems(content).filter((item) => item.id !== id);
|
|
401
|
-
const newItem = { id, text: summary, line: `- **I${id}:** ${summary}` };
|
|
402
|
-
const updatedItems = [newItem, ...items];
|
|
403
|
-
return replaceInboxSection(content, updatedItems);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
363
|
function removeInboxItemFromContent(content, id) {
|
|
407
364
|
const items = parseInboxItems(content).filter((item) => item.id !== id);
|
|
408
365
|
return replaceInboxSection(content, items);
|
|
409
366
|
}
|
|
410
367
|
|
|
411
|
-
function getNextInboxId(content) {
|
|
412
|
-
const items = parseInboxItems(content);
|
|
413
|
-
if (items.length === 0) return 1;
|
|
414
|
-
return items.reduce((max, item) => (item.id > max ? item.id : max), 0) + 1;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
368
|
function insertIntoNotesSection(content, block) {
|
|
418
369
|
const regex = /(## Notes\n)([\s\S]*?)(\n---|\n##|$)/;
|
|
419
370
|
const match = content.match(regex);
|
package/commands/clean.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* atris clean - Workspace housekeeping with auto-heal
|
|
@@ -396,10 +397,6 @@ function findSymbolLine(fileContent, symbol) {
|
|
|
396
397
|
return null;
|
|
397
398
|
}
|
|
398
399
|
|
|
399
|
-
function escapeRegExp(string) {
|
|
400
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
401
|
-
}
|
|
402
|
-
|
|
403
400
|
/**
|
|
404
401
|
* Find wiki pages whose sources have been modified after last_compiled.
|
|
405
402
|
* Scans all .md files under atris/ for frontmatter with last_compiled + sources.
|
package/commands/compile.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the promoted artifact runs token-free.
|
|
7
7
|
*
|
|
8
8
|
* atris compile record <name> --input <json|@file> --output <json|@file>
|
|
9
|
-
* atris compile build <name> (uses
|
|
9
|
+
* atris compile build <name> (uses the shared runner command)
|
|
10
10
|
* atris compile backtest <name>
|
|
11
11
|
* atris compile promote <name>
|
|
12
12
|
* atris compile exec <name> --input <json|@file> [--record]
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const { execSync } = require('child_process');
|
|
21
|
+
const {
|
|
22
|
+
buildRunnerAvailabilityCommand,
|
|
23
|
+
buildRunnerCommand,
|
|
24
|
+
resolveClaudeRunnerBin,
|
|
25
|
+
} = require('../lib/runner-command');
|
|
21
26
|
|
|
22
27
|
const DEFAULT_THRESHOLD = 0.99;
|
|
23
28
|
const SAMPLE_RECORDS_FOR_BUILD = 25;
|
|
@@ -275,9 +280,9 @@ function executeBuild(root, name, options = {}) {
|
|
|
275
280
|
|
|
276
281
|
if (!cmdOverride) {
|
|
277
282
|
try {
|
|
278
|
-
execSync(
|
|
283
|
+
execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' });
|
|
279
284
|
} catch {
|
|
280
|
-
throw new Error(
|
|
285
|
+
throw new Error(`${resolveClaudeRunnerBin()} CLI not found. Set ATRIS_RUNNER_BIN (or legacy ATRIS_CLAUDE_BIN), or install the configured runner first.`);
|
|
281
286
|
}
|
|
282
287
|
}
|
|
283
288
|
|
|
@@ -287,7 +292,7 @@ function executeBuild(root, name, options = {}) {
|
|
|
287
292
|
fs.writeFileSync(tmpFile, prompt);
|
|
288
293
|
|
|
289
294
|
try {
|
|
290
|
-
const cmd = cmdOverride ||
|
|
295
|
+
const cmd = cmdOverride || buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Read,Write,Edit,Glob,Grep' });
|
|
291
296
|
const env = { ...process.env };
|
|
292
297
|
delete env.CLAUDECODE;
|
|
293
298
|
execSync(cmd, {
|
package/commands/console.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { spawn, spawnSync } = require('child_process');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
+
const { resolveClaudeRunnerBin } = require('../lib/runner-command');
|
|
6
7
|
|
|
7
8
|
// ── Context Gathering ──────────────────────────────────────────────
|
|
8
9
|
|
|
@@ -206,7 +207,10 @@ function renderSkillsBar(ctx) {
|
|
|
206
207
|
// ── Backend Detection & Auth ───────────────────────────────────────
|
|
207
208
|
|
|
208
209
|
function detectBackend(requested) {
|
|
209
|
-
const
|
|
210
|
+
const claudeBin = resolveClaudeRunnerBin();
|
|
211
|
+
const hasClaude = claudeBin.includes(path.sep)
|
|
212
|
+
? fs.existsSync(claudeBin)
|
|
213
|
+
: spawnSync('which', [claudeBin], { stdio: 'pipe' }).status === 0;
|
|
210
214
|
const hasCodex = spawnSync('which', ['codex'], { stdio: 'pipe' }).status === 0;
|
|
211
215
|
|
|
212
216
|
if (requested) {
|
|
@@ -277,20 +281,21 @@ function checkAuth(backend) {
|
|
|
277
281
|
// ── Launch ──────────────────────────────────────────────────────────
|
|
278
282
|
|
|
279
283
|
function launchClaude(systemPrompt, extraArgs) {
|
|
284
|
+
const runnerBin = resolveClaudeRunnerBin();
|
|
280
285
|
const args = [
|
|
281
286
|
'--dangerously-skip-permissions',
|
|
282
287
|
'--append-system-prompt', systemPrompt,
|
|
283
288
|
...extraArgs,
|
|
284
289
|
];
|
|
285
290
|
|
|
286
|
-
const child = spawnSync(
|
|
291
|
+
const child = spawnSync(runnerBin, args, {
|
|
287
292
|
cwd: process.cwd(),
|
|
288
293
|
stdio: 'inherit',
|
|
289
294
|
env: { ...process.env, CLAUDECODE: undefined },
|
|
290
295
|
});
|
|
291
296
|
|
|
292
297
|
if (child.error) {
|
|
293
|
-
console.error(`✗ Failed to start
|
|
298
|
+
console.error(`✗ Failed to start ${runnerBin}: ${child.error.message}`);
|
|
294
299
|
process.exit(1);
|
|
295
300
|
}
|
|
296
301
|
process.exit(child.status ?? 0);
|