@kinqs/brainrouter-cli 0.3.5 → 0.3.7
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 +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/bin/cli.cjs +71 -0
- package/dist/agent/agent.d.ts +224 -3
- package/dist/agent/agent.js +561 -55
- package/dist/cli/banner.d.ts +80 -0
- package/dist/cli/banner.js +232 -0
- package/dist/cli/cliPrompt.d.ts +106 -0
- package/dist/cli/cliPrompt.js +314 -0
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/_helpers.js +6 -6
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/guard.js +75 -10
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +19 -0
- package/dist/cli/commands/mcp.js +286 -0
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/obs.js +22 -22
- package/dist/cli/commands/orchestration.js +18 -0
- package/dist/cli/commands/session.js +13 -5
- package/dist/cli/commands/ui.js +202 -91
- package/dist/cli/commands/workflow.d.ts +20 -0
- package/dist/cli/commands/workflow.js +368 -51
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +65 -0
- package/dist/cli/ink/Picker.js +133 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +571 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +73 -0
- package/dist/cli/ink/toolFormat.js +180 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +64 -646
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/spinner.d.ts +34 -0
- package/dist/cli/spinner.js +36 -0
- package/dist/cli/statusline.d.ts +67 -0
- package/dist/cli/statusline.js +204 -0
- package/dist/cli/theme.d.ts +79 -0
- package/dist/cli/theme.js +106 -0
- package/dist/cli/whereView.d.ts +81 -0
- package/dist/cli/whereView.js +245 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +52 -0
- package/dist/config/config.js +89 -75
- package/dist/index.js +215 -206
- package/dist/memory/briefing.d.ts +11 -1
- package/dist/memory/briefing.js +69 -1
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +10 -1
- package/dist/orchestration/tools.js +48 -4
- package/dist/prompt/breadthHint.d.ts +5 -0
- package/dist/prompt/breadthHint.js +44 -0
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.d.ts +34 -0
- package/dist/prompt/systemPrompt.js +128 -108
- package/dist/runtime/dangerousCommand.d.ts +53 -0
- package/dist/runtime/dangerousCommand.js +105 -0
- package/dist/runtime/mcpClient.d.ts +38 -1
- package/dist/runtime/mcpClient.js +104 -13
- package/dist/runtime/mcpPool.d.ts +162 -0
- package/dist/runtime/mcpPool.js +423 -0
- package/dist/runtime/mcpUtils.d.ts +3 -1
- package/dist/state/goalStore.d.ts +98 -17
- package/dist/state/goalStore.js +132 -42
- package/dist/state/preferencesStore.d.ts +67 -3
- package/dist/state/preferencesStore.js +84 -1
- package/dist/state/workflowArtifacts.d.ts +63 -2
- package/dist/state/workflowArtifacts.js +120 -8
- package/dist/tests/_helpers.d.ts +31 -0
- package/dist/tests/_helpers.js +91 -0
- package/package.json +12 -5
- package/.env.example +0 -109
|
@@ -7,19 +7,76 @@ import { spawn } from 'node:child_process';
|
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import { exec } from 'node:child_process';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import
|
|
10
|
+
import { spinner as makeSpinner } from '../spinner.js';
|
|
11
11
|
import { LOCAL_TOOLS } from '../../agent/agent.js';
|
|
12
12
|
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
13
13
|
import { listSessions, reconcileStale } from '../../orchestration/orchestrator.js';
|
|
14
|
-
import { ARTIFACT, artifactRelativePath, createWorkflow, getCurrentWorkflow, listWorkflows, readArtifact, updateWorkflowStatus } from '../../state/workflowArtifacts.js';
|
|
15
|
-
import { clearGoal, completeGoal, editGoal, GoalConflictError, GoalTooLongError, GOAL_TEXT_MAX_CHARS, pauseGoal, readGoal, resumeGoal, setGoal, setGoalBudget, setGoalTokenBudget } from '../../state/goalStore.js';
|
|
14
|
+
import { ARTIFACT, artifactRelativePath, createWorkflow, getCurrentWorkflow, listWorkflows, readArtifact, setCurrentWorkflow, slugify, updateWorkflowStatus, workflowExists } from '../../state/workflowArtifacts.js';
|
|
15
|
+
import { clearGoal, completeGoal, editGoal, formatBudget, GoalConflictError, GoalTooLongError, GOAL_TEXT_MAX_CHARS, pauseGoal, readGoal, resumeGoal, setGoal, setGoalBudget, setGoalTokenBudget } from '../../state/goalStore.js';
|
|
16
16
|
import { askYesNo } from '../cliPrompt.js';
|
|
17
17
|
import { formatPlan, readPlan, updatePlan } from '../../state/taskStore.js';
|
|
18
18
|
import { getLoopState, parseInterval, startLoop, stopLoop } from '../../runtime/loopRunner.js';
|
|
19
19
|
import { SLASH_TO_SKILL } from '../../prompt/skillRunner.js';
|
|
20
|
+
import { listFilesystemSkills, mergeSkillLists } from '../../prompt/skillCatalog.js';
|
|
20
21
|
import { buildGoalKickoffPrompt, runSkillByName, runSkillCommand } from './_helpers.js';
|
|
21
22
|
// Promise-flavored exec for case bodies that shell out.
|
|
22
23
|
const execPromise = promisify(exec);
|
|
24
|
+
/**
|
|
25
|
+
* Decide whether `/grill-me` should refuse to fire because the current
|
|
26
|
+
* workflow already has a written `spec.md`. The clarifying pass is meant to
|
|
27
|
+
* happen BEFORE the spec is committed — once a spec exists, asking again
|
|
28
|
+
* usually means we're re-litigating answers the user already gave, which
|
|
29
|
+
* wastes a turn. `--force` is the explicit escape hatch when the user
|
|
30
|
+
* genuinely wants a second clarifying pass (e.g., scope has drifted).
|
|
31
|
+
*
|
|
32
|
+
* Exported helper for unit tests so the guard logic can be exercised
|
|
33
|
+
* without standing up the whole REPL context. NOT pure: reads workflow
|
|
34
|
+
* state from disk (`getCurrentWorkflow`, `readArtifact`) and the latter
|
|
35
|
+
* may mkdirSync the workflow folder as a side effect.
|
|
36
|
+
*/
|
|
37
|
+
export function shouldSkipGrillMe(workspaceRoot, force, sessionKey) {
|
|
38
|
+
if (force)
|
|
39
|
+
return { skip: false };
|
|
40
|
+
// 9d-bugfix: scope the "is there an active workflow?" check to THIS
|
|
41
|
+
// session, not the workspace pointer. A fresh CLI with no session
|
|
42
|
+
// binding should not be told "plan already exists" just because a
|
|
43
|
+
// previous CLI ran `/spec` here.
|
|
44
|
+
const slug = getCurrentWorkflow(workspaceRoot, sessionKey);
|
|
45
|
+
if (!slug)
|
|
46
|
+
return { skip: false };
|
|
47
|
+
const spec = readArtifact(workspaceRoot, slug, ARTIFACT.spec);
|
|
48
|
+
if (!spec)
|
|
49
|
+
return { skip: false };
|
|
50
|
+
return {
|
|
51
|
+
skip: true,
|
|
52
|
+
slug,
|
|
53
|
+
specPath: artifactRelativePath(workspaceRoot, slug, ARTIFACT.spec),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Strip a `--force` token from a slash command's arg list, returning the
|
|
58
|
+
* flag's presence plus the rest. Used by /feature-dev / /spec / /review
|
|
59
|
+
* to gate Subtask 6's clobber prompt (mirrors /grill-me's --force parsing).
|
|
60
|
+
*/
|
|
61
|
+
function parseForceFlag(args) {
|
|
62
|
+
return { force: args.includes('--force'), rest: args.filter((a) => a !== '--force') };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Print the one-line confirmation banner after a successful `/workflow
|
|
66
|
+
* switch <slug>` (or a no-op switch onto the already-current workflow).
|
|
67
|
+
* Format: `Switched to workflow <slug> — goal: <status>, iteration N of cap`
|
|
68
|
+
* — or `goal: —` when no goal is bound.
|
|
69
|
+
*/
|
|
70
|
+
function printWorkflowSwitchConfirmation(slug, goal) {
|
|
71
|
+
if (!goal) {
|
|
72
|
+
console.log(chalk.green(`\n✓ Switched to workflow "${slug}" — goal: —.\n`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const statusLabel = goal.status.replace('_', ' ');
|
|
76
|
+
const iter = goal.budget.iterationsUsed;
|
|
77
|
+
const cap = formatBudget(goal.budget.maxIterations);
|
|
78
|
+
console.log(chalk.green(`\n✓ Switched to workflow "${slug}" — goal: ${statusLabel}, iteration ${iter} of ${cap}.\n`));
|
|
79
|
+
}
|
|
23
80
|
export async function tryHandleWorkflowCommand(ctx) {
|
|
24
81
|
const { command, args, agent, mcpClient, config, rl, repl } = ctx;
|
|
25
82
|
// 'ctx' alias to keep references to the old ReplContext name working
|
|
@@ -27,16 +84,29 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
27
84
|
switch (command) {
|
|
28
85
|
case '/skills':
|
|
29
86
|
{
|
|
30
|
-
const
|
|
87
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
88
|
+
const spinner = makeSpinner(chalk.gray('Fetching skills...')).start();
|
|
31
89
|
try {
|
|
32
90
|
const res = await callMcpTool(mcpClient, 'list_skills', { scope: 'all' });
|
|
33
91
|
spinner.stop();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
92
|
+
const mcpSkills = !res.isError ? normalizeSkillsList(res.parsed) : undefined;
|
|
93
|
+
const filesystemSkills = listFilesystemSkills(agent.workspaceRoot);
|
|
94
|
+
const skillsList = mcpSkills
|
|
95
|
+
? mergeSkillLists(mcpSkills, filesystemSkills)
|
|
96
|
+
: filesystemSkills;
|
|
97
|
+
if (skillsList) {
|
|
98
|
+
console.log(chalk.bold(`\n🧠 BrainRouter Skills (${skillsList.length}):`));
|
|
37
99
|
if (skillsList.length > 0) {
|
|
38
100
|
for (const skill of skillsList) {
|
|
39
|
-
|
|
101
|
+
const category = skill.category ? `${skill.category}/` : '';
|
|
102
|
+
const suffix = verbose && skill.description ? ` - ${skill.description}` : '';
|
|
103
|
+
console.log(` • ${chalk.cyan(`${category}${skill.name}`)} (${chalk.gray(skill.scope ?? 'unknown')})${suffix}`);
|
|
104
|
+
}
|
|
105
|
+
if (mcpSkills && skillsList.length > mcpSkills.length) {
|
|
106
|
+
console.log(chalk.gray(` Showing ${skillsList.length} skills (${mcpSkills.length} from MCP, ${skillsList.length - mcpSkills.length} filled from local files).`));
|
|
107
|
+
}
|
|
108
|
+
else if (!mcpSkills && filesystemSkills.length > 0) {
|
|
109
|
+
console.log(chalk.gray(' MCP list unavailable; showing local filesystem skills.'));
|
|
40
110
|
}
|
|
41
111
|
}
|
|
42
112
|
else {
|
|
@@ -45,6 +115,8 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
45
115
|
}
|
|
46
116
|
else {
|
|
47
117
|
console.log(chalk.red('\nFailed to parse skills list response.'));
|
|
118
|
+
if (res.text)
|
|
119
|
+
console.log(chalk.gray(` ${res.text.slice(0, 300)}`));
|
|
48
120
|
}
|
|
49
121
|
}
|
|
50
122
|
catch (err) {
|
|
@@ -56,24 +128,29 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
56
128
|
}
|
|
57
129
|
case '/tools':
|
|
58
130
|
{
|
|
59
|
-
|
|
131
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
132
|
+
console.log(chalk.bold(`\nLocal Workspace Tools (${LOCAL_TOOLS.length}):`));
|
|
60
133
|
for (const tool of LOCAL_TOOLS) {
|
|
61
|
-
|
|
134
|
+
const suffix = verbose ? ` - ${tool.description}` : '';
|
|
135
|
+
console.log(` • ${chalk.cyan(tool.name)}${suffix}`);
|
|
62
136
|
}
|
|
63
|
-
const spinner =
|
|
137
|
+
const spinner = makeSpinner(chalk.gray('Fetching MCP tools...')).start();
|
|
64
138
|
try {
|
|
65
139
|
const res = await mcpClient.listTools();
|
|
66
140
|
spinner.stop();
|
|
67
141
|
const tools = res.tools || [];
|
|
68
|
-
console.log(chalk.bold(
|
|
142
|
+
console.log(chalk.bold(`\nMCP Tools (${tools.length}):`));
|
|
69
143
|
if (tools.length === 0) {
|
|
70
144
|
console.log(chalk.yellow(' No MCP tools exposed by the active server.'));
|
|
71
145
|
}
|
|
72
146
|
else {
|
|
73
147
|
for (const tool of tools) {
|
|
74
|
-
|
|
148
|
+
const suffix = verbose ? ` - ${tool.description || 'No description'}` : '';
|
|
149
|
+
console.log(` • ${chalk.cyan(tool.name)}${suffix}`);
|
|
75
150
|
}
|
|
76
151
|
}
|
|
152
|
+
if (!verbose)
|
|
153
|
+
console.log(chalk.gray(' Use /tools --verbose to include descriptions.'));
|
|
77
154
|
}
|
|
78
155
|
catch (err) {
|
|
79
156
|
spinner.fail(chalk.red('Failed to list MCP tools.'));
|
|
@@ -84,13 +161,31 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
84
161
|
}
|
|
85
162
|
case '/plan':
|
|
86
163
|
{
|
|
164
|
+
// `/plan clear` is the explicit escape hatch when stale items from a
|
|
165
|
+
// prior workflow are blocking goal_complete (the plan-honesty guard
|
|
166
|
+
// refuses to complete with open items). `/goal <text>` also
|
|
167
|
+
// auto-clears, but this lets the user reset without setting a new
|
|
168
|
+
// goal — useful mid-session if you just abandoned a workflow.
|
|
169
|
+
if (args[0] === 'clear') {
|
|
170
|
+
const before = readPlan(agent.workspaceRoot, agent.sessionKey);
|
|
171
|
+
const pendingCount = before.items.filter((i) => i.status !== 'completed').length;
|
|
172
|
+
updatePlan(agent.workspaceRoot, { plan: [], explanation: 'cleared by /plan clear' }, agent.sessionKey);
|
|
173
|
+
console.log(chalk.green(`\n✓ Plan cleared.`));
|
|
174
|
+
if (pendingCount > 0) {
|
|
175
|
+
console.log(chalk.gray(` Removed ${pendingCount} pending item${pendingCount === 1 ? '' : 's'}.\n`));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log();
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
87
182
|
const state = readPlan(agent.workspaceRoot, agent.sessionKey);
|
|
88
183
|
console.log(chalk.bold('\nPlan:'));
|
|
89
184
|
console.log(chalk.gray(formatPlan(state)));
|
|
90
185
|
if (state.updatedAt) {
|
|
91
186
|
console.log(chalk.gray(`Updated: ${state.updatedAt}`));
|
|
92
187
|
}
|
|
93
|
-
console.log();
|
|
188
|
+
console.log(chalk.gray('\nSubcommands: /plan | /plan clear\n'));
|
|
94
189
|
return true;
|
|
95
190
|
}
|
|
96
191
|
case '/diff':
|
|
@@ -138,7 +233,7 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
138
233
|
// nothing to commit. The actual commit work goes through ctx.repl.runAgentTurn
|
|
139
234
|
// so it inherits the normal pipeline: isProcessing locking, goal
|
|
140
235
|
// continuation, /raw honoring, contradiction surfacing, token summary.
|
|
141
|
-
const spinner =
|
|
236
|
+
const spinner = makeSpinner(chalk.gray('Checking git status...')).start();
|
|
142
237
|
let statusOut = '';
|
|
143
238
|
let diffOut = '';
|
|
144
239
|
try {
|
|
@@ -165,12 +260,16 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
165
260
|
}
|
|
166
261
|
case '/feature-dev':
|
|
167
262
|
{
|
|
168
|
-
|
|
263
|
+
// `--force` accepted but ignored — workflows no longer carry goals,
|
|
264
|
+
// so there's nothing to "clobber" when starting a new one. Kept on
|
|
265
|
+
// the CLI for back-compat with any user muscle memory / scripts.
|
|
266
|
+
const parsed = parseForceFlag(args);
|
|
267
|
+
const feature = parsed.rest.join(' ').trim();
|
|
169
268
|
if (!feature) {
|
|
170
269
|
console.log(chalk.red('\nUsage: /feature-dev <feature description>\n'));
|
|
171
270
|
break;
|
|
172
271
|
}
|
|
173
|
-
const meta = createWorkflow(agent.workspaceRoot, { title: feature, kind: 'feature-dev' });
|
|
272
|
+
const meta = createWorkflow(agent.workspaceRoot, { title: feature, kind: 'feature-dev', sessionKey: agent.sessionKey });
|
|
174
273
|
const specPath = artifactRelativePath(agent.workspaceRoot, meta.slug, ARTIFACT.spec);
|
|
175
274
|
const tasksPath = artifactRelativePath(agent.workspaceRoot, meta.slug, ARTIFACT.tasks);
|
|
176
275
|
console.log(chalk.gray(`Workflow folder: ${path.dirname(specPath)}`));
|
|
@@ -209,14 +308,50 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
209
308
|
].join('\n'), ctx.repl.runAgentTurn);
|
|
210
309
|
return true;
|
|
211
310
|
}
|
|
311
|
+
case '/grill-me':
|
|
312
|
+
{
|
|
313
|
+
// `/grill-me` doesn't render its own picker — it just nudges the model
|
|
314
|
+
// (via the CLARIFY-mode overlay in systemPrompt.ts) to ask 2–5
|
|
315
|
+
// questions back instead of jumping to implementation tools. The
|
|
316
|
+
// picker UI lives in cliPrompt.ts and stays untouched.
|
|
317
|
+
const force = args.includes('--force');
|
|
318
|
+
const task = args.filter((a) => a !== '--force').join(' ').trim();
|
|
319
|
+
if (!task) {
|
|
320
|
+
console.log(chalk.red('\nUsage: /grill-me [--force] <task description>\n'));
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
const decision = shouldSkipGrillMe(agent.workspaceRoot, force, agent.sessionKey);
|
|
324
|
+
if (decision.skip) {
|
|
325
|
+
console.log(chalk.yellow(`\nPlan already exists at ${chalk.cyan(decision.specPath)}.`));
|
|
326
|
+
console.log(chalk.gray(` Drop into it with \`/workflow switch ${decision.slug}\`, or use \`/grill-me --force\` to clarify additional details.\n`));
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
// Latch activeSkill BEFORE refreshing the system prompt so the
|
|
330
|
+
// CLARIFY overlay lands in chatHistory[0]. The post-turn hook in
|
|
331
|
+
// repl.ts clears activeSkill + refreshes again, so the overlay
|
|
332
|
+
// doesn't bleed into the user's next plain prompt.
|
|
333
|
+
agent.activeSkill = 'grill-me';
|
|
334
|
+
agent.refreshSystemPrompt();
|
|
335
|
+
const prompt = [
|
|
336
|
+
'[CLARIFY — grill-me]',
|
|
337
|
+
'',
|
|
338
|
+
`The user wants help with: ${task}`,
|
|
339
|
+
'',
|
|
340
|
+
'Before doing anything, ask 2–5 short questions back to disambiguate scope, format, and unstated assumptions. Use `ask_user_choice` for mutually-exclusive options; plain prose for free-form input. End with a one-paragraph "what I\'ll do once you answer" so the user can sanity-check your read of the request.',
|
|
341
|
+
].join('\n');
|
|
342
|
+
ctx.repl.runAgentTurn(prompt);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
212
345
|
case '/spec':
|
|
213
346
|
{
|
|
214
|
-
|
|
347
|
+
// `--force` accepted but ignored — see /feature-dev for rationale.
|
|
348
|
+
const parsed = parseForceFlag(args);
|
|
349
|
+
const feature = parsed.rest.join(' ').trim();
|
|
215
350
|
if (!feature) {
|
|
216
351
|
console.log(chalk.red('\nUsage: /spec <feature title>\n'));
|
|
217
352
|
break;
|
|
218
353
|
}
|
|
219
|
-
const meta = createWorkflow(agent.workspaceRoot, { title: feature, kind: 'spec' });
|
|
354
|
+
const meta = createWorkflow(agent.workspaceRoot, { title: feature, kind: 'spec', sessionKey: agent.sessionKey });
|
|
220
355
|
const specPath = artifactRelativePath(agent.workspaceRoot, meta.slug, ARTIFACT.spec);
|
|
221
356
|
console.log(chalk.gray(`Workflow folder: ${path.dirname(specPath)}`));
|
|
222
357
|
await runSkillCommand(agent, mcpClient, '/spec', feature, [
|
|
@@ -237,8 +372,11 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
237
372
|
}
|
|
238
373
|
case '/review':
|
|
239
374
|
{
|
|
240
|
-
|
|
241
|
-
const
|
|
375
|
+
// `--force` accepted but ignored — see /feature-dev for rationale.
|
|
376
|
+
const parsed = parseForceFlag(args);
|
|
377
|
+
const scope = parsed.rest.join(' ').trim() || 'current unstaged and staged changes (git diff HEAD)';
|
|
378
|
+
const reviewTitle = `Review: ${scope}`;
|
|
379
|
+
const meta = createWorkflow(agent.workspaceRoot, { title: reviewTitle, kind: 'review', sessionKey: agent.sessionKey });
|
|
242
380
|
const reportPath = artifactRelativePath(agent.workspaceRoot, meta.slug, 'review.md');
|
|
243
381
|
console.log(chalk.gray(`Workflow folder: ${path.dirname(reportPath)}`));
|
|
244
382
|
await runSkillCommand(agent, mcpClient, command, scope, [
|
|
@@ -268,8 +406,8 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
268
406
|
}
|
|
269
407
|
// Attach this execution turn to the current workflow if there is one, so
|
|
270
408
|
// walkthrough.md accumulates per workflow rather than per CLI session.
|
|
271
|
-
const currentSlug = getCurrentWorkflow(agent.workspaceRoot);
|
|
272
|
-
const slug = currentSlug ?? createWorkflow(agent.workspaceRoot, { title: next.step, kind: 'implement-plan' }).slug;
|
|
409
|
+
const currentSlug = getCurrentWorkflow(agent.workspaceRoot, agent.sessionKey);
|
|
410
|
+
const slug = currentSlug ?? createWorkflow(agent.workspaceRoot, { title: next.step, kind: 'implement-plan', sessionKey: agent.sessionKey }).slug;
|
|
273
411
|
const walkPath = artifactRelativePath(agent.workspaceRoot, slug, ARTIFACT.walkthrough);
|
|
274
412
|
console.log(chalk.gray(`Workflow folder: ${path.dirname(walkPath)}`));
|
|
275
413
|
await runSkillCommand(agent, mcpClient, command, `Next plan item: "${next.step}"`, [
|
|
@@ -289,7 +427,7 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
289
427
|
}
|
|
290
428
|
case '/approve':
|
|
291
429
|
{
|
|
292
|
-
const slug = args[0] || getCurrentWorkflow(agent.workspaceRoot);
|
|
430
|
+
const slug = args[0] || getCurrentWorkflow(agent.workspaceRoot, agent.sessionKey);
|
|
293
431
|
if (!slug) {
|
|
294
432
|
console.log(chalk.red('\nNo current workflow. Use /spec or /feature-dev first, or /approve <slug>.\n'));
|
|
295
433
|
return true;
|
|
@@ -317,6 +455,109 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
317
455
|
`6. STOP after the first task and ask whether to continue. Do not silently work through every task — the user approves slices, not the whole batch.`);
|
|
318
456
|
return true;
|
|
319
457
|
}
|
|
458
|
+
case '/workflow':
|
|
459
|
+
{
|
|
460
|
+
// Subcommands: switch <slug> | pause | resume <slug>
|
|
461
|
+
// The plural `/workflows` (next case) is the list command. Singular
|
|
462
|
+
// `/workflow` carries actions on the current pointer / a named slug.
|
|
463
|
+
const sub = (args[0] ?? '').toLowerCase();
|
|
464
|
+
if (!sub) {
|
|
465
|
+
console.log(chalk.red('\nUsage: /workflow switch <slug> | pause | resume <slug>\n'));
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
if (sub === 'switch') {
|
|
469
|
+
const rawSlug = (args[1] ?? '').trim();
|
|
470
|
+
if (!rawSlug) {
|
|
471
|
+
console.log(chalk.red('\nUsage: /workflow switch <slug>\n'));
|
|
472
|
+
console.log(chalk.gray(' See /workflows for available slugs.\n'));
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
// Canonicalize at the entry point. Without this, a user typing
|
|
476
|
+
// `/workflow switch My Workflow` (or any title-cased / spaced
|
|
477
|
+
// variant) would have the raw string written verbatim into the
|
|
478
|
+
// pointer file by setCurrentWorkflow, breaking `w.slug ===
|
|
479
|
+
// currentSlug` matching everywhere downstream (the ★ marker
|
|
480
|
+
// on /workflows, the "already on it" no-op below, etc.).
|
|
481
|
+
const targetSlug = slugify(rawSlug);
|
|
482
|
+
if (!workflowExists(agent.workspaceRoot, targetSlug)) {
|
|
483
|
+
console.log(chalk.red(`\nNo such workflow: "${rawSlug}".`));
|
|
484
|
+
console.log(chalk.gray(' Use /workflows to see what exists, or /spec / /feature-dev to create a new one.\n'));
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
if (getCurrentWorkflow(agent.workspaceRoot, agent.sessionKey) === targetSlug) {
|
|
488
|
+
// Already on it — print the same banner as if we'd switched so
|
|
489
|
+
// the user gets a consistent confirmation instead of "no-op".
|
|
490
|
+
const g = readGoal(agent.workspaceRoot, agent.sessionKey);
|
|
491
|
+
printWorkflowSwitchConfirmation(targetSlug, g);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
// Post-decoupling (`/workflow switch` is now pure navigation):
|
|
495
|
+
// - Updates the session-scoped workflow pointer so subsequent
|
|
496
|
+
// `/spec` / `/feature-dev` / `/implement-plan` calls in THIS
|
|
497
|
+
// session land artifacts under <targetSlug>'s folder.
|
|
498
|
+
// - Does NOT touch the session's goal. Goal is session-scoped
|
|
499
|
+
// runtime state, workflows are durable storage — see
|
|
500
|
+
// goalStore.ts:resolveGoalScope for the design rationale.
|
|
501
|
+
// - Earlier migration / conflict prompt (planWorkflowSwitch +
|
|
502
|
+
// migrateSessionGoalToWorkflow + applyMigrationResolution) is
|
|
503
|
+
// gone with the workflow-goal storage that motivated it.
|
|
504
|
+
setCurrentWorkflow(agent.workspaceRoot, targetSlug, agent.sessionKey);
|
|
505
|
+
agent.refreshSystemPrompt();
|
|
506
|
+
const sessionGoal = readGoal(agent.workspaceRoot, agent.sessionKey);
|
|
507
|
+
printWorkflowSwitchConfirmation(targetSlug, sessionGoal);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
if (sub === 'pause') {
|
|
511
|
+
// `/workflow pause` is now an alias for `/goal pause` — workflows
|
|
512
|
+
// don't carry their own goal anymore, so "pause the workflow's
|
|
513
|
+
// goal" is just "pause the session's goal" which is what
|
|
514
|
+
// pauseGoal already does.
|
|
515
|
+
const g = pauseGoal(agent.workspaceRoot, agent.sessionKey);
|
|
516
|
+
if (!g) {
|
|
517
|
+
console.log(chalk.yellow('\nNo active goal to pause. Use /goal <text> to set one.\n'));
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
agent.refreshSystemPrompt();
|
|
521
|
+
const titlePreview = g.text.length > 60 ? g.text.slice(0, 60) + '…' : g.text;
|
|
522
|
+
const slug = getCurrentWorkflow(agent.workspaceRoot, agent.sessionKey);
|
|
523
|
+
const wfLabel = slug ? ` (in workflow "${slug}")` : '';
|
|
524
|
+
console.log(chalk.yellow(`\n⏸ Paused goal${wfLabel}: ${titlePreview}.`));
|
|
525
|
+
console.log(chalk.gray(' /goal resume to continue this goal later.\n'));
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
if (sub === 'resume') {
|
|
529
|
+
const rawSlug = (args[1] ?? '').trim();
|
|
530
|
+
if (!rawSlug) {
|
|
531
|
+
console.log(chalk.red('\nUsage: /workflow resume <slug>\n'));
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
// Canonicalize for the same reason as /workflow switch (see above).
|
|
535
|
+
const targetSlug = slugify(rawSlug);
|
|
536
|
+
if (!workflowExists(agent.workspaceRoot, targetSlug)) {
|
|
537
|
+
console.log(chalk.red(`\nNo such workflow: "${rawSlug}".\n`));
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
// `/workflow resume <slug>` is now sugar for "switch artifacts
|
|
541
|
+
// to <slug>'s folder, then resume the session's paused goal if
|
|
542
|
+
// there is one." Workflows no longer carry goals; resume is a
|
|
543
|
+
// goal-only operation that just happens to want a workflow set
|
|
544
|
+
// first so artifact writes land in the right place.
|
|
545
|
+
setCurrentWorkflow(agent.workspaceRoot, targetSlug, agent.sessionKey);
|
|
546
|
+
const resumed = resumeGoal(agent.workspaceRoot, agent.sessionKey);
|
|
547
|
+
agent.refreshSystemPrompt();
|
|
548
|
+
if (!resumed) {
|
|
549
|
+
console.log(chalk.green(`\n▶ Switched to workflow "${targetSlug}" — no paused session goal to resume.\n`));
|
|
550
|
+
console.log(chalk.gray(` Set a fresh /goal here when you're ready.\n`));
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
console.log(chalk.green(`\n▶ Switched to workflow "${targetSlug}" and resumed goal (${resumed.budget.iterationsUsed}/${formatBudget(resumed.budget.maxIterations)} used). Firing next iteration…\n`));
|
|
554
|
+
ctx.repl.runAgentTurn(buildGoalKickoffPrompt(resumed, 'resume'));
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
console.log(chalk.red(`\nUnknown /workflow subcommand: "${sub}".`));
|
|
558
|
+
console.log(chalk.gray(' Subcommands: switch <slug> | pause | resume <slug>\n'));
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
320
561
|
case '/workflows':
|
|
321
562
|
{
|
|
322
563
|
const workflows = listWorkflows(agent.workspaceRoot);
|
|
@@ -325,14 +566,22 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
325
566
|
console.log(chalk.yellow(' (none yet — try /spec or /feature-dev)'));
|
|
326
567
|
}
|
|
327
568
|
else {
|
|
328
|
-
const currentSlug = getCurrentWorkflow(agent.workspaceRoot);
|
|
569
|
+
const currentSlug = getCurrentWorkflow(agent.workspaceRoot, agent.sessionKey);
|
|
329
570
|
for (const w of workflows) {
|
|
330
|
-
|
|
571
|
+
// Subtask 4: current-pointer marker is now ★ (the spec's chosen
|
|
572
|
+
// glyph). Existing column structure on the first/second lines
|
|
573
|
+
// preserved so script readers don't break — the new goal column
|
|
574
|
+
// lands at the right of the artifact-markers line. The ★
|
|
575
|
+
// reflects THIS session's binding (9d-bugfix), so two CLIs in
|
|
576
|
+
// the same workspace can each see their own bound workflow.
|
|
577
|
+
const marker = w.slug === currentSlug ? chalk.green(' ★') : '';
|
|
331
578
|
console.log(` ${chalk.cyan(w.slug)} [${chalk.gray(w.status)}] ${chalk.gray(w.kind)}${marker}`);
|
|
332
579
|
console.log(` ${w.title}`);
|
|
333
580
|
const hasSpec = !!readArtifact(agent.workspaceRoot, w.slug, ARTIFACT.spec);
|
|
334
581
|
const hasTasks = !!readArtifact(agent.workspaceRoot, w.slug, ARTIFACT.tasks);
|
|
335
582
|
const hasWalk = !!readArtifact(agent.workspaceRoot, w.slug, ARTIFACT.walkthrough);
|
|
583
|
+
// Workflows are pure artifact folders now — no goal column.
|
|
584
|
+
// Goal state lives at session scope only; see goalStore.ts.
|
|
336
585
|
console.log(chalk.gray(` spec.md:${hasSpec ? '✓' : '·'} tasks.md:${hasTasks ? '✓' : '·'} walkthrough.md:${hasWalk ? '✓' : '·'}`));
|
|
337
586
|
}
|
|
338
587
|
}
|
|
@@ -358,6 +607,13 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
358
607
|
case '/goal':
|
|
359
608
|
{
|
|
360
609
|
const arg = args.join(' ').trim();
|
|
610
|
+
// Eager session resolve — without this, the FIRST /goal of a new
|
|
611
|
+
// CLI session writes goal.json under the deterministic fallback
|
|
612
|
+
// sessionKey, but every later runTurn reads from the
|
|
613
|
+
// MCP-resolved UUID key. Split-brain: kickoff banner shows the
|
|
614
|
+
// new goal, the agent reads a stale one from a different file.
|
|
615
|
+
// ensureInitialized is idempotent and tolerates missing MCP.
|
|
616
|
+
await agent.ensureInitialized();
|
|
361
617
|
const ws = agent.workspaceRoot;
|
|
362
618
|
const sk = agent.sessionKey;
|
|
363
619
|
const showStatus = (g) => {
|
|
@@ -376,7 +632,7 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
376
632
|
console.log(chalk.bold('\nGoal'));
|
|
377
633
|
console.log(` Status: ${status}`);
|
|
378
634
|
console.log(` Outcome: ${chalk.cyan(g.text)}`);
|
|
379
|
-
console.log(` Iterations: ${g.budget.iterationsUsed}/${g.budget.maxIterations} used`);
|
|
635
|
+
console.log(` Iterations: ${g.budget.iterationsUsed}/${formatBudget(g.budget.maxIterations)} used`);
|
|
380
636
|
if (g.budget.maxTokens) {
|
|
381
637
|
console.log(` Tokens: ${(g.budget.tokensUsed ?? 0).toLocaleString()}/${g.budget.maxTokens.toLocaleString()} used`);
|
|
382
638
|
}
|
|
@@ -413,12 +669,8 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
413
669
|
console.log(chalk.yellow('\nNo goal to resume.\n'));
|
|
414
670
|
return true;
|
|
415
671
|
}
|
|
416
|
-
// Resume from paused/blocked/usage_limited — any stale "wrap up"
|
|
417
|
-
// steering from the previous final-budget tick must be dropped so
|
|
418
|
-
// the resumed turn doesn't see contradictory directives.
|
|
419
|
-
agent.removeTaggedSystemMessage('goal-budget-steering');
|
|
420
672
|
agent.refreshSystemPrompt();
|
|
421
|
-
console.log(chalk.green(`\n▶ Goal resumed (${g.budget.iterationsUsed}/${g.budget.maxIterations} used). Starting next iteration…\n`));
|
|
673
|
+
console.log(chalk.green(`\n▶ Goal resumed (${g.budget.iterationsUsed}/${formatBudget(g.budget.maxIterations)} used). Starting next iteration…\n`));
|
|
422
674
|
// Fire the next iteration immediately so the user doesn't have to type
|
|
423
675
|
// a "proceed" message — the whole point of /goal is autonomy.
|
|
424
676
|
ctx.repl.runAgentTurn(buildGoalKickoffPrompt(g, 'resume'));
|
|
@@ -444,13 +696,8 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
444
696
|
if (!g)
|
|
445
697
|
console.log(chalk.yellow('\nNo goal to update.\n'));
|
|
446
698
|
else {
|
|
447
|
-
// The user just raised (or rarely lowered) the cap — any stale
|
|
448
|
-
// wrap-up steering message from the prior tight-budget state is
|
|
449
|
-
// now misleading. Drop it; the post-turn loop will re-inject if
|
|
450
|
-
// the new state still puts us on a final-budget turn.
|
|
451
|
-
agent.removeTaggedSystemMessage('goal-budget-steering');
|
|
452
699
|
agent.refreshSystemPrompt();
|
|
453
|
-
console.log(chalk.green(`\n✓ Iteration budget set to ${g.budget.maxIterations} (${g.budget.iterationsUsed} already used).\n`));
|
|
700
|
+
console.log(chalk.green(`\n✓ Iteration budget set to ${formatBudget(g.budget.maxIterations)} (${g.budget.iterationsUsed} already used).\n`));
|
|
454
701
|
}
|
|
455
702
|
return true;
|
|
456
703
|
}
|
|
@@ -466,10 +713,6 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
466
713
|
console.log(chalk.yellow('\nNo goal to update.\n'));
|
|
467
714
|
return true;
|
|
468
715
|
}
|
|
469
|
-
// Clear stale wrap-up steering — the budget state just changed
|
|
470
|
-
// and any previously-injected "this is your last turn" directive
|
|
471
|
-
// would now be misleading.
|
|
472
|
-
agent.removeTaggedSystemMessage('goal-budget-steering');
|
|
473
716
|
agent.refreshSystemPrompt();
|
|
474
717
|
if (n === 0) {
|
|
475
718
|
console.log(chalk.green('\n✓ Token budget cleared (iteration cap still applies).\n'));
|
|
@@ -529,10 +772,6 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
529
772
|
console.log(chalk.yellow('\nNo goal to edit. Set one first with /goal <text>.\n'));
|
|
530
773
|
}
|
|
531
774
|
else {
|
|
532
|
-
// Any edit may have changed the budget headroom; clear stale
|
|
533
|
-
// wrap-up steering so subsequent turns aren't told "this is
|
|
534
|
-
// your last turn" with stale data.
|
|
535
|
-
agent.removeTaggedSystemMessage('goal-budget-steering');
|
|
536
775
|
agent.refreshSystemPrompt();
|
|
537
776
|
console.log(chalk.green(`\n✓ Updated.\n`));
|
|
538
777
|
showStatus(g);
|
|
@@ -553,9 +792,26 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
553
792
|
// GoalConflictError and prompt the user before overwriting; a
|
|
554
793
|
// complete goal is replaced silently (the prior work is done, this
|
|
555
794
|
// is just starting fresh and the prompt would be noise).
|
|
795
|
+
//
|
|
796
|
+
// Inline budget parsing: users naturally write "/goal do X. Budget 3
|
|
797
|
+
// iterations." and expect that to set the cap. Without parsing, the
|
|
798
|
+
// goal store falls back to the default (10) and the user is
|
|
799
|
+
// confused by "Budget: 10" in the kickoff banner. We extract the
|
|
800
|
+
// first `budget[:\s]+N[\s iterations?]?` pattern from the text,
|
|
801
|
+
// strip it, and pass N as the maxIterations option.
|
|
802
|
+
let parsedBudget;
|
|
803
|
+
let cleanedText = arg;
|
|
804
|
+
const budgetMatch = arg.match(/\bbudget[:\s]+(\d+)(?:\s*(?:iterations?|turns?|rounds?))?\.?/i);
|
|
805
|
+
if (budgetMatch) {
|
|
806
|
+
const n = Number(budgetMatch[1]);
|
|
807
|
+
if (Number.isFinite(n) && n >= 1 && n <= 200) {
|
|
808
|
+
parsedBudget = Math.floor(n);
|
|
809
|
+
cleanedText = arg.replace(budgetMatch[0], '').replace(/\s{2,}/g, ' ').trim();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
556
812
|
let goal;
|
|
557
813
|
try {
|
|
558
|
-
goal = setGoal(ws,
|
|
814
|
+
goal = setGoal(ws, cleanedText, sk, parsedBudget !== undefined ? { maxIterations: parsedBudget } : undefined);
|
|
559
815
|
}
|
|
560
816
|
catch (err) {
|
|
561
817
|
if (err instanceof GoalTooLongError) {
|
|
@@ -567,15 +823,19 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
567
823
|
const existing = err.existing;
|
|
568
824
|
console.log(chalk.yellow(`\n⚠️ A goal is already ${existing.status.replace('_', ' ')}:`));
|
|
569
825
|
console.log(` ${chalk.cyan(existing.text)}`);
|
|
570
|
-
console.log(` ${chalk.gray(`${existing.budget.iterationsUsed}/${existing.budget.maxIterations} iterations used`)}`);
|
|
826
|
+
console.log(` ${chalk.gray(`${existing.budget.iterationsUsed}/${formatBudget(existing.budget.maxIterations)} iterations used`)}`);
|
|
571
827
|
const confirmed = await askYesNo('Replace it with the new objective? (y/N) ', false);
|
|
572
828
|
if (!confirmed) {
|
|
573
829
|
console.log(chalk.gray('\nKeeping the current goal. Use `/goal edit text <new>` to change just the wording.\n'));
|
|
574
830
|
return true;
|
|
575
831
|
}
|
|
576
|
-
// Force-replace.
|
|
832
|
+
// Force-replace. Use the cleaned text + parsed budget so the
|
|
833
|
+
// inline "Budget N" still applies on the second-try path.
|
|
577
834
|
try {
|
|
578
|
-
goal = setGoal(ws,
|
|
835
|
+
goal = setGoal(ws, cleanedText, sk, {
|
|
836
|
+
force: true,
|
|
837
|
+
...(parsedBudget !== undefined ? { maxIterations: parsedBudget } : {}),
|
|
838
|
+
});
|
|
579
839
|
}
|
|
580
840
|
catch (err2) {
|
|
581
841
|
console.log(chalk.red(`\n✗ ${err2?.message ?? err2}\n`));
|
|
@@ -588,7 +848,39 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
588
848
|
}
|
|
589
849
|
agent.refreshSystemPrompt();
|
|
590
850
|
console.log(chalk.green(`\n✓ Goal set: ${chalk.cyan(goal.text)}`));
|
|
591
|
-
|
|
851
|
+
// Reconcile stale plan items from prior workflows. The plan store is
|
|
852
|
+
// sessionKey-scoped, so a leftover `[⏳]` from an abandoned
|
|
853
|
+
// /feature-dev run blocks goal_complete for an unrelated new goal —
|
|
854
|
+
// the plan-honesty guard correctly refuses, but the user is bitten
|
|
855
|
+
// by cross-contamination they didn't sign up for. Mirror what a
|
|
856
|
+
// smart agent does when context shifts: drop the orphan items and
|
|
857
|
+
// start the new objective with a fresh slate. The cleared items
|
|
858
|
+
// are printed so it's transparent, not silent.
|
|
859
|
+
try {
|
|
860
|
+
const existingPlan = readPlan(ws, sk);
|
|
861
|
+
const orphans = existingPlan.items.filter((i) => i.status !== 'completed');
|
|
862
|
+
if (orphans.length > 0) {
|
|
863
|
+
updatePlan(ws, { plan: [], explanation: `auto-cleared on new /goal: ${goal.text.slice(0, 80)}` }, sk);
|
|
864
|
+
console.log(chalk.yellow(`⚠️ Cleared ${orphans.length} stale plan item${orphans.length === 1 ? '' : 's'} from prior work:`));
|
|
865
|
+
for (const it of orphans.slice(0, 5)) {
|
|
866
|
+
const mark = it.status === 'in_progress' ? '⏳' : '☐';
|
|
867
|
+
console.log(chalk.gray(` ${mark} ${it.step.slice(0, 100)}`));
|
|
868
|
+
}
|
|
869
|
+
if (orphans.length > 5)
|
|
870
|
+
console.log(chalk.gray(` …and ${orphans.length - 5} more.`));
|
|
871
|
+
console.log(chalk.gray(` The agent can rebuild a new plan for this goal via update_plan.`));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
// Plan reconciliation is best-effort — never fatal to the /goal flow.
|
|
876
|
+
}
|
|
877
|
+
{
|
|
878
|
+
const b = formatBudget(goal.budget.maxIterations);
|
|
879
|
+
const budgetLine = b === 'unlimited'
|
|
880
|
+
? `Budget: unlimited (cap with "/goal budget N" or include "Budget N" in the goal text). The CLI auto-continues until the agent calls goal_complete / goal_blocked or you /goal pause | clear.`
|
|
881
|
+
: `Budget: ${b} iterations. The CLI auto-continues after each turn until the agent calls goal_complete / goal_blocked or you /goal pause | clear.`;
|
|
882
|
+
console.log(chalk.gray(budgetLine));
|
|
883
|
+
}
|
|
592
884
|
console.log(chalk.gray('Kicking off iteration 1 now — type anything to cancel.\n'));
|
|
593
885
|
// Fire the first turn immediately so /goal doesn't require a "proceed"
|
|
594
886
|
// follow-up. The post-turn continuation loop in runAgentTurn handles
|
|
@@ -689,3 +981,28 @@ export async function tryHandleWorkflowCommand(ctx) {
|
|
|
689
981
|
}
|
|
690
982
|
return false;
|
|
691
983
|
}
|
|
984
|
+
export function normalizeSkillsList(payload) {
|
|
985
|
+
const list = Array.isArray(payload)
|
|
986
|
+
? payload
|
|
987
|
+
: Array.isArray(payload?.skills)
|
|
988
|
+
? payload.skills
|
|
989
|
+
: Array.isArray(payload?.items)
|
|
990
|
+
? payload.items
|
|
991
|
+
: Array.isArray(payload?.results)
|
|
992
|
+
? payload.results
|
|
993
|
+
: undefined;
|
|
994
|
+
if (!Array.isArray(list))
|
|
995
|
+
return undefined;
|
|
996
|
+
return list
|
|
997
|
+
.filter((item) => item && typeof item === 'object' && typeof item.name === 'string')
|
|
998
|
+
.map((item) => {
|
|
999
|
+
const normalized = { name: item.name };
|
|
1000
|
+
if (typeof item.scope === 'string')
|
|
1001
|
+
normalized.scope = item.scope;
|
|
1002
|
+
if (typeof item.category === 'string')
|
|
1003
|
+
normalized.category = item.category;
|
|
1004
|
+
if (typeof item.description === 'string')
|
|
1005
|
+
normalized.description = item.description;
|
|
1006
|
+
return normalized;
|
|
1007
|
+
});
|
|
1008
|
+
}
|