atris 3.52.0 → 3.55.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/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/engines/SKILL.md +8 -7
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +89 -40
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/ax +592 -91
- package/bin/atris +21 -0
- package/bin/atris.js +580 -378
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/computer.js +2 -2
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +399 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +339 -74
- package/commands/now.js +25 -6
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +96 -40
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +418 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -33
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +10 -2
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/engine-ask.js +11 -3
- package/lib/engine-registry.js +32 -7
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-ledger-compact.js +127 -0
- package/lib/mission-root.js +4 -2
- package/lib/mission-runtime-loop.js +30 -2
- package/lib/next-moves.js +34 -34
- package/lib/noninteractive.js +88 -0
- package/lib/permission-grants.js +10 -1
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/runner-command.js +5 -4
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +22 -2
- package/lib/task-proof.js +75 -9
- package/lib/task-receipt.js +5 -1
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +4 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- package/scripts/outbound-artifact-gate.js +227 -0
- package/utils/update-check.js +15 -0
package/lib/fleet.js
CHANGED
|
@@ -30,7 +30,7 @@ const {
|
|
|
30
30
|
buildOneLapValidatorPrompt,
|
|
31
31
|
parseOneLapValidatorVerdict,
|
|
32
32
|
} = require('./one-lap-validator');
|
|
33
|
-
const { listWorktrees } = require('../commands/worktree');
|
|
33
|
+
const { listWorktrees, defaultMainlineBase } = require('../commands/worktree');
|
|
34
34
|
const { isConductorStatusLine } = require('./conductor-artifacts');
|
|
35
35
|
const { matchLessons } = require('./lesson-preflight');
|
|
36
36
|
const { matchTaste } = require('./taste-lessons');
|
|
@@ -81,7 +81,7 @@ const ATRIS_BUILD_PROCESS_PREAMBLE = [
|
|
|
81
81
|
'claim the task, read the map, and work only the named contract.',
|
|
82
82
|
'run every gate bare and read its real exit code.',
|
|
83
83
|
'report honestly, including failures, and stop at the named stop point.',
|
|
84
|
-
'never use --no-verify.',
|
|
84
|
+
'if a git hook blocks your commit, stop and report it; never use --no-verify.',
|
|
85
85
|
].join('\n');
|
|
86
86
|
|
|
87
87
|
// The bounded prompt every engine gets. Same contract the manual flight used:
|
|
@@ -109,6 +109,7 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
|
|
|
109
109
|
lines.push(
|
|
110
110
|
'Rules:',
|
|
111
111
|
'- Read atris/MAP.md first to locate the code; never guess file locations.',
|
|
112
|
+
'- Keep one concern per PR; split anything larger into separate PRs because git history guides future agents and small PRs are cheap to revert and bisect.',
|
|
112
113
|
'- Run git status first. Stage ONLY files you changed. Never revert or touch files another agent modified.',
|
|
113
114
|
'- Include or update a focused regression test; run it with npm run test:fast && node --test <focused files> before committing.',
|
|
114
115
|
`- ${commitRule}`,
|
|
@@ -161,6 +162,21 @@ const YOLO_ENGINE_FLAGS = Object.freeze({
|
|
|
161
162
|
claude: '--dangerously-skip-permissions',
|
|
162
163
|
});
|
|
163
164
|
const DISPATCH_SELF_LAND_TARGET = 'origin/master';
|
|
165
|
+
|
|
166
|
+
// Dispatch used to cut every build worktree from origin/master and check
|
|
167
|
+
// self-landing against it, hard-coded. A workspace whose protected branch is
|
|
168
|
+
// `main` had no origin/master, so the worktree cut and the ancestry check both
|
|
169
|
+
// failed. Resolve the workspace's real protected branch instead (remote HEAD,
|
|
170
|
+
// then origin/main), falling back to the historical origin/master when nothing
|
|
171
|
+
// else can be determined so single-branch repos and test roots behave as before.
|
|
172
|
+
function resolveDispatchLandTarget(root, explicitBase) {
|
|
173
|
+
if (explicitBase) return explicitBase;
|
|
174
|
+
try {
|
|
175
|
+
const mainline = defaultMainlineBase(root);
|
|
176
|
+
if (mainline && mainline !== 'HEAD') return mainline;
|
|
177
|
+
} catch {}
|
|
178
|
+
return DISPATCH_SELF_LAND_TARGET;
|
|
179
|
+
}
|
|
164
180
|
const DEFAULT_DISPATCH_TIMEOUT_MS = 900000;
|
|
165
181
|
const CODEX_DISPATCH_TIMEOUT_MS = 3660000;
|
|
166
182
|
const CODEX_WATCHDOG_SOURCE = path.join(__dirname, '..', 'scripts', 'det', 'codex-watchdog.js');
|
|
@@ -213,12 +229,18 @@ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = fal
|
|
|
213
229
|
}
|
|
214
230
|
if (sealed && engineName === 'cursor') cmd = `${cmd} --sandbox enabled`;
|
|
215
231
|
if (sealed && engineName === 'devin') return cmd.replace(/^devin -p /, 'devin -p --sandbox --permission-mode accept-edits ');
|
|
232
|
+
// Headless print mode has one write gate: blocked by default, open with
|
|
233
|
+
// --yolo (--auto-accept is interactive-only and does not lift it). Fleet
|
|
234
|
+
// builds always run in an isolated worktree, so --yolo here rides the
|
|
235
|
+
// same trust model as every other engine's sealed mode.
|
|
236
|
+
if (sealed && engineName === 'commandcode') cmd = `${cmd} --yolo --trust`;
|
|
216
237
|
if (sealed && engineName === 'grok') {
|
|
217
238
|
cmd = `${cmd.replace(/\s+--always-approve\b/, '')} --sandbox enabled --permission-mode acceptEdits --no-memory --no-subagents`;
|
|
218
239
|
}
|
|
219
240
|
if (engineName === 'devin') return cmd.replace(/^devin -p /, 'devin -p --permission-mode dangerous ');
|
|
220
241
|
if (yolo && engineName === 'codex') cmd = cmd.replace(/\bexec\b/, `exec ${YOLO_ENGINE_FLAGS.codex}`);
|
|
221
242
|
if (yolo && (engineName === 'claude' || engineName === 'fable')) cmd = `${cmd} ${YOLO_ENGINE_FLAGS.claude}`;
|
|
243
|
+
if (yolo && engineName === 'commandcode') cmd = `${cmd} --yolo`;
|
|
222
244
|
if (engineName === 'codex') cmd = wrapCodexWithWatchdog(cmd, watchdogPath, watchdogReceiptPath);
|
|
223
245
|
return cmd;
|
|
224
246
|
} finally {
|
|
@@ -923,10 +945,14 @@ function assignEngines(staffed, engines) {
|
|
|
923
945
|
// conductor or a human can take it. `git` is injectable for tests.
|
|
924
946
|
// Ship failures print the cause first (MODULE_NOT_FOUND, command name) and a
|
|
925
947
|
// long node stack after. Tail-only clips hide the cause; keep a head+tail.
|
|
926
|
-
function
|
|
927
|
-
const s = String(
|
|
948
|
+
function headTail(str, head = 400, tail = 500) {
|
|
949
|
+
const s = String(str || '');
|
|
928
950
|
if (s.length <= head + tail) return s;
|
|
929
|
-
return `${s.slice(0, head)}
|
|
951
|
+
return `${s.slice(0, head)} … ${s.slice(-tail)}`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function clipHeadTail(text, { head = 400, tail = 500 } = {}) {
|
|
955
|
+
return headTail(text, head, tail);
|
|
930
956
|
}
|
|
931
957
|
|
|
932
958
|
function dispatchResultToTerminal(entry = {}, result = {}) {
|
|
@@ -964,11 +990,11 @@ function shipWithRetry(cli, args, cwd) {
|
|
|
964
990
|
return shipped;
|
|
965
991
|
}
|
|
966
992
|
|
|
967
|
-
function shipFailureDetail(shipped, { head = 400, tail =
|
|
993
|
+
function shipFailureDetail(shipped, { head = 400, tail = 500 } = {}) {
|
|
968
994
|
const combined = `${shipped.stderr || ''}\n${shipped.stdout || ''}`;
|
|
969
995
|
const moduleLine = combined.match(/Cannot find module '[^']+'[^\n]*/);
|
|
970
996
|
const cause = moduleLine ? `${moduleLine[0]}${shipped.retried_module_not_found ? ' (persisted after one retry)' : ''}\n` : '';
|
|
971
|
-
return `${cause}${
|
|
997
|
+
return `${cause}${headTail(combined.trim(), head, tail)}`;
|
|
972
998
|
}
|
|
973
999
|
|
|
974
1000
|
function fleetShipArgs(entry, check) {
|
|
@@ -1095,6 +1121,7 @@ function landArrival({ worktreePath, git = null }) {
|
|
|
1095
1121
|
module.exports = {
|
|
1096
1122
|
DENIED_TAGS,
|
|
1097
1123
|
DEAD_ENGINE_OUTPUT_PATTERNS,
|
|
1124
|
+
releasePausedClaimsWithoutWork,
|
|
1098
1125
|
reviewOnlyEngineEnvironment,
|
|
1099
1126
|
runInReapedProcessGroup,
|
|
1100
1127
|
shipWithRetry,
|
|
@@ -1108,6 +1135,7 @@ module.exports = {
|
|
|
1108
1135
|
get prepareReviewSandbox() { return prepareReviewSandbox; },
|
|
1109
1136
|
YOLO_ENGINE_FLAGS,
|
|
1110
1137
|
DISPATCH_SELF_LAND_TARGET,
|
|
1138
|
+
resolveDispatchLandTarget,
|
|
1111
1139
|
parseDoneCheck,
|
|
1112
1140
|
METHOD_KERNEL,
|
|
1113
1141
|
ATRIS_BUILD_PROCESS_PREAMBLE,
|
|
@@ -1131,6 +1159,7 @@ module.exports = {
|
|
|
1131
1159
|
dispatchResultToTerminal,
|
|
1132
1160
|
failedDispatchLeg,
|
|
1133
1161
|
fleetShipArgs,
|
|
1162
|
+
headTail,
|
|
1134
1163
|
clipHeadTail,
|
|
1135
1164
|
guardGlobalCliLink,
|
|
1136
1165
|
defaultSelfLandCheck,
|
|
@@ -1142,7 +1171,7 @@ module.exports = {
|
|
|
1142
1171
|
// Engines that can edit a repo headlessly. atris-fast (ax) is a chat lane,
|
|
1143
1172
|
// not a repo worker — it keeps owning normal mission ticks, not fleet builds.
|
|
1144
1173
|
const FLEET_CAPABLE = ['claude', 'codex', 'cursor', 'devin', 'grok'];
|
|
1145
|
-
const DISPATCH_CAPABLE = [...FLEET_CAPABLE, 'fable'];
|
|
1174
|
+
const DISPATCH_CAPABLE = [...FLEET_CAPABLE, 'fable', 'commandcode'];
|
|
1146
1175
|
|
|
1147
1176
|
let receiptSequence = 0;
|
|
1148
1177
|
function nowStamp() {
|
|
@@ -1880,7 +1909,7 @@ function bindOneLapProof(worktreePath, proofRef, sourceCommit) {
|
|
|
1880
1909
|
}
|
|
1881
1910
|
|
|
1882
1911
|
function importReviewCommit(boundary, { cli, taskId, engine, expectedCommit = '', expectedTree = '', startBaseArgs = [] } = {}) {
|
|
1883
|
-
const fail = (stage, detail, extra = {}) => ({ ok: false, stage, detail: String(detail || '').trim()
|
|
1912
|
+
const fail = (stage, detail, extra = {}) => ({ ok: false, stage, detail: headTail(String(detail || '').trim()), ...extra });
|
|
1884
1913
|
const runSandbox = (args) => spawnSync('git', args, {
|
|
1885
1914
|
cwd: boundary.worktreePath,
|
|
1886
1915
|
encoding: 'utf8',
|
|
@@ -2193,7 +2222,7 @@ async function runIndependentValidator({
|
|
|
2193
2222
|
if (exitCode !== 0) {
|
|
2194
2223
|
const outage = detectDeadEngineDispatch(dispatched);
|
|
2195
2224
|
if (outage && outage.reason === 'usage_limit' && index + 1 < candidates.length) continue;
|
|
2196
|
-
validatorResult.reason = String(dispatched.stderr || verdict.reason || `validator exited ${exitCode}`).trim()
|
|
2225
|
+
validatorResult.reason = headTail(String(dispatched.stderr || verdict.reason || `validator exited ${exitCode}`).trim());
|
|
2197
2226
|
return { ok: false, stage: 'validator_failed', validator_result: validatorResult };
|
|
2198
2227
|
}
|
|
2199
2228
|
if (verdict.verdict === 'reject') return { ok: false, stage: 'validation_rejected', validator_result: validatorResult };
|
|
@@ -2607,7 +2636,7 @@ async function runFleetFlight({
|
|
|
2607
2636
|
const check = focusedCheck(entry.task) || 'git log -1 --oneline';
|
|
2608
2637
|
const shipped = shipWithRetry(cli, fleetShipArgs(entry, check), entry.worktreePath);
|
|
2609
2638
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
2610
|
-
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped
|
|
2639
|
+
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped) };
|
|
2611
2640
|
}
|
|
2612
2641
|
return { ok: true, stage: 'shipped' };
|
|
2613
2642
|
});
|
|
@@ -2643,7 +2672,7 @@ async function runFleetFlight({
|
|
|
2643
2672
|
const restaffProof = result.restaffed ? ` Restaffed from ${result.restaffed.from} to ${result.restaffed.to} (${result.restaffed.reason}).` : '';
|
|
2644
2673
|
cli([
|
|
2645
2674
|
'task', 'ready', String(entry.task.display_id),
|
|
2646
|
-
'--proof', `Built by ${activeEngine} engine in fleet flight.${restaffProof} Landed via worktree ship gate (rebase-before-ship, verify re-run). Receipt saved at ${path.relative(root, receiptPath)}. Report tail: ${String(result.report || '').
|
|
2675
|
+
'--proof', `Built by ${activeEngine} engine in fleet flight.${restaffProof} Landed via worktree ship gate (rebase-before-ship, verify re-run). Receipt saved at ${path.relative(root, receiptPath)}. Report tail: ${headTail(String(result.report || '').replace(/\n/g, ' '))}`,
|
|
2647
2676
|
'--result', 'Operators can now review fleet-shipped work faster because the worktree was verified before landing.',
|
|
2648
2677
|
'--as', `fleet-${activeEngine}`,
|
|
2649
2678
|
]);
|
|
@@ -2667,6 +2696,14 @@ async function runFleetFlight({
|
|
|
2667
2696
|
: ` alert: global atris link was ${cliLink.reason}; repoint attempted at ${cliLink.restoredTo} but the cli still fails, so every cron/loop/mission is down`);
|
|
2668
2697
|
}
|
|
2669
2698
|
|
|
2699
|
+
releasePausedClaimsWithoutWork(cli, {
|
|
2700
|
+
paused: flight.paused,
|
|
2701
|
+
worktreeByTask: new Map(prepared.map((entry) => [String(entry.task.display_id), entry.worktreePath])),
|
|
2702
|
+
actorByTask: new Map(staffed.map((entry) => [String(entry.task.display_id), `fleet-${entry.engine}`])),
|
|
2703
|
+
baseRef: checkoutBase || 'origin/master',
|
|
2704
|
+
log,
|
|
2705
|
+
});
|
|
2706
|
+
|
|
2670
2707
|
flight.status = flight.paused.length ? 'failed' : 'completed';
|
|
2671
2708
|
flight.finished_at = new Date().toISOString();
|
|
2672
2709
|
atomicWriteFlightReceipt(flight, receiptPath);
|
|
@@ -2691,6 +2728,38 @@ function releaseUnstartedDispatchClaim(cli, { taskId, actor, detail }) {
|
|
|
2691
2728
|
return { released: releasedOk, detail: base ? `${base}. ${suffix}` : suffix };
|
|
2692
2729
|
}
|
|
2693
2730
|
|
|
2731
|
+
// A pause with no work must not keep the task. On 2026-08-21, 49 bug tasks sat
|
|
2732
|
+
// falsely "in progress" because guard-blocked engines paused their flights
|
|
2733
|
+
// while the claims taken before dispatch outlived them. After landing, any
|
|
2734
|
+
// paused task whose worktree holds no commits past base and nothing but
|
|
2735
|
+
// conductor litter gets its claim released; a paused task WITH work keeps the
|
|
2736
|
+
// claim so takeover stays attributed.
|
|
2737
|
+
function releasePausedClaimsWithoutWork(cli, { paused, worktreeByTask, actorByTask, baseRef = 'origin/master', log = () => {} }) {
|
|
2738
|
+
for (const row of paused || []) {
|
|
2739
|
+
if (row.stage === 'claim' || row.stage === 'task_lookup') continue; // claim never held
|
|
2740
|
+
if (row.claim_released !== undefined) continue;
|
|
2741
|
+
const taskId = String(row.task || '');
|
|
2742
|
+
const actor = actorByTask.get(taskId);
|
|
2743
|
+
if (!taskId || !actor) continue;
|
|
2744
|
+
const worktreePath = row.worktree || worktreeByTask.get(taskId) || '';
|
|
2745
|
+
if (worktreePath && fs.existsSync(worktreePath)) {
|
|
2746
|
+
const ahead = spawnSync('git', ['-C', worktreePath, 'rev-list', '--count', `${baseRef}..HEAD`], { encoding: 'utf8' });
|
|
2747
|
+
if (ahead.status !== 0 || parseInt(String(ahead.stdout).trim(), 10) > 0) continue;
|
|
2748
|
+
// -uall lists untracked files one by one; the default collapses a fully
|
|
2749
|
+
// untracked .atris/ into "?? .atris/", which the allowlist cannot judge.
|
|
2750
|
+
const status = spawnSync('git', ['-C', worktreePath, 'status', '--porcelain', '-uall'], { encoding: 'utf8' });
|
|
2751
|
+
if (status.status !== 0) continue;
|
|
2752
|
+
const dirt = String(status.stdout || '').split('\n').filter(Boolean).filter((line) => !isConductorStatusLine(line));
|
|
2753
|
+
if (dirt.length > 0) continue;
|
|
2754
|
+
} else if (row.stage !== 'worktree_start' && row.stage !== 'remote_quarantine' && row.stage !== 'prepare') {
|
|
2755
|
+
continue; // worktree gone and stage ambiguous: leave the claim for a human
|
|
2756
|
+
}
|
|
2757
|
+
const released = cli(['task', 'release', taskId, '--as', actor]);
|
|
2758
|
+
row.claim_released = Boolean(released && released.status === 0);
|
|
2759
|
+
if (row.claim_released) log(` released claim on ${taskId} (engine produced no work)`);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2694
2763
|
// The manual version of this loop took 6 Bash calls per task the night this
|
|
2695
2764
|
// was written: claim, worktree start, prompt file, engine -p, verify, ship.
|
|
2696
2765
|
// One or more explicit task ids build in parallel isolated worktrees on ONE
|
|
@@ -2710,7 +2779,7 @@ async function runDispatchFlight({
|
|
|
2710
2779
|
lander = null,
|
|
2711
2780
|
verifier = null,
|
|
2712
2781
|
rebase = null,
|
|
2713
|
-
checkoutBase =
|
|
2782
|
+
checkoutBase = null,
|
|
2714
2783
|
installedEngines = null,
|
|
2715
2784
|
selfLandCheck = null,
|
|
2716
2785
|
startCommitReader = null,
|
|
@@ -2746,6 +2815,9 @@ async function runDispatchFlight({
|
|
|
2746
2815
|
}
|
|
2747
2816
|
|
|
2748
2817
|
const cli = ownCli || defaultOwnCli(root);
|
|
2818
|
+
// Cut worktrees from — and check self-landing against — the workspace's real
|
|
2819
|
+
// protected branch, not a hard-coded origin/master (CLI-1298).
|
|
2820
|
+
const landTarget = resolveDispatchLandTarget(root, checkoutBase);
|
|
2749
2821
|
const verify = verifier || (trustedVerifier ? defaultTrustedVerifyRunner : defaultVerifyRunner);
|
|
2750
2822
|
const inspectChange = changeInspector || inspectReviewChange;
|
|
2751
2823
|
const readStartCommit = startCommitReader || defaultDispatchStartCommit;
|
|
@@ -2775,9 +2847,9 @@ async function runDispatchFlight({
|
|
|
2775
2847
|
log(` dispatch — ${ids.length} task${ids.length === 1 ? '' : 's'} -> ${engine}`);
|
|
2776
2848
|
log('');
|
|
2777
2849
|
|
|
2778
|
-
// Same rule as the fleet: dispatch worktrees cut from
|
|
2850
|
+
// Same rule as the fleet: dispatch worktrees cut from the protected branch by
|
|
2779
2851
|
// default so rebase-before-ship never replays a launcher feature branch.
|
|
2780
|
-
const startBaseArgs =
|
|
2852
|
+
const startBaseArgs = landTarget ? ['--base', landTarget] : [];
|
|
2781
2853
|
const prepared = [];
|
|
2782
2854
|
for (const taskId of ids) {
|
|
2783
2855
|
const task = readTaskById(cli, taskId);
|
|
@@ -2807,7 +2879,7 @@ async function runDispatchFlight({
|
|
|
2807
2879
|
actor: taskActor,
|
|
2808
2880
|
detail: String(started.stderr || '').slice(0, 200),
|
|
2809
2881
|
});
|
|
2810
|
-
flight.paused.push({ task: taskId, stage: 'worktree_start', detail: released.detail });
|
|
2882
|
+
flight.paused.push({ task: taskId, stage: 'worktree_start', detail: released.detail, claim_released: released.released });
|
|
2811
2883
|
log(` ✗ ${taskId} worktree start failed`);
|
|
2812
2884
|
continue;
|
|
2813
2885
|
}
|
|
@@ -2817,13 +2889,14 @@ async function runDispatchFlight({
|
|
|
2817
2889
|
const released = releaseUnstartedDispatchClaim(cli, {
|
|
2818
2890
|
taskId,
|
|
2819
2891
|
actor: taskActor,
|
|
2820
|
-
detail: String(remoteBoundary && remoteBoundary.detail || 'could not prepare a sealed review sandbox')
|
|
2892
|
+
detail: headTail(String(remoteBoundary && remoteBoundary.detail || 'could not prepare a sealed review sandbox')),
|
|
2821
2893
|
});
|
|
2822
2894
|
flight.paused.push({
|
|
2823
2895
|
task: taskId,
|
|
2824
2896
|
stage: 'remote_quarantine',
|
|
2825
2897
|
detail: released.detail,
|
|
2826
2898
|
worktree: null,
|
|
2899
|
+
claim_released: released.released,
|
|
2827
2900
|
});
|
|
2828
2901
|
log(` paused ${taskId} because its sealed review sandbox could not be prepared`);
|
|
2829
2902
|
continue;
|
|
@@ -2855,7 +2928,7 @@ async function runDispatchFlight({
|
|
|
2855
2928
|
actor: taskActor,
|
|
2856
2929
|
detail: String(err && err.message || err).slice(0, 200),
|
|
2857
2930
|
});
|
|
2858
|
-
flight.paused.push({ task: taskId, stage: 'prepare', detail: released.detail });
|
|
2931
|
+
flight.paused.push({ task: taskId, stage: 'prepare', detail: released.detail, claim_released: released.released });
|
|
2859
2932
|
log(` ✗ ${taskId} prepare failed`);
|
|
2860
2933
|
}
|
|
2861
2934
|
}
|
|
@@ -2942,12 +3015,12 @@ async function runDispatchFlight({
|
|
|
2942
3015
|
const candidate = reviewCandidateSnapshot(entry.remoteBoundary, entry.candidate);
|
|
2943
3016
|
if (!candidate.ok) return candidate;
|
|
2944
3017
|
}
|
|
2945
|
-
const change = reviewOnly ? inspectChange(entry.worktreePath,
|
|
3018
|
+
const change = reviewOnly ? inspectChange(entry.worktreePath, landTarget) : null;
|
|
2946
3019
|
if (reviewOnly && (!change || change.has_change !== true)) {
|
|
2947
3020
|
return {
|
|
2948
3021
|
ok: false,
|
|
2949
3022
|
stage: 'no_change',
|
|
2950
|
-
detail: String(change && change.detail || 'the engine produced no committed or worktree diff')
|
|
3023
|
+
detail: headTail(String(change && change.detail || 'the engine produced no committed or worktree diff')),
|
|
2951
3024
|
change: change || null,
|
|
2952
3025
|
};
|
|
2953
3026
|
}
|
|
@@ -2975,7 +3048,7 @@ async function runDispatchFlight({
|
|
|
2975
3048
|
return {
|
|
2976
3049
|
ok: false,
|
|
2977
3050
|
stage: 'verify_failed',
|
|
2978
|
-
detail: `${verified.stdout}${verified.stderr}
|
|
3051
|
+
detail: headTail(`${verified.stdout}${verified.stderr}`),
|
|
2979
3052
|
verifyOutput: `${verified.stdout}${verified.stderr}`,
|
|
2980
3053
|
check,
|
|
2981
3054
|
verifier_result: verifierResult,
|
|
@@ -2997,7 +3070,7 @@ async function runDispatchFlight({
|
|
|
2997
3070
|
}
|
|
2998
3071
|
const shipped = shipWithRetry(cli, fleetShipArgs({ task: entry.task, engine: entry.engine || engine }, check), entry.worktreePath);
|
|
2999
3072
|
if (shipped.status !== 0 || !/done: worktree shipped/.test(shipped.stdout)) {
|
|
3000
|
-
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped
|
|
3073
|
+
return { ok: false, stage: 'ship', detail: shipFailureDetail(shipped) };
|
|
3001
3074
|
}
|
|
3002
3075
|
return {
|
|
3003
3076
|
ok: true,
|
|
@@ -3035,7 +3108,7 @@ async function runDispatchFlight({
|
|
|
3035
3108
|
task: entry.taskId,
|
|
3036
3109
|
engine: activeEngine,
|
|
3037
3110
|
stage: candidate && candidate.stage || 'candidate_changed',
|
|
3038
|
-
detail: String(candidate && candidate.detail || 'the executor candidate could not be frozen')
|
|
3111
|
+
detail: headTail(String(candidate && candidate.detail || 'the executor candidate could not be frozen')),
|
|
3039
3112
|
worktree: entry.worktreePath,
|
|
3040
3113
|
};
|
|
3041
3114
|
flight.paused.push(paused);
|
|
@@ -3066,11 +3139,11 @@ async function runDispatchFlight({
|
|
|
3066
3139
|
entry,
|
|
3067
3140
|
result,
|
|
3068
3141
|
worktreePath: entry.worktreePath,
|
|
3069
|
-
targetRef:
|
|
3142
|
+
targetRef: landTarget,
|
|
3070
3143
|
startCommit: entry.startCommit,
|
|
3071
3144
|
});
|
|
3072
3145
|
if (selfLanded.ok) {
|
|
3073
|
-
const target = selfLanded.target ||
|
|
3146
|
+
const target = selfLanded.target || landTarget;
|
|
3074
3147
|
flight.landed.push({
|
|
3075
3148
|
task: entry.taskId,
|
|
3076
3149
|
engine: activeEngine,
|
|
@@ -3092,7 +3165,7 @@ async function runDispatchFlight({
|
|
|
3092
3165
|
task: entry.taskId,
|
|
3093
3166
|
engine,
|
|
3094
3167
|
stage,
|
|
3095
|
-
target: selfLanded.target ||
|
|
3168
|
+
target: selfLanded.target || landTarget,
|
|
3096
3169
|
detail: selfLanded.detail || '',
|
|
3097
3170
|
});
|
|
3098
3171
|
stampDispatchBrief(root, result.brief_id, 'partial', stage === 'no_work_landed'
|
|
@@ -3136,13 +3209,13 @@ async function runDispatchFlight({
|
|
|
3136
3209
|
continue;
|
|
3137
3210
|
}
|
|
3138
3211
|
finalMaster = boundaryBeforeReview.protected_master || finalMaster;
|
|
3139
|
-
const change = landed.change || inspectChange(entry.worktreePath,
|
|
3212
|
+
const change = landed.change || inspectChange(entry.worktreePath, landTarget);
|
|
3140
3213
|
if (!change || change.has_change !== true) {
|
|
3141
3214
|
flight.paused.push({
|
|
3142
3215
|
task: entry.taskId,
|
|
3143
3216
|
engine: activeEngine,
|
|
3144
3217
|
stage: 'no_change',
|
|
3145
|
-
detail: String(change && change.detail || 'the engine produced no committed or worktree diff')
|
|
3218
|
+
detail: headTail(String(change && change.detail || 'the engine produced no committed or worktree diff')),
|
|
3146
3219
|
check: landed.check,
|
|
3147
3220
|
verifier_result: landed.verifier_result,
|
|
3148
3221
|
worktree: entry.worktreePath,
|
|
@@ -3172,7 +3245,7 @@ async function runDispatchFlight({
|
|
|
3172
3245
|
task: entry.taskId,
|
|
3173
3246
|
engine: activeEngine,
|
|
3174
3247
|
stage: validation.stage,
|
|
3175
|
-
detail: String(validatorResult && validatorResult.reason || 'independent validator failed')
|
|
3248
|
+
detail: headTail(String(validatorResult && validatorResult.reason || 'independent validator failed')),
|
|
3176
3249
|
check: landed.check,
|
|
3177
3250
|
verifier_result: landed.verifier_result,
|
|
3178
3251
|
validator_result: validatorResult,
|
|
@@ -3331,14 +3404,14 @@ async function runDispatchFlight({
|
|
|
3331
3404
|
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
|
3332
3405
|
const verificationCommit = String(reviewChange.source_commit || reviewChange.commit || reviewChange.head || '').trim();
|
|
3333
3406
|
const verificationCitation = verificationCommit
|
|
3334
|
-
? ` Verification snapshot: commit ${verificationCommit}${reviewChange.dirty ? ' with a worktree diff' : ''};
|
|
3335
|
-
:
|
|
3407
|
+
? ` Verification snapshot: commit ${verificationCommit}${reviewChange.dirty ? ' with a worktree diff' : ''}; the declared verifier passed (exit 0).`
|
|
3408
|
+
: ' Verification snapshot: the worktree diff was present; the declared verifier passed (exit 0).';
|
|
3336
3409
|
const validatorCitation = validatorResult
|
|
3337
3410
|
? ` Independent validator ${validatorResult.engine} signed off: ${validatorResult.reason}.`
|
|
3338
3411
|
: '';
|
|
3339
3412
|
const readyResult = cli([
|
|
3340
3413
|
'task', 'ready', entry.taskId,
|
|
3341
|
-
'--proof', `Built by ${activeEngine} engine via one-lap dispatch.${restaffProof} Preserved in isolated worktree ${reviewWorktreePath}
|
|
3414
|
+
'--proof', `Built by ${activeEngine} engine via one-lap dispatch.${restaffProof} Preserved in isolated worktree ${reviewWorktreePath}.${verificationCitation}${validatorCitation} Verify output: ${verifyTail || '(command produced no output, exit 0)'}. Receipt saved at ${path.relative(root, receiptPath)}.`,
|
|
3342
3415
|
'--result', 'Operators can review a completed, verified change before it lands, reducing the risk of unreviewed changes reaching users.',
|
|
3343
3416
|
'--landing', 'Operators can review the verified change before it reaches users, reducing the risk of an unreviewed release.',
|
|
3344
3417
|
'--checked', `${landed.check} passed in the isolated worktree`,
|
|
@@ -3351,7 +3424,7 @@ async function runDispatchFlight({
|
|
|
3351
3424
|
task: entry.taskId,
|
|
3352
3425
|
engine: activeEngine,
|
|
3353
3426
|
stage: 'task_ready',
|
|
3354
|
-
detail: String(readyResult && (readyResult.stderr || readyResult.stdout) || 'task ready failed')
|
|
3427
|
+
detail: headTail(String(readyResult && (readyResult.stderr || readyResult.stdout) || 'task ready failed')),
|
|
3355
3428
|
check: landed.check,
|
|
3356
3429
|
verifier_result: landed.verifier_result,
|
|
3357
3430
|
worktree: reviewWorktreePath,
|
|
@@ -3375,7 +3448,7 @@ async function runDispatchFlight({
|
|
|
3375
3448
|
stampDispatchBrief(root, result.brief_id, 'pass', `landed ${entry.taskId} via engine dispatch`);
|
|
3376
3449
|
cli([
|
|
3377
3450
|
'task', 'ready', entry.taskId,
|
|
3378
|
-
'--proof', `Built by ${activeEngine} engine via atris engine dispatch.${restaffProof} Landed via worktree ship gate (rebase-before-ship, verify re-run).
|
|
3451
|
+
'--proof', `Built by ${activeEngine} engine via atris engine dispatch.${restaffProof} Landed via worktree ship gate (rebase-before-ship, verify re-run). The declared verifier passed (exit 0). Verify output: ${verifyTail || '(command produced no output, exit 0)'}. Receipt saved at ${path.relative(root, receiptPath)}.`,
|
|
3379
3452
|
'--result', 'Operators can now review engine-built work faster because dispatch reran the verifier before landing.',
|
|
3380
3453
|
'--landing', 'The verified change is on master and ready for review.',
|
|
3381
3454
|
'--checked', `${landed.check} passed before landing`,
|
|
@@ -3396,6 +3469,14 @@ async function runDispatchFlight({
|
|
|
3396
3469
|
}
|
|
3397
3470
|
}
|
|
3398
3471
|
|
|
3472
|
+
releasePausedClaimsWithoutWork(cli, {
|
|
3473
|
+
paused: flight.paused,
|
|
3474
|
+
worktreeByTask: new Map(prepared.map((entry) => [String(entry.taskId), entry.worktreePath])),
|
|
3475
|
+
actorByTask: new Map(ids.map((taskId) => [String(taskId), taskActor])),
|
|
3476
|
+
baseRef: landTarget,
|
|
3477
|
+
log,
|
|
3478
|
+
});
|
|
3479
|
+
|
|
3399
3480
|
flight.status = flight.paused.length ? 'failed' : 'completed';
|
|
3400
3481
|
flight.finished_at = new Date().toISOString();
|
|
3401
3482
|
writeDispatchReceipt(flight, receiptPath, { ids, reviewOnly, enforceRemoteBoundary });
|
package/lib/functional-owner.js
CHANGED
|
@@ -124,6 +124,16 @@ function resolveFunctionalOwner({ requestedOwner, title, tag, note, goal, root,
|
|
|
124
124
|
const tagText = tag && tag !== true ? String(tag).toLowerCase() : '';
|
|
125
125
|
const text = [highSignalText, tagText].filter(Boolean).join(' ');
|
|
126
126
|
|
|
127
|
+
// Existing workspace members win, even when the slug is also an engine name
|
|
128
|
+
// (starter team ships `executor`; dogfood expects --owner executor to stick).
|
|
129
|
+
if (requested && existingMembers.has(requested)) {
|
|
130
|
+
return {
|
|
131
|
+
owner: requested,
|
|
132
|
+
reason: 'explicit_member_owner',
|
|
133
|
+
requested_owner: requested,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
if (requested && !isEngineOwner(requested)) {
|
|
128
138
|
return {
|
|
129
139
|
owner: requested,
|
|
@@ -137,11 +147,15 @@ function resolveFunctionalOwner({ requestedOwner, title, tag, note, goal, root,
|
|
|
137
147
|
existingMembers
|
|
138
148
|
);
|
|
139
149
|
if (exact) {
|
|
150
|
+
const remapped = Boolean(requested && isEngineOwner(requested) && requested !== exact);
|
|
140
151
|
return {
|
|
141
152
|
owner: exact,
|
|
142
153
|
reason: requested ? 'engine_owner_resolved_by_exact_member' : 'inferred_exact_member',
|
|
143
154
|
requested_owner: requested || null,
|
|
144
155
|
executed_by: requested && isEngineOwner(requested) ? requested : null,
|
|
156
|
+
...(remapped ? {
|
|
157
|
+
remap_reason: `"${requested}" is an engine name with no matching member folder; using ${exact}. Override: atris mission start "<objective>" --owner <existing-member> --verify "..."`,
|
|
158
|
+
} : {}),
|
|
145
159
|
};
|
|
146
160
|
}
|
|
147
161
|
|
|
@@ -158,23 +172,31 @@ function resolveFunctionalOwner({ requestedOwner, title, tag, note, goal, root,
|
|
|
158
172
|
.sort((a, b) => b.score - a.score);
|
|
159
173
|
const inferred = firstAvailableFunctionalOwner(scored.map(row => row.owner), existingMembers);
|
|
160
174
|
if (inferred) {
|
|
175
|
+
const remapped = Boolean(requested && isEngineOwner(requested) && requested !== inferred);
|
|
161
176
|
return {
|
|
162
177
|
owner: inferred,
|
|
163
178
|
reason: requested ? 'engine_owner_resolved_by_task_signal' : 'inferred_by_task_signal',
|
|
164
179
|
requested_owner: requested || null,
|
|
165
180
|
executed_by: requested && isEngineOwner(requested) ? requested : null,
|
|
181
|
+
...(remapped ? {
|
|
182
|
+
remap_reason: `"${requested}" is an engine name with no matching member folder; using ${inferred}. Override: atris mission start "<objective>" --owner <existing-member> --verify "..."`,
|
|
183
|
+
} : {}),
|
|
166
184
|
};
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
const fallback = firstAvailableFunctionalOwner(fallbackOwners || ['architect', 'task-planner', 'mission-lead', 'validator'], existingMembers)
|
|
170
188
|
|| Array.from(existingMembers).find(owner => !isEngineOwner(owner))
|
|
171
189
|
|| 'architect';
|
|
190
|
+
const remapped = Boolean(requested && requested !== fallback);
|
|
172
191
|
return {
|
|
173
192
|
owner: fallback,
|
|
174
193
|
reason: requested ? 'engine_owner_resolved_to_factory_owner' : 'missing_functional_owner_fit',
|
|
175
194
|
requested_owner: requested || null,
|
|
176
195
|
executed_by: requested && isEngineOwner(requested) ? requested : null,
|
|
177
196
|
proposed_member: proposedMemberSlugFromTask({ title, tag }),
|
|
197
|
+
...(remapped ? {
|
|
198
|
+
remap_reason: `"${requested}" could not be kept as owner; using ${fallback}. Override: atris mission start "<objective>" --owner <existing-member> --verify "..."`,
|
|
199
|
+
} : {}),
|
|
178
200
|
};
|
|
179
201
|
}
|
|
180
202
|
|
package/lib/known-commands.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const knownCommands = ['init', 'log', 'logs', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', '
|
|
3
|
+
const knownCommands = ['init', 'doctor', 'install', 'workspace', 'log', 'logs', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', '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', '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',
|
|
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', 'x-search',
|
|
6
6
|
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', '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
|
-
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
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'];
|
|
7
|
+
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
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', 'playbook', 'feed', 'brief', 'social', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join'];
|
|
9
9
|
|
|
10
10
|
// Damerau-Levenshtein edit distance between two short strings. Counts an
|
|
11
11
|
// adjacent transposition (e.g. "taks" -> "task") as a single edit, since
|
|
@@ -39,6 +39,15 @@ function suggestCommand(input, commands = knownCommands) {
|
|
|
39
39
|
if (!input || typeof input !== 'string') return null;
|
|
40
40
|
const word = input.toLowerCase().trim();
|
|
41
41
|
if (!word) return null;
|
|
42
|
+
// Semantic near-misses that edit distance will not catch (context ≠ activate).
|
|
43
|
+
const ALIASES = {
|
|
44
|
+
context: 'activate',
|
|
45
|
+
ctx: 'activate',
|
|
46
|
+
info: 'status',
|
|
47
|
+
state: 'status',
|
|
48
|
+
whereami: 'status',
|
|
49
|
+
};
|
|
50
|
+
if (ALIASES[word] && commands.includes(ALIASES[word])) return ALIASES[word];
|
|
42
51
|
// Ignore internal/private commands (leading underscore) as suggestions.
|
|
43
52
|
const candidates = commands.filter((c) => !c.startsWith('_'));
|
|
44
53
|
let best = null;
|
|
@@ -55,4 +64,21 @@ function suggestCommand(input, commands = knownCommands) {
|
|
|
55
64
|
return best;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
function suggestCommands(input, commands = knownCommands) {
|
|
68
|
+
const primary = suggestCommand(input, commands);
|
|
69
|
+
const word = String(input || '').toLowerCase().trim();
|
|
70
|
+
const extras = [];
|
|
71
|
+
if (word === 'context' || word === 'ctx') {
|
|
72
|
+
for (const name of ['activate', 'status']) {
|
|
73
|
+
if (commands.includes(name) && name !== primary) extras.push(name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const out = [];
|
|
77
|
+
if (primary) out.push(primary);
|
|
78
|
+
for (const name of extras) {
|
|
79
|
+
if (!out.includes(name)) out.push(name);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { knownCommands, suggestCommand, suggestCommands, editDistance };
|