atris 3.37.1 → 3.40.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/README.md +2 -0
- package/atris/PERSONA.md +11 -11
- package/atris.md +17 -2
- package/ax +160 -34
- package/bin/atris.js +78 -6
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +13 -18
- package/commands/autoland.js +76 -47
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/bench.js +10 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +452 -12
- package/commands/chat-scan.js +5 -7
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +8 -12
- package/commands/computer.js +6 -5
- package/commands/console.js +64 -11
- package/commands/decide.js +166 -0
- package/commands/deck.js +30 -12
- package/commands/dream.js +0 -7
- package/commands/drill.js +14 -25
- package/commands/engine.js +206 -26
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +17 -7
- package/commands/goal.js +0 -6
- package/commands/harvest.js +1 -5
- package/commands/improve.js +29 -8
- package/commands/init.js +41 -37
- package/commands/integrations.js +0 -7
- package/commands/land.js +77 -20
- package/commands/launchpad.js +1 -16
- package/commands/lesson.js +17 -7
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +622 -573
- package/commands/mission.js +790 -314
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +3599 -424
- package/commands/play.js +6 -6
- package/commands/probe.js +12 -6
- package/commands/pulse.js +88 -64
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/release.js +10 -9
- package/commands/report.js +0 -1
- package/commands/router.js +320 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/site-deploy.js +870 -0
- package/commands/site.js +11 -2
- package/commands/skill.js +7 -2
- package/commands/slop.js +237 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +4 -20
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +999 -595
- package/commands/taste.js +101 -0
- package/commands/teach.js +86 -0
- package/commands/team.js +83 -3
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/vercel.js +4 -2
- package/commands/visualize.js +0 -5
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -28
- package/commands/wiki.js +42 -15
- package/commands/wish.js +0 -1
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +88 -28
- package/commands/xp.js +28 -30
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +126 -0
- package/lib/activity-stream.js +0 -2
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +261 -3
- package/lib/autoland.js +66 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +18 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +7 -2
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +157 -28
- package/lib/experiments/daily.js +0 -6
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +275 -42
- package/lib/functional-owner.js +0 -3
- package/lib/git-spawn.js +15 -0
- package/lib/inspect-fields.js +0 -1
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +11 -19
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +296 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +47 -6
- package/lib/orb-context.js +14 -4
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/pack-capabilities.js +685 -0
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +51 -3
- package/lib/runner-command.js +0 -10
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +45 -14
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +137 -7
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/team-presence.js +0 -1
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +129 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +21 -7
- package/lib/wish-delegate.js +27 -21
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +95 -8
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
package/lib/fleet.js
CHANGED
|
@@ -24,16 +24,19 @@ const {
|
|
|
24
24
|
} = require('./brief-ledger');
|
|
25
25
|
const { RUNNER_PROFILE_DEFS, buildRunnerCommand } = require('./runner-command');
|
|
26
26
|
const { resolveDefaultVerifier } = require('./default-verifier');
|
|
27
|
-
const {
|
|
27
|
+
const { rankEnginesDetailed } = require('./router-brain');
|
|
28
28
|
const {
|
|
29
29
|
buildOneLapValidatorPrompt,
|
|
30
30
|
parseOneLapValidatorVerdict,
|
|
31
31
|
} = require('./one-lap-validator');
|
|
32
32
|
const { listWorktrees } = require('../commands/worktree');
|
|
33
|
+
const { isConductorStatusLine } = require('./conductor-artifacts');
|
|
34
|
+
const { matchLessons } = require('./lesson-preflight');
|
|
35
|
+
const { matchTaste } = require('./taste-lessons');
|
|
33
36
|
|
|
34
37
|
// Lanes a fleet may never staff on its own: the human keeps irreversible
|
|
35
38
|
// calls. Mirrors the autoland denied lanes.
|
|
36
|
-
const DENIED_TAGS = ['billing', 'deploy', 'security', 'customer', 'external', 'feedback'];
|
|
39
|
+
const DENIED_TAGS = ['billing', 'money', 'payments', 'deploy', 'security', 'customer', 'external', 'feedback'];
|
|
37
40
|
|
|
38
41
|
// ---------------------------------------------------------------------------
|
|
39
42
|
// T1 — dispatch primitive
|
|
@@ -93,6 +96,30 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
|
|
|
93
96
|
'',
|
|
94
97
|
'Final report (plain text): files changed, test command + result, commit sha (or say the commit failed and why).'
|
|
95
98
|
);
|
|
99
|
+
const lessonFiles = [
|
|
100
|
+
...(Array.isArray(task.files) ? task.files : []),
|
|
101
|
+
...(Array.isArray(task.metadata && task.metadata.files) ? task.metadata.files : []),
|
|
102
|
+
...fileSurface(task),
|
|
103
|
+
];
|
|
104
|
+
const briefText = lines.join('\n');
|
|
105
|
+
const preflightRoot = worktreePath || process.cwd();
|
|
106
|
+
const lessons = matchLessons({
|
|
107
|
+
briefText,
|
|
108
|
+
files: lessonFiles,
|
|
109
|
+
root: preflightRoot,
|
|
110
|
+
});
|
|
111
|
+
if (lessons.length) {
|
|
112
|
+
lines.push('', '## lessons that apply', ...lessons.map((lesson) => `- ${lesson.text}`));
|
|
113
|
+
}
|
|
114
|
+
const requestedTasteScope = String(
|
|
115
|
+
task.scope || (task.metadata && task.metadata.scope) || task.tag || 'any'
|
|
116
|
+
).toLowerCase();
|
|
117
|
+
const taste = matchTaste({ briefText, scope: requestedTasteScope, root: preflightRoot });
|
|
118
|
+
if (taste.length) {
|
|
119
|
+
lines.push('', "## the owner's taste", ...taste.map((entry) => (
|
|
120
|
+
`- The operator's verdict is ${entry.verdict} for "${entry.subject}". The reason is: ${entry.why}`
|
|
121
|
+
)));
|
|
122
|
+
}
|
|
96
123
|
return lines.join('\n');
|
|
97
124
|
}
|
|
98
125
|
|
|
@@ -317,6 +344,20 @@ function dispatchResultExitCode(result) {
|
|
|
317
344
|
return 0;
|
|
318
345
|
}
|
|
319
346
|
|
|
347
|
+
// A child killed by a signal comes back either with spawnSync's `signal` field
|
|
348
|
+
// set (plain spawn) or a 128+N exit status (the sealed supervisor re-exits 143
|
|
349
|
+
// on SIGTERM). Either way stdout/report and stderr are empty, so name the
|
|
350
|
+
// signal instead of letting the leg read as a silent no-op (CLI-1190: on
|
|
351
|
+
// WEB-455 2026-07-25 the claude engine exited 143/SIGTERM with an empty report
|
|
352
|
+
// and the restaff receipt looked like nothing had happened).
|
|
353
|
+
const SIGNAL_EXIT_NAMES = Object.freeze({ 129: 'SIGHUP', 130: 'SIGINT', 137: 'SIGKILL', 143: 'SIGTERM' });
|
|
354
|
+
function dispatchResultSignal(result) {
|
|
355
|
+
if (!result || typeof result !== 'object') return null;
|
|
356
|
+
if (result.signal) return String(result.signal);
|
|
357
|
+
const code = dispatchResultExitCode(result);
|
|
358
|
+
return SIGNAL_EXIT_NAMES[code] || null;
|
|
359
|
+
}
|
|
360
|
+
|
|
320
361
|
function dispatchTaskId(task) {
|
|
321
362
|
return task && (task.display_id || task.task_id || task.id) ? String(task.display_id || task.task_id || task.id) : '';
|
|
322
363
|
}
|
|
@@ -364,6 +405,8 @@ function detectDeadEngineDispatch(result) {
|
|
|
364
405
|
const output = dispatchResultOutput(result).toLowerCase();
|
|
365
406
|
const pattern = DEAD_ENGINE_OUTPUT_PATTERNS.find((p) => output.includes(p));
|
|
366
407
|
if (pattern) return { reason: 'usage_limit', pattern };
|
|
408
|
+
const signal = dispatchResultSignal(result);
|
|
409
|
+
if (signal) return { reason: 'signalled', signal, exitCode };
|
|
367
410
|
return { reason: 'nonzero_exit', exitCode };
|
|
368
411
|
}
|
|
369
412
|
|
|
@@ -379,13 +422,18 @@ function installedFleetEngines(root) {
|
|
|
379
422
|
return normalizeInstalledEngines(roster(root).filter((e) => e.installed));
|
|
380
423
|
}
|
|
381
424
|
|
|
382
|
-
function
|
|
383
|
-
return
|
|
425
|
+
function rankFleetEnginesDetailed(engines, root = process.cwd(), options = {}) {
|
|
426
|
+
return rankEnginesDetailed(normalizeInstalledEngines(engines), {
|
|
384
427
|
root,
|
|
385
428
|
taskType: 'executor',
|
|
429
|
+
lowStakes: options.lowStakes === true,
|
|
386
430
|
});
|
|
387
431
|
}
|
|
388
432
|
|
|
433
|
+
function rankFleetEngines(engines, root = process.cwd()) {
|
|
434
|
+
return rankFleetEnginesDetailed(engines, root).candidates;
|
|
435
|
+
}
|
|
436
|
+
|
|
389
437
|
function nextInstalledFleetEngine(current, { root = process.cwd(), installedEngines = null } = {}) {
|
|
390
438
|
const engines = installedEngines ? normalizeInstalledEngines(installedEngines) : installedFleetEngines(root);
|
|
391
439
|
const currentName = String(current || '').trim();
|
|
@@ -406,12 +454,21 @@ function normalizeDispatchResult(result, engineName) {
|
|
|
406
454
|
}
|
|
407
455
|
|
|
408
456
|
function failedDispatchLeg(result, engineName) {
|
|
409
|
-
|
|
457
|
+
const report = String(result && result.report || '').slice(-2000);
|
|
458
|
+
const signal = dispatchResultSignal(result);
|
|
459
|
+
const leg = {
|
|
410
460
|
engine: String(result && result.engine || engineName || ''),
|
|
411
461
|
exitCode: dispatchResultExitCode(result),
|
|
412
462
|
stderr: String(result && result.stderr || '').slice(-2000),
|
|
413
|
-
report
|
|
463
|
+
report,
|
|
414
464
|
};
|
|
465
|
+
if (signal) {
|
|
466
|
+
// Record that the predecessor was killed; a signalled leg has no report of
|
|
467
|
+
// its own, so name the signal in place of the empty string (CLI-1190).
|
|
468
|
+
leg.signal = signal;
|
|
469
|
+
if (!report) leg.report = `(no report: killed by ${signal})`;
|
|
470
|
+
}
|
|
471
|
+
return leg;
|
|
415
472
|
}
|
|
416
473
|
|
|
417
474
|
async function dispatchEntryWithRestaff({
|
|
@@ -535,6 +592,7 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
535
592
|
brief_id: capturedBriefId || null,
|
|
536
593
|
command,
|
|
537
594
|
exitCode: result.status,
|
|
595
|
+
signal: result.signal || null,
|
|
538
596
|
report: String(result.stdout || '').slice(-8000),
|
|
539
597
|
stderr: String(result.stderr || '').slice(-2000),
|
|
540
598
|
};
|
|
@@ -543,27 +601,30 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
543
601
|
// ---------------------------------------------------------------------------
|
|
544
602
|
// T2 — staffing
|
|
545
603
|
|
|
604
|
+
const {
|
|
605
|
+
taskTagTokens,
|
|
606
|
+
isDecisionHoldTag,
|
|
607
|
+
isDecisionTask,
|
|
608
|
+
DECISION_REFUSE_REASON,
|
|
609
|
+
} = require('./task-decision');
|
|
610
|
+
|
|
546
611
|
function taskTags(task) {
|
|
547
|
-
const fromTags = Array.isArray(task.tags) ? task.tags : [];
|
|
548
|
-
const fromTag = task && task.tag ? [task.tag] : [];
|
|
549
612
|
// Tags added after creation live in metadata.tags (`atris task tag`); a
|
|
550
613
|
// fleet that only read task.tags/title hashtags would ignore an owner-hold
|
|
551
614
|
// flag stamped on a live task and keep restaffing it (CLI-879).
|
|
552
|
-
|
|
553
|
-
const fromTitle = (String(task.title || '').match(/#([a-z0-9-]+)/gi) || []).map((t) => t.slice(1));
|
|
554
|
-
return [...fromTag, ...fromTags, ...fromMeta, ...fromTitle].map((t) => String(t).toLowerCase());
|
|
615
|
+
return taskTagTokens(task);
|
|
555
616
|
}
|
|
556
617
|
|
|
557
618
|
// A task flagged for a human decision is never fleet-staffable, whatever its
|
|
558
|
-
// lane. Mirrors the sweep's needs-human hold so both loops agree.
|
|
619
|
+
// lane. Mirrors the sweep's needs-human hold so both loops agree. Also honors
|
|
620
|
+
// the clearer `decision` tag so policy questions stay off autonomous lanes.
|
|
559
621
|
function isHumanHoldTag(tag) {
|
|
560
|
-
|
|
561
|
-
return normalized === 'needs-human' || normalized === 'needshuman';
|
|
622
|
+
return isDecisionHoldTag(tag);
|
|
562
623
|
}
|
|
563
624
|
|
|
564
625
|
function isSafeLane(task) {
|
|
626
|
+
if (isDecisionTask(task)) return false;
|
|
565
627
|
const tags = taskTags(task);
|
|
566
|
-
if (tags.some(isHumanHoldTag)) return false;
|
|
567
628
|
return !tags.some((t) => DENIED_TAGS.includes(t));
|
|
568
629
|
}
|
|
569
630
|
|
|
@@ -617,10 +678,41 @@ function assignEngines(staffed, engines) {
|
|
|
617
678
|
// Land one arrival: rebase onto latest base first; a conflict pauses the
|
|
618
679
|
// landing (never auto-resolve) and reports which files collided so the
|
|
619
680
|
// conductor or a human can take it. `git` is injectable for tests.
|
|
681
|
+
// Ship failures print the cause first (MODULE_NOT_FOUND, command name) and a
|
|
682
|
+
// long node stack after. Tail-only clips hide the cause; keep a head+tail.
|
|
683
|
+
function clipHeadTail(text, { head = 400, tail = 300 } = {}) {
|
|
684
|
+
const s = String(text || '');
|
|
685
|
+
if (s.length <= head + tail) return s;
|
|
686
|
+
return `${s.slice(0, head)}\n...\n${s.slice(-tail)}`;
|
|
687
|
+
}
|
|
688
|
+
|
|
620
689
|
// Fleet landings always target master. Without the explicit --target, ship
|
|
621
690
|
// falls back to the launcher branch's atris-base — a flight launched from a
|
|
622
691
|
// feature-branch checkout would merge PRs into that branch while the receipt
|
|
623
692
|
// and land board define "landed" as in-master (PRs #207/#208, 2026-07-04).
|
|
693
|
+
// A ship failure must name its real cause. MODULE_NOT_FOUND from the spawned
|
|
694
|
+
// CLI is usually transient: the fleet ships by spawning node against this
|
|
695
|
+
// live checkout, and a concurrent rebase/pull can make a require vanish for a
|
|
696
|
+
// moment. So the ship retries once on that signature, and the failure detail
|
|
697
|
+
// always surfaces the missing-module line from either stream instead of
|
|
698
|
+
// letting head/tail clipping truncate it away (cost measured live: CLI-1185
|
|
699
|
+
// receipts kept only a loader stack tail and the module name was lost).
|
|
700
|
+
function shipWithRetry(cli, args, cwd) {
|
|
701
|
+
let shipped = cli(args, cwd);
|
|
702
|
+
if (shipped.status !== 0 && /MODULE_NOT_FOUND|Cannot find module/.test(`${shipped.stderr}${shipped.stdout}`)) {
|
|
703
|
+
shipped = cli(args, cwd);
|
|
704
|
+
shipped.retried_module_not_found = true;
|
|
705
|
+
}
|
|
706
|
+
return shipped;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function shipFailureDetail(shipped, { head = 400, tail = 300 } = {}) {
|
|
710
|
+
const combined = `${shipped.stderr || ''}\n${shipped.stdout || ''}`;
|
|
711
|
+
const moduleLine = combined.match(/Cannot find module '[^']+'[^\n]*/);
|
|
712
|
+
const cause = moduleLine ? `${moduleLine[0]}${shipped.retried_module_not_found ? ' (persisted after one retry)' : ''}\n` : '';
|
|
713
|
+
return `${cause}${clipHeadTail(combined.trim(), { head, tail })}`;
|
|
714
|
+
}
|
|
715
|
+
|
|
624
716
|
function fleetShipArgs(entry, check) {
|
|
625
717
|
return [
|
|
626
718
|
'worktree', 'ship',
|
|
@@ -631,19 +723,113 @@ function fleetShipArgs(entry, check) {
|
|
|
631
723
|
];
|
|
632
724
|
}
|
|
633
725
|
|
|
726
|
+
// A fleet build worktree is ephemeral. If any process inside one runs
|
|
727
|
+
// `npm link` (which repoints the global `atris` package symlink at that
|
|
728
|
+
// worktree), then removing the worktree at teardown leaves the global `atris`
|
|
729
|
+
// binary a dangling symlink, and everything resolving `atris` through PATH —
|
|
730
|
+
// interactive shells, any tooling that shells out to the bare command — dies
|
|
731
|
+
// with 'command not found'. Measured scope, not assumed: crons that invoke
|
|
732
|
+
// `node <checkout>/bin/atris.js` by absolute path are UNAFFECTED; an autoland
|
|
733
|
+
// tick ran fine four minutes before the repair. The failure is silent and
|
|
734
|
+
// reads like a shell/PATH problem, not a worktree-cleanup one (found live
|
|
735
|
+
// 2026-07-26, CLI-1193; same shape as the cron PATH wedge that cost 20 days
|
|
736
|
+
// of autoland).
|
|
737
|
+
// Recovery was one line: repoint the global link at the real checkout. So
|
|
738
|
+
// teardown does exactly that on its own: if the global `atris` link points
|
|
739
|
+
// into one of this flight's worktrees, repoint it at the primary checkout and
|
|
740
|
+
// verify the CLI still runs before the flight reports success.
|
|
741
|
+
function defaultReadGlobalCliLink() {
|
|
742
|
+
const rootResult = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
|
|
743
|
+
if (rootResult.status !== 0) return null;
|
|
744
|
+
const globalRoot = String(rootResult.stdout || '').trim();
|
|
745
|
+
if (!globalRoot) return null;
|
|
746
|
+
const pkgPath = path.join(globalRoot, 'atris');
|
|
747
|
+
let stat;
|
|
748
|
+
try {
|
|
749
|
+
stat = fs.lstatSync(pkgPath);
|
|
750
|
+
} catch {
|
|
751
|
+
return null; // atris is not installed globally at all
|
|
752
|
+
}
|
|
753
|
+
if (!stat.isSymbolicLink()) return { path: pkgPath, target: null }; // real install, not a link
|
|
754
|
+
return { path: pkgPath, target: path.resolve(path.dirname(pkgPath), fs.readlinkSync(pkgPath)) };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function defaultRepointGlobalCliLink(primaryCheckout) {
|
|
758
|
+
// `npm link` from the real checkout re-creates the global symlink pointing at it.
|
|
759
|
+
return spawnSync('npm', ['link'], { cwd: primaryCheckout, encoding: 'utf8' });
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function defaultVerifyCliRuns() {
|
|
763
|
+
const result = spawnSync('atris', ['--version'], { encoding: 'utf8' });
|
|
764
|
+
return { ok: result.status === 0, status: result.status, stdout: String(result.stdout || '').trim() };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function guardGlobalCliLink({
|
|
768
|
+
root = process.cwd(),
|
|
769
|
+
worktreePaths = [],
|
|
770
|
+
readGlobalLink = defaultReadGlobalCliLink,
|
|
771
|
+
repointLink = defaultRepointGlobalCliLink,
|
|
772
|
+
verifyCli = defaultVerifyCliRuns,
|
|
773
|
+
} = {}) {
|
|
774
|
+
const link = readGlobalLink();
|
|
775
|
+
if (!link || !link.target) return { ok: true, changed: false, reason: 'not_linked' };
|
|
776
|
+
const resolvedTarget = realpathOrResolve(link.target);
|
|
777
|
+
const worktrees = worktreePaths.filter(Boolean).map((wt) => realpathOrResolve(wt));
|
|
778
|
+
// Match the raw string so a worktree already removed by ship still matches
|
|
779
|
+
// (its path is gone from disk but the dangling link still names it).
|
|
780
|
+
const insideWorktree = worktrees.some((wt) => resolvedTarget === wt || resolvedTarget.startsWith(`${wt}${path.sep}`));
|
|
781
|
+
if (!insideWorktree) return { ok: true, changed: false, reason: 'healthy', target: link.target };
|
|
782
|
+
let primaryPath = root;
|
|
783
|
+
try {
|
|
784
|
+
primaryPath = listWorktrees(root)[0]?.path || root;
|
|
785
|
+
} catch {
|
|
786
|
+
primaryPath = root;
|
|
787
|
+
}
|
|
788
|
+
const primary = realpathOrResolve(primaryPath);
|
|
789
|
+
const repoint = repointLink(primary);
|
|
790
|
+
const verify = verifyCli();
|
|
791
|
+
return {
|
|
792
|
+
ok: Boolean(verify && verify.ok),
|
|
793
|
+
changed: true,
|
|
794
|
+
reason: 'linked_into_worktree',
|
|
795
|
+
was: link.target,
|
|
796
|
+
restoredTo: primary,
|
|
797
|
+
repoint: { status: repoint ? repoint.status : null },
|
|
798
|
+
verify,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// One-line " (path, path)" suffix naming the files behind a paused landing —
|
|
803
|
+
// unmerged files for a rebase_conflict, uncommitted paths for a dirty_worktree.
|
|
804
|
+
function pausedPathsSuffix(landed) {
|
|
805
|
+
const paths = (landed && landed.conflicts) || (landed && landed.dirty) || [];
|
|
806
|
+
return paths.length ? ` (${paths.join(', ')})` : '';
|
|
807
|
+
}
|
|
808
|
+
|
|
634
809
|
function landArrival({ worktreePath, git = null }) {
|
|
635
810
|
const run = git || ((args) => spawnSync('git', args, { cwd: worktreePath, encoding: 'utf8' }));
|
|
636
811
|
const fetch = run(['fetch', 'origin']);
|
|
637
812
|
if (fetch.status !== 0) return { ok: false, stage: 'fetch', detail: String(fetch.stderr || '').trim() };
|
|
638
813
|
const rebase = run(['rebase', 'origin/master']);
|
|
639
814
|
if (rebase.status !== 0) {
|
|
640
|
-
const
|
|
815
|
+
const conflicts = String(run(['diff', '--name-only', '--diff-filter=U']).stdout || '')
|
|
816
|
+
.trim().split('\n').filter(Boolean);
|
|
641
817
|
run(['rebase', '--abort']);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
818
|
+
// git rebase refuses outright when the worktree has uncommitted changes:
|
|
819
|
+
// it stops before a single conflict can occur, so the unmerged-file list is
|
|
820
|
+
// empty. Reporting an empty list as rebase_conflict sends the operator
|
|
821
|
+
// chasing a merge problem that does not exist while intact work sits
|
|
822
|
+
// uncommitted in the worktree (CLI-1190; hit live on WEB-455 2026-07-25
|
|
823
|
+
// after a killed engine left modified/untracked files behind). An empty
|
|
824
|
+
// conflicts list is never a conflict: name the dirty paths instead.
|
|
825
|
+
if (!conflicts.length) {
|
|
826
|
+
const dirty = String(run(['status', '--porcelain']).stdout || '')
|
|
827
|
+
.split('\n')
|
|
828
|
+
.map((line) => line.slice(3).trim())
|
|
829
|
+
.filter(Boolean);
|
|
830
|
+
return { ok: false, stage: 'dirty_worktree', dirty };
|
|
831
|
+
}
|
|
832
|
+
return { ok: false, stage: 'rebase_conflict', conflicts };
|
|
647
833
|
}
|
|
648
834
|
return { ok: true, stage: 'rebased' };
|
|
649
835
|
}
|
|
@@ -651,11 +837,15 @@ function landArrival({ worktreePath, git = null }) {
|
|
|
651
837
|
module.exports = {
|
|
652
838
|
DENIED_TAGS,
|
|
653
839
|
DEAD_ENGINE_OUTPUT_PATTERNS,
|
|
840
|
+
reviewOnlyEngineEnvironment,
|
|
841
|
+
shipWithRetry,
|
|
842
|
+
shipFailureDetail,
|
|
654
843
|
get FLEET_CAPABLE() { return FLEET_CAPABLE; },
|
|
655
844
|
get runFleetFlight() { return runFleetFlight; },
|
|
656
845
|
get focusedCheck() { return focusedCheck; },
|
|
657
846
|
get dispatchCheck() { return dispatchCheck; },
|
|
658
847
|
get runDispatchFlight() { return runDispatchFlight; },
|
|
848
|
+
get prepareReviewSandbox() { return prepareReviewSandbox; },
|
|
659
849
|
YOLO_ENGINE_FLAGS,
|
|
660
850
|
DISPATCH_SELF_LAND_TARGET,
|
|
661
851
|
parseDoneCheck,
|
|
@@ -665,19 +855,22 @@ module.exports = {
|
|
|
665
855
|
buildEngineCommand,
|
|
666
856
|
isSafeLane,
|
|
667
857
|
taskTags,
|
|
668
|
-
detectDeadEngineDispatch,
|
|
669
|
-
rankFleetEngines,
|
|
670
858
|
nextInstalledFleetEngine,
|
|
859
|
+
rankFleetEnginesDetailed,
|
|
671
860
|
dispatchToEngine,
|
|
672
861
|
taskTags,
|
|
673
862
|
isHumanHoldTag,
|
|
863
|
+
isDecisionTask,
|
|
864
|
+
DECISION_REFUSE_REASON,
|
|
674
865
|
isSafeLane,
|
|
675
|
-
fileSurface,
|
|
676
|
-
surfacesOverlap,
|
|
677
866
|
staffFlight,
|
|
678
867
|
assignEngines,
|
|
679
868
|
landArrival,
|
|
869
|
+
detectDeadEngineDispatch,
|
|
870
|
+
failedDispatchLeg,
|
|
680
871
|
fleetShipArgs,
|
|
872
|
+
clipHeadTail,
|
|
873
|
+
guardGlobalCliLink,
|
|
681
874
|
defaultSelfLandCheck,
|
|
682
875
|
};
|
|
683
876
|
|
|
@@ -913,7 +1106,9 @@ function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
|
|
|
913
1106
|
STRIPE_SECRET_KEY: '',
|
|
914
1107
|
OPENAI_API_KEY: selectedSecret(['codex'], 'OPENAI_API_KEY'),
|
|
915
1108
|
CODEX_API_KEY: selectedSecret(['codex'], 'CODEX_API_KEY'),
|
|
916
|
-
|
|
1109
|
+
// owner policy: claude work runs on the subscription login, never API
|
|
1110
|
+
// billing, so the api key is stripped even when the parent env has one.
|
|
1111
|
+
ANTHROPIC_API_KEY: '',
|
|
917
1112
|
CLAUDE_CODE_OAUTH_TOKEN: selectedSecret(['claude'], 'CLAUDE_CODE_OAUTH_TOKEN'),
|
|
918
1113
|
CURSOR_API_KEY: selectedSecret(['cursor'], 'CURSOR_API_KEY'),
|
|
919
1114
|
DEVIN_API_KEY: selectedSecret(['devin'], 'DEVIN_API_KEY'),
|
|
@@ -1042,6 +1237,15 @@ function prepareReviewSandbox({ root, taskId, engine }) {
|
|
|
1042
1237
|
env: conductorGitEnvironment(),
|
|
1043
1238
|
timeout: 30000,
|
|
1044
1239
|
});
|
|
1240
|
+
// `git` walks up to the nearest enclosing repository when `root` is not itself
|
|
1241
|
+
// a git checkout. Without this guard, review-only dispatch would fetch and
|
|
1242
|
+
// trust the origin/master of whatever repo happens to sit above `root`, and
|
|
1243
|
+
// build against it. Refuse unless `root` is its own git top level.
|
|
1244
|
+
const toplevel = runConductorGit(['rev-parse', '--show-toplevel']);
|
|
1245
|
+
if (toplevel.status !== 0
|
|
1246
|
+
|| realpathOrResolve(String(toplevel.stdout || '').trim()) !== realpathOrResolve(root)) {
|
|
1247
|
+
return { ok: false, detail: 'review-only dispatch requires the workspace to be its own git repository' };
|
|
1248
|
+
}
|
|
1045
1249
|
const fetched = runConductorGit(['fetch', 'origin', 'master']);
|
|
1046
1250
|
const original = runConductorGit(['remote', 'get-url', 'origin']);
|
|
1047
1251
|
const base = run(['rev-parse', '--verify', 'origin/master^{commit}']);
|
|
@@ -1213,9 +1417,8 @@ function reviewCandidateSnapshot(boundary, expected = null) {
|
|
|
1213
1417
|
const repeatedCommit = String(secondHead.stdout || '').trim();
|
|
1214
1418
|
const treeOid = String(tree.stdout || '').trim();
|
|
1215
1419
|
if (commit !== repeatedCommit) return fail('the executor changed HEAD while its candidate was being frozen');
|
|
1216
|
-
const conductorEntry = /^\?\? \.atris\/(?:agent-worktree\.json|state\/briefs\.jsonl)$/;
|
|
1217
1420
|
const dirty = String(status.stdout || '').trim().split(/\r?\n/).filter(Boolean)
|
|
1218
|
-
.filter((line) => !
|
|
1421
|
+
.filter((line) => !isConductorStatusLine(line));
|
|
1219
1422
|
if (dirty.length) return fail(`the executor left uncommitted changes: ${dirty.slice(0, 10).join(', ')}`);
|
|
1220
1423
|
if (expected && (commit !== expected.commit || treeOid !== expected.tree)) {
|
|
1221
1424
|
return fail('the sealed candidate changed after the executor exited', { commit, tree: treeOid });
|
|
@@ -1415,8 +1618,7 @@ function importReviewCommit(boundary, { cli, taskId, engine, expectedCommit = ''
|
|
|
1415
1618
|
const status = runSandbox(['status', '--porcelain=v1', '--untracked-files=all']);
|
|
1416
1619
|
if (status.status !== 0) return fail('review_import', status.stderr || 'could not inspect the sealed sandbox');
|
|
1417
1620
|
const dirtyEntries = String(status.stdout || '').trim().split(/\r?\n/).filter(Boolean);
|
|
1418
|
-
const
|
|
1419
|
-
const uncommitted = dirtyEntries.filter((line) => !conductorEntries.test(line));
|
|
1621
|
+
const uncommitted = dirtyEntries.filter((line) => !isConductorStatusLine(line));
|
|
1420
1622
|
if (uncommitted.length) return fail('uncommitted_change', `the sealed executor must commit every requested change before Review: ${uncommitted.join(', ')}`);
|
|
1421
1623
|
const headOid = expectedCommit;
|
|
1422
1624
|
if (headOid === boundary.baseOid) return fail('no_change', 'the executor produced no committed change');
|
|
@@ -1543,9 +1745,12 @@ function inspectReviewChange(worktreePath, baseRef = 'origin/master') {
|
|
|
1543
1745
|
detail: String(status.stderr || 'could not inspect worktree changes').trim(),
|
|
1544
1746
|
};
|
|
1545
1747
|
}
|
|
1546
|
-
|
|
1748
|
+
// Conductor plumbing is not engine output. Counting it as a change is what let a
|
|
1749
|
+
// flight report "landed" for an engine that said "nothing to commit": the fleet's
|
|
1750
|
+
// own prompt file registered as dirt, has_change went true, and the no_change
|
|
1751
|
+
// pause never fired.
|
|
1547
1752
|
const dirty = String(status.stdout || '').trim().split('\n').filter(Boolean)
|
|
1548
|
-
.filter((line) => !
|
|
1753
|
+
.filter((line) => !isConductorStatusLine(line));
|
|
1549
1754
|
const hasCommittedDiff = committed.status === 1;
|
|
1550
1755
|
return {
|
|
1551
1756
|
has_change: hasCommittedDiff || dirty.length > 0,
|
|
@@ -1932,18 +2137,29 @@ async function runFleetFlight({
|
|
|
1932
2137
|
ownCli = null,
|
|
1933
2138
|
dispatcher = null,
|
|
1934
2139
|
lander = null,
|
|
2140
|
+
rebase = null,
|
|
1935
2141
|
checkoutBase = 'origin/master',
|
|
2142
|
+
guardCliLink = guardGlobalCliLink,
|
|
1936
2143
|
} = {}) {
|
|
1937
2144
|
const cli = ownCli || defaultOwnCli(root);
|
|
1938
|
-
|
|
2145
|
+
// Staff first, rank second: every task staffFlight returns already cleared
|
|
2146
|
+
// the safe-lane filter (not a decision row, no denied tags), so the rank
|
|
2147
|
+
// that pairs engines to those tasks is low stakes and the stretch zone
|
|
2148
|
+
// rule may trade the strongest engine for the cheapest learnable one.
|
|
2149
|
+
// Protected or denied work never reaches staffFlight's output, so it never
|
|
2150
|
+
// gets the flag; when nothing is staffed there is no task to rank for and
|
|
2151
|
+
// the flag stays off.
|
|
2152
|
+
const staffedTasks = staffFlight(readProjectionTasks(root), { slots });
|
|
2153
|
+
const rosterDetail = engines ? null : (() => {
|
|
1939
2154
|
const { roster: fullRoster } = require('../commands/engine');
|
|
1940
2155
|
const installed = fullRoster(root)
|
|
1941
2156
|
.filter((e) => e.installed && FLEET_CAPABLE.includes(e.name))
|
|
1942
2157
|
.map((e) => e.name);
|
|
1943
|
-
return
|
|
2158
|
+
return rankFleetEnginesDetailed(installed, root, { lowStakes: staffedTasks.length > 0 });
|
|
1944
2159
|
})();
|
|
2160
|
+
const roster = engines || rosterDetail.candidates;
|
|
1945
2161
|
|
|
1946
|
-
const staffed = assignEngines(
|
|
2162
|
+
const staffed = assignEngines(staffedTasks, roster);
|
|
1947
2163
|
// The receipt path is decided BEFORE landings so each task's ready-proof
|
|
1948
2164
|
// can cite it — the proof policy certifies receipt-backed proofs agent-side.
|
|
1949
2165
|
const receiptPath = path.join(root, 'atris', 'runs', `fleet-${nowStamp()}.json`);
|
|
@@ -1953,10 +2169,14 @@ async function runFleetFlight({
|
|
|
1953
2169
|
slots,
|
|
1954
2170
|
roster,
|
|
1955
2171
|
staffed: staffed.map((s) => ({ task: s.task.display_id, title: String(s.task.title || '').slice(0, 140), engine: s.engine, surface: s.surface })),
|
|
2172
|
+
stretch_zone_pick: rosterDetail && rosterDetail.stretch_zone_pick ? rosterDetail.stretch_zone_pick : null,
|
|
1956
2173
|
results: [],
|
|
1957
2174
|
landed: [],
|
|
1958
2175
|
paused: [],
|
|
1959
2176
|
};
|
|
2177
|
+
if (flight.stretch_zone_pick) {
|
|
2178
|
+
flight.stretch_zone_note = `low-stakes staffing picked ${flight.stretch_zone_pick} from the stretch zone: the cheapest engine with a learnable track record won this lane.`;
|
|
2179
|
+
}
|
|
1960
2180
|
|
|
1961
2181
|
log('');
|
|
1962
2182
|
log(` fleet — ${roster.length} engine${roster.length === 1 ? '' : 's'} ready, ${staffed.length} task${staffed.length === 1 ? '' : 's'} staffed`);
|
|
@@ -2021,13 +2241,14 @@ async function runFleetFlight({
|
|
|
2021
2241
|
});
|
|
2022
2242
|
|
|
2023
2243
|
// Land serially: rebase-before-ship, conflict pauses (never auto-resolve).
|
|
2244
|
+
const rebaseArrival = rebase || landArrival;
|
|
2024
2245
|
const land = lander || (({ entry }) => {
|
|
2025
|
-
const rebased =
|
|
2246
|
+
const rebased = rebaseArrival({ worktreePath: entry.worktreePath });
|
|
2026
2247
|
if (!rebased.ok) return rebased;
|
|
2027
2248
|
const check = focusedCheck(entry.task) || 'git log -1 --oneline';
|
|
2028
|
-
const shipped = cli
|
|
2249
|
+
const shipped = shipWithRetry(cli, fleetShipArgs(entry, check), entry.worktreePath);
|
|
2029
2250
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
2030
|
-
return { ok: false, stage: 'ship', detail: (shipped
|
|
2251
|
+
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped, { head: 400, tail: 300 }) };
|
|
2031
2252
|
}
|
|
2032
2253
|
return { ok: true, stage: 'shipped' };
|
|
2033
2254
|
});
|
|
@@ -2064,10 +2285,22 @@ async function runFleetFlight({
|
|
|
2064
2285
|
} else {
|
|
2065
2286
|
flight.paused.push({ task: entry.task.display_id, engine: activeEngine, ...(result.restaffed ? { restaffed: result.restaffed } : {}), ...landed });
|
|
2066
2287
|
stampDispatchBrief(root, result.brief_id, 'partial', `paused ${entry.task.display_id} at ${landed.stage || 'landing'}`);
|
|
2067
|
-
log(` ${activeEngine.padEnd(8)} ⏸ paused ${entry.task.display_id} at ${landed.stage}${
|
|
2288
|
+
log(` ${activeEngine.padEnd(8)} ⏸ paused ${entry.task.display_id} at ${landed.stage}${pausedPathsSuffix(landed)} — worktree kept`);
|
|
2068
2289
|
}
|
|
2069
2290
|
}
|
|
2070
2291
|
|
|
2292
|
+
// Teardown guard: a build worktree may have run `npm link` and hijacked the
|
|
2293
|
+
// global `atris`. Before reporting success, make sure this flight did not
|
|
2294
|
+
// leave the global CLI pointed into one of its own worktrees.
|
|
2295
|
+
const flightWorktrees = prepared.map((entry) => entry.worktreePath).filter(Boolean);
|
|
2296
|
+
const cliLink = guardCliLink({ root, worktreePaths: flightWorktrees });
|
|
2297
|
+
flight.cli_link = cliLink;
|
|
2298
|
+
if (cliLink.changed) {
|
|
2299
|
+
log(cliLink.ok
|
|
2300
|
+
? ` restored global atris link (was ${cliLink.reason}); repointed at ${path.relative(root, cliLink.restoredTo) || cliLink.restoredTo} and the cli runs`
|
|
2301
|
+
: ` alert: global atris link was ${cliLink.reason}; repoint attempted at ${cliLink.restoredTo} but the cli still fails, so every cron/loop/mission is down`);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2071
2304
|
fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
|
|
2072
2305
|
flight.receipt = receiptPath;
|
|
2073
2306
|
fs.writeFileSync(flight.receipt, `${JSON.stringify(flight, null, 2)}\n`);
|
|
@@ -2351,9 +2584,9 @@ async function runDispatchFlight({
|
|
|
2351
2584
|
change,
|
|
2352
2585
|
};
|
|
2353
2586
|
}
|
|
2354
|
-
const shipped = cli
|
|
2587
|
+
const shipped = shipWithRetry(cli, fleetShipArgs({ task: entry.task, engine: entry.engine || engine }, check), entry.worktreePath);
|
|
2355
2588
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
2356
|
-
return { ok: false, stage: 'ship', detail: (shipped
|
|
2589
|
+
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped, { head: 400, tail: 500 }) };
|
|
2357
2590
|
}
|
|
2358
2591
|
return {
|
|
2359
2592
|
ok: true,
|
|
@@ -2736,7 +2969,7 @@ async function runDispatchFlight({
|
|
|
2736
2969
|
...landed,
|
|
2737
2970
|
});
|
|
2738
2971
|
stampDispatchBrief(root, result.brief_id, 'partial', `paused ${entry.taskId} at ${landed.stage || 'landing'}`);
|
|
2739
|
-
log(` ⏸ paused ${entry.taskId} at ${landed.stage}${
|
|
2972
|
+
log(` ⏸ paused ${entry.taskId} at ${landed.stage}${pausedPathsSuffix(landed)} — worktree kept`);
|
|
2740
2973
|
}
|
|
2741
2974
|
}
|
|
2742
2975
|
|
package/lib/functional-owner.js
CHANGED
|
@@ -179,11 +179,8 @@ function resolveFunctionalOwner({ requestedOwner, title, tag, note, goal, root,
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
module.exports = {
|
|
182
|
-
ENGINE_OWNER_IDS,
|
|
183
182
|
FUNCTIONAL_MEMBER_TOPICS,
|
|
184
183
|
normalizeOwnerSlug,
|
|
185
|
-
isEngineOwner,
|
|
186
184
|
listWorkspaceMemberSlugs,
|
|
187
|
-
proposedMemberSlugFromTask,
|
|
188
185
|
resolveFunctionalOwner,
|
|
189
186
|
};
|
package/lib/git-spawn.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// One synchronous git runner for commands that shell out to git.
|
|
6
|
+
// check: true throws on a non-zero exit; check: false hands back the raw result.
|
|
7
|
+
function runGit(args, { cwd = process.cwd(), check = true, maxBuffer } = {}) {
|
|
8
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', ...(maxBuffer ? { maxBuffer } : {}) });
|
|
9
|
+
if (check && result.status !== 0) {
|
|
10
|
+
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { runGit };
|
package/lib/inspect-fields.js
CHANGED
package/lib/json-file.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Shared JSON file read/write so every call site fails the same way.
|
|
2
|
+
//
|
|
3
|
+
// Bare `JSON.parse(fs.readFileSync(...))` drifted across commands: some sites
|
|
4
|
+
// caught, some returned null, some returned {}. These two helpers keep the
|
|
5
|
+
// tolerant shape in one place.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read and parse a JSON file, returning `fallback` when it is missing or corrupt.
|
|
12
|
+
* @param {string} filePath
|
|
13
|
+
* @param {*} [fallback=null]
|
|
14
|
+
* @returns {*}
|
|
15
|
+
*/
|
|
16
|
+
function readJson(filePath, fallback = null) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Write a value as pretty JSON with a trailing newline, creating parent dirs.
|
|
26
|
+
* @param {string} filePath
|
|
27
|
+
* @param {*} value
|
|
28
|
+
* @param {{ indent?: number }} [options]
|
|
29
|
+
* @returns {string} the path written
|
|
30
|
+
*/
|
|
31
|
+
function writeJson(filePath, value, { indent = 2 } = {}) {
|
|
32
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, indent) + '\n', 'utf8');
|
|
34
|
+
return filePath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { readJson, writeJson };
|
package/lib/known-commands.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const knownCommands = ['init', 'log', 'logs', 'wish', 'drill', 'dream', 'now', 'goal', 'wtf', 'orb', 'radar', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', '_start', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
|
-
'clean', 'close', 'harvest', 'verify', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'bench', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
6
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'agents', 'probe', 'worktree', 'land', 'autoland', 'drive', 'aeo', 'slop', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail',
|
|
5
|
+
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
6
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
|
|
7
7
|
'github', 'vercel', 'supabase', 'linear', 'stripe', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'feed', 'brief'];
|
|
9
9
|
|