@kinqs/brainrouter-cli 0.3.4
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/.env.example +109 -0
- package/README.md +185 -0
- package/dist/agent/agent.d.ts +765 -0
- package/dist/agent/agent.js +1977 -0
- package/dist/cli/cliPrompt.d.ts +15 -0
- package/dist/cli/cliPrompt.js +62 -0
- package/dist/cli/commands/_context.d.ts +53 -0
- package/dist/cli/commands/_context.js +14 -0
- package/dist/cli/commands/_helpers.d.ts +45 -0
- package/dist/cli/commands/_helpers.js +140 -0
- package/dist/cli/commands/guard.d.ts +6 -0
- package/dist/cli/commands/guard.js +292 -0
- package/dist/cli/commands/memory.d.ts +12 -0
- package/dist/cli/commands/memory.js +263 -0
- package/dist/cli/commands/obs.d.ts +6 -0
- package/dist/cli/commands/obs.js +208 -0
- package/dist/cli/commands/orchestration.d.ts +6 -0
- package/dist/cli/commands/orchestration.js +218 -0
- package/dist/cli/commands/session.d.ts +6 -0
- package/dist/cli/commands/session.js +191 -0
- package/dist/cli/commands/ui.d.ts +6 -0
- package/dist/cli/commands/ui.js +477 -0
- package/dist/cli/commands/workflow.d.ts +6 -0
- package/dist/cli/commands/workflow.js +691 -0
- package/dist/cli/repl.d.ts +12 -0
- package/dist/cli/repl.js +894 -0
- package/dist/config/config.d.ts +22 -0
- package/dist/config/config.js +105 -0
- package/dist/config/workspace.d.ts +7 -0
- package/dist/config/workspace.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +610 -0
- package/dist/memory/briefing.d.ts +46 -0
- package/dist/memory/briefing.js +152 -0
- package/dist/memory/consolidation.d.ts +60 -0
- package/dist/memory/consolidation.js +208 -0
- package/dist/memory/formatters.d.ts +38 -0
- package/dist/memory/formatters.js +102 -0
- package/dist/memory/mentions.d.ts +10 -0
- package/dist/memory/mentions.js +72 -0
- package/dist/orchestration/orchestrator.d.ts +36 -0
- package/dist/orchestration/orchestrator.js +71 -0
- package/dist/orchestration/roles.d.ts +11 -0
- package/dist/orchestration/roles.js +117 -0
- package/dist/orchestration/tools.d.ts +244 -0
- package/dist/orchestration/tools.js +528 -0
- package/dist/prompt/breadthHint.d.ts +48 -0
- package/dist/prompt/breadthHint.js +93 -0
- package/dist/prompt/compactor.d.ts +31 -0
- package/dist/prompt/compactor.js +112 -0
- package/dist/prompt/initAgentMd.d.ts +13 -0
- package/dist/prompt/initAgentMd.js +194 -0
- package/dist/prompt/skillRunner.d.ts +34 -0
- package/dist/prompt/skillRunner.js +146 -0
- package/dist/prompt/systemPrompt.d.ts +10 -0
- package/dist/prompt/systemPrompt.js +171 -0
- package/dist/runtime/clipboard.d.ts +17 -0
- package/dist/runtime/clipboard.js +52 -0
- package/dist/runtime/llmSemaphore.d.ts +30 -0
- package/dist/runtime/llmSemaphore.js +67 -0
- package/dist/runtime/loopRunner.d.ts +25 -0
- package/dist/runtime/loopRunner.js +79 -0
- package/dist/runtime/mcpClient.d.ts +156 -0
- package/dist/runtime/mcpClient.js +234 -0
- package/dist/runtime/mcpUtils.d.ts +36 -0
- package/dist/runtime/mcpUtils.js +64 -0
- package/dist/runtime/sandbox.d.ts +48 -0
- package/dist/runtime/sandbox.js +156 -0
- package/dist/runtime/tracing.d.ts +25 -0
- package/dist/runtime/tracing.js +91 -0
- package/dist/state/cliState.d.ts +59 -0
- package/dist/state/cliState.js +311 -0
- package/dist/state/goalStore.d.ts +174 -0
- package/dist/state/goalStore.js +410 -0
- package/dist/state/hookifyStore.d.ts +80 -0
- package/dist/state/hookifyStore.js +237 -0
- package/dist/state/hooksStore.d.ts +42 -0
- package/dist/state/hooksStore.js +71 -0
- package/dist/state/preferencesStore.d.ts +41 -0
- package/dist/state/preferencesStore.js +25 -0
- package/dist/state/sessionStore.d.ts +42 -0
- package/dist/state/sessionStore.js +193 -0
- package/dist/state/taskStore.d.ts +23 -0
- package/dist/state/taskStore.js +80 -0
- package/dist/state/workflowArtifacts.d.ts +33 -0
- package/dist/state/workflowArtifacts.js +139 -0
- package/package.json +71 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-EXTRACTED from cli/repl.ts as part of the slash-command split.
|
|
3
|
+
* Hand-tune imports if the compiler complains.
|
|
4
|
+
*/
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { exec } from 'node:child_process';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import { LOCAL_TOOLS } from '../../agent/agent.js';
|
|
12
|
+
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
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';
|
|
16
|
+
import { askYesNo } from '../cliPrompt.js';
|
|
17
|
+
import { formatPlan, readPlan, updatePlan } from '../../state/taskStore.js';
|
|
18
|
+
import { getLoopState, parseInterval, startLoop, stopLoop } from '../../runtime/loopRunner.js';
|
|
19
|
+
import { SLASH_TO_SKILL } from '../../prompt/skillRunner.js';
|
|
20
|
+
import { buildGoalKickoffPrompt, runSkillByName, runSkillCommand } from './_helpers.js';
|
|
21
|
+
// Promise-flavored exec for case bodies that shell out.
|
|
22
|
+
const execPromise = promisify(exec);
|
|
23
|
+
export async function tryHandleWorkflowCommand(ctx) {
|
|
24
|
+
const { command, args, agent, mcpClient, config, rl, repl } = ctx;
|
|
25
|
+
// 'ctx' alias to keep references to the old ReplContext name working
|
|
26
|
+
const replCtx = repl;
|
|
27
|
+
switch (command) {
|
|
28
|
+
case '/skills':
|
|
29
|
+
{
|
|
30
|
+
const spinner = ora(chalk.gray('Fetching skills...')).start();
|
|
31
|
+
try {
|
|
32
|
+
const res = await callMcpTool(mcpClient, 'list_skills', { scope: 'all' });
|
|
33
|
+
spinner.stop();
|
|
34
|
+
if (!res.isError && Array.isArray(res.parsed)) {
|
|
35
|
+
const skillsList = res.parsed;
|
|
36
|
+
console.log(chalk.bold('\n🧠 BrainRouter Skills:'));
|
|
37
|
+
if (skillsList.length > 0) {
|
|
38
|
+
for (const skill of skillsList) {
|
|
39
|
+
console.log(` • ${chalk.cyan(skill.name)} (${chalk.gray(skill.scope)}) - ${skill.description}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log(chalk.yellow(' No skills found.'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(chalk.red('\nFailed to parse skills list response.'));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
spinner.fail(chalk.red('Failed to list skills.'));
|
|
52
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
53
|
+
}
|
|
54
|
+
console.log();
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
case '/tools':
|
|
58
|
+
{
|
|
59
|
+
console.log(chalk.bold('\nLocal Workspace Tools:'));
|
|
60
|
+
for (const tool of LOCAL_TOOLS) {
|
|
61
|
+
console.log(` ${chalk.cyan(tool.name)} - ${tool.description}`);
|
|
62
|
+
}
|
|
63
|
+
const spinner = ora(chalk.gray('Fetching MCP tools...')).start();
|
|
64
|
+
try {
|
|
65
|
+
const res = await mcpClient.listTools();
|
|
66
|
+
spinner.stop();
|
|
67
|
+
const tools = res.tools || [];
|
|
68
|
+
console.log(chalk.bold('\nMCP Tools:'));
|
|
69
|
+
if (tools.length === 0) {
|
|
70
|
+
console.log(chalk.yellow(' No MCP tools exposed by the active server.'));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (const tool of tools) {
|
|
74
|
+
console.log(` ${chalk.cyan(tool.name)} - ${tool.description || 'No description'}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
spinner.fail(chalk.red('Failed to list MCP tools.'));
|
|
80
|
+
console.warn(chalk.yellow(` Warning: ${err.message}`));
|
|
81
|
+
}
|
|
82
|
+
console.log();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
case '/plan':
|
|
86
|
+
{
|
|
87
|
+
const state = readPlan(agent.workspaceRoot, agent.sessionKey);
|
|
88
|
+
console.log(chalk.bold('\nPlan:'));
|
|
89
|
+
console.log(chalk.gray(formatPlan(state)));
|
|
90
|
+
if (state.updatedAt) {
|
|
91
|
+
console.log(chalk.gray(`Updated: ${state.updatedAt}`));
|
|
92
|
+
}
|
|
93
|
+
console.log();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
case '/diff':
|
|
97
|
+
{
|
|
98
|
+
// Stream the diff instead of buffering. The old execPromise approach
|
|
99
|
+
// read the whole diff into memory, then colored every line in a JS
|
|
100
|
+
// loop before any output appeared — for a 5k-line diff that took
|
|
101
|
+
// seconds and you saw nothing until completion. Now: spawn `git diff
|
|
102
|
+
// --color=always` and pipe stdout directly so output appears as it
|
|
103
|
+
// streams.
|
|
104
|
+
const stagedFlag = args.includes('--staged') || args.includes('--cached');
|
|
105
|
+
const allFlag = args.includes('--all') || args.includes('HEAD');
|
|
106
|
+
const gitArgs = ['diff', '--color=always'];
|
|
107
|
+
if (allFlag)
|
|
108
|
+
gitArgs.push('HEAD');
|
|
109
|
+
else if (stagedFlag)
|
|
110
|
+
gitArgs.push('--cached');
|
|
111
|
+
console.log(chalk.bold(`\n--- Git Diff (${allFlag ? 'staged + unstaged' : stagedFlag ? 'staged' : 'unstaged'}) ---`));
|
|
112
|
+
await new Promise((resolve) => {
|
|
113
|
+
const child = spawn('git', gitArgs, {
|
|
114
|
+
cwd: agent.workspaceRoot,
|
|
115
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
116
|
+
});
|
|
117
|
+
child.on('exit', (code) => {
|
|
118
|
+
if (code !== 0 && code !== null && code !== 1) {
|
|
119
|
+
// git diff returns 1 when there are differences with --exit-code;
|
|
120
|
+
// without that flag it's just success. Anything else is an error.
|
|
121
|
+
console.log(chalk.yellow(`(git diff exited ${code})`));
|
|
122
|
+
}
|
|
123
|
+
resolve();
|
|
124
|
+
});
|
|
125
|
+
child.on('error', (err) => {
|
|
126
|
+
console.log(chalk.red(`Failed to spawn git: ${err.message}`));
|
|
127
|
+
resolve();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// If the diff was empty, git printed nothing — give the user a hint.
|
|
131
|
+
// (We can't detect empty without buffering; just always print the tip.)
|
|
132
|
+
console.log(chalk.gray(' Tip: /diff --staged for staged changes only, /diff --all (or HEAD) for both.\n'));
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
case '/commit':
|
|
136
|
+
{
|
|
137
|
+
// Pre-check git status so we can skip an LLM round-trip when there's
|
|
138
|
+
// nothing to commit. The actual commit work goes through ctx.repl.runAgentTurn
|
|
139
|
+
// so it inherits the normal pipeline: isProcessing locking, goal
|
|
140
|
+
// continuation, /raw honoring, contradiction surfacing, token summary.
|
|
141
|
+
const spinner = ora(chalk.gray('Checking git status...')).start();
|
|
142
|
+
let statusOut = '';
|
|
143
|
+
let diffOut = '';
|
|
144
|
+
try {
|
|
145
|
+
({ stdout: statusOut } = await execPromise('git status --short'));
|
|
146
|
+
if (!statusOut.trim()) {
|
|
147
|
+
spinner.succeed(chalk.green('Working directory clean. Nothing to commit.'));
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
spinner.text = chalk.gray('Reading diff...');
|
|
151
|
+
({ stdout: diffOut } = await execPromise('git diff HEAD'));
|
|
152
|
+
spinner.stop();
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
spinner.fail(chalk.red(`Failed to read git status: ${err.message}`));
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
console.log(chalk.bold('\nGit changes detected:'));
|
|
159
|
+
console.log(chalk.gray(statusOut));
|
|
160
|
+
const prompt = `Based on the following git status and git diff, please create a commit. ` +
|
|
161
|
+
`Stage the modified/untracked files (using git add) and run git commit with an appropriate conventional commit message.\n\n` +
|
|
162
|
+
`Git status:\n${statusOut}\n\nDiff:\n${diffOut}`;
|
|
163
|
+
ctx.repl.runAgentTurn(prompt);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
case '/feature-dev':
|
|
167
|
+
{
|
|
168
|
+
const feature = args.join(' ').trim();
|
|
169
|
+
if (!feature) {
|
|
170
|
+
console.log(chalk.red('\nUsage: /feature-dev <feature description>\n'));
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
const meta = createWorkflow(agent.workspaceRoot, { title: feature, kind: 'feature-dev' });
|
|
174
|
+
const specPath = artifactRelativePath(agent.workspaceRoot, meta.slug, ARTIFACT.spec);
|
|
175
|
+
const tasksPath = artifactRelativePath(agent.workspaceRoot, meta.slug, ARTIFACT.tasks);
|
|
176
|
+
console.log(chalk.gray(`Workflow folder: ${path.dirname(specPath)}`));
|
|
177
|
+
try {
|
|
178
|
+
updatePlan(agent.workspaceRoot, {
|
|
179
|
+
explanation: `Feature: ${feature}`,
|
|
180
|
+
plan: [
|
|
181
|
+
{ step: 'Discovery: clarify scope and constraints', status: 'in_progress' },
|
|
182
|
+
{ step: 'Exploration: map relevant code with explorer agents', status: 'pending' },
|
|
183
|
+
{ step: 'Architecture: choose design via architect agent', status: 'pending' },
|
|
184
|
+
{ step: `Write spec.md to ${specPath}`, status: 'pending' },
|
|
185
|
+
{ step: `Write tasks.md to ${tasksPath}`, status: 'pending' },
|
|
186
|
+
{ step: 'Implementation: worker agent edits code', status: 'pending' },
|
|
187
|
+
{ step: 'Review: reviewer agent inspects diff', status: 'pending' },
|
|
188
|
+
{ step: 'Verify: verifier agent runs tests', status: 'pending' },
|
|
189
|
+
],
|
|
190
|
+
}, agent.sessionKey);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.log(chalk.yellow(`Plan setup warning: ${err.message}`));
|
|
194
|
+
}
|
|
195
|
+
await runSkillCommand(agent, mcpClient, command, feature, [
|
|
196
|
+
'## Required memory-first opening',
|
|
197
|
+
'Run `memory_search` with the feature name AND `memory_graph_query` to surface prior knowledge in this workspace. Pass any recovered record IDs to children via `spawn_agent`\'s `seedRecordIds`.',
|
|
198
|
+
'',
|
|
199
|
+
'## Workflow (mandatory, no shortcuts)',
|
|
200
|
+
`Workflow slug: \`${meta.slug}\`. Folder: \`${path.dirname(specPath)}\`.`,
|
|
201
|
+
'',
|
|
202
|
+
'Phase 1 — Exploration: call `spawn_agent` AT LEAST TWICE in parallel with role=explorer. Different children must cover different parts of the codebase relevant to this feature. Do not narrate exploration yourself; use the tool.',
|
|
203
|
+
'',
|
|
204
|
+
'Phase 2 — Architecture: after explorers complete (use `wait_agent`), call `spawn_agent` with role=architect to produce ≥2 design alternatives and a recommended slice.',
|
|
205
|
+
'',
|
|
206
|
+
`Phase 3 — Persist artifacts: call \`write_file\` to create \`${specPath}\` (the spec) AND \`${tasksPath}\` (the task breakdown). Use the spec-driven-skill structure for \`spec.md\` and the planning-skill structure for \`tasks.md\`. These files are the canonical record — do NOT produce a chat-only plan.`,
|
|
207
|
+
'',
|
|
208
|
+
'Phase 4 — STOP: present a short summary in chat referencing the file paths, then explicitly ask the user to confirm before any `worker` implementation begins.',
|
|
209
|
+
].join('\n'), ctx.repl.runAgentTurn);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
case '/spec':
|
|
213
|
+
{
|
|
214
|
+
const feature = args.join(' ').trim();
|
|
215
|
+
if (!feature) {
|
|
216
|
+
console.log(chalk.red('\nUsage: /spec <feature title>\n'));
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
const meta = createWorkflow(agent.workspaceRoot, { title: feature, kind: 'spec' });
|
|
220
|
+
const specPath = artifactRelativePath(agent.workspaceRoot, meta.slug, ARTIFACT.spec);
|
|
221
|
+
console.log(chalk.gray(`Workflow folder: ${path.dirname(specPath)}`));
|
|
222
|
+
await runSkillCommand(agent, mcpClient, '/spec', feature, [
|
|
223
|
+
'## Goal',
|
|
224
|
+
`Produce a complete specification for: "${feature}".`,
|
|
225
|
+
'',
|
|
226
|
+
'## Mandatory steps',
|
|
227
|
+
'1. Open with `memory_search` for related prior work; cite any recovered record IDs in the spec.',
|
|
228
|
+
'2. Optionally spawn 1–2 `explorer` children to confirm scope before drafting (only if the feature touches unfamiliar code).',
|
|
229
|
+
`3. Call \`write_file\` with path \`${specPath}\` containing the full spec, structured per the spec-driven-skill template (Objective, Commands, Project Structure, Code Style, Testing Strategy, Boundaries).`,
|
|
230
|
+
'4. In chat, summarize the spec in ≤ 10 lines and reference the file path. Ask the user to approve before generating tasks or implementation.',
|
|
231
|
+
'',
|
|
232
|
+
'## Anti-patterns',
|
|
233
|
+
'- Do NOT produce a multi-section spec inline in chat without writing the file.',
|
|
234
|
+
'- Do NOT proceed to task breakdown or implementation until the user explicitly approves.',
|
|
235
|
+
].join('\n'), ctx.repl.runAgentTurn);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
case '/review':
|
|
239
|
+
{
|
|
240
|
+
const scope = args.join(' ').trim() || 'current unstaged and staged changes (git diff HEAD)';
|
|
241
|
+
const meta = createWorkflow(agent.workspaceRoot, { title: `Review: ${scope}`, kind: 'review' });
|
|
242
|
+
const reportPath = artifactRelativePath(agent.workspaceRoot, meta.slug, 'review.md');
|
|
243
|
+
console.log(chalk.gray(`Workflow folder: ${path.dirname(reportPath)}`));
|
|
244
|
+
await runSkillCommand(agent, mcpClient, command, scope, [
|
|
245
|
+
'## Required memory-first opening',
|
|
246
|
+
'Run `memory_search` for similar past reviews and `memory_file_history` for any files touched by this diff. Pass relevant record IDs through `seedRecordIds`.',
|
|
247
|
+
'',
|
|
248
|
+
'## Workflow (mandatory)',
|
|
249
|
+
`Workflow slug: \`${meta.slug}\`. Output file: \`${reportPath}\`.`,
|
|
250
|
+
'',
|
|
251
|
+
'Step 1: call `spawn_agent` THREE times in parallel with role=reviewer and access=read. Focuses:',
|
|
252
|
+
'(a) correctness / bugs / security;',
|
|
253
|
+
'(b) maintainability / readability / design;',
|
|
254
|
+
'(c) conventions / tests / documentation.',
|
|
255
|
+
'Step 2: `wait_agent` on all three.',
|
|
256
|
+
`Step 3: \`write_file\` to \`${reportPath}\` containing a severity-ordered synthesis (blocker / major / minor / nit) with file:line citations.`,
|
|
257
|
+
'Step 4: summarize ≤ 15 lines in chat referencing the file. Do NOT edit reviewed files.',
|
|
258
|
+
].join('\n'), ctx.repl.runAgentTurn);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
case '/implement-plan':
|
|
262
|
+
{
|
|
263
|
+
const plan = readPlan(agent.workspaceRoot, agent.sessionKey);
|
|
264
|
+
const next = plan.items.find(i => i.status === 'pending' || i.status === 'in_progress');
|
|
265
|
+
if (!next) {
|
|
266
|
+
console.log(chalk.yellow('\nNo pending plan items.\n'));
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
// Attach this execution turn to the current workflow if there is one, so
|
|
270
|
+
// 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;
|
|
273
|
+
const walkPath = artifactRelativePath(agent.workspaceRoot, slug, ARTIFACT.walkthrough);
|
|
274
|
+
console.log(chalk.gray(`Workflow folder: ${path.dirname(walkPath)}`));
|
|
275
|
+
await runSkillCommand(agent, mcpClient, command, `Next plan item: "${next.step}"`, [
|
|
276
|
+
'## Required memory-first opening',
|
|
277
|
+
'Run `memory_search` and `memory_task_state` scoped to this plan item. Seed the `worker` child with the record IDs.',
|
|
278
|
+
'',
|
|
279
|
+
'## Workflow (mandatory)',
|
|
280
|
+
`Workflow slug: \`${slug}\`. Walkthrough file: \`${walkPath}\`.`,
|
|
281
|
+
'',
|
|
282
|
+
'Step 1: `update_plan` to mark this item `in_progress`.',
|
|
283
|
+
'Step 2: `spawn_agent` role=worker access=write with concrete acceptance criteria AND `seedRecordIds`.',
|
|
284
|
+
'Step 3: after the worker completes, `spawn_agent` role=verifier access=shell to run tests/typechecks.',
|
|
285
|
+
`Step 4: append a section to \`${walkPath}\` (use \`read_file\` then \`write_file\`) recording: item name, files changed, verification commands run, PASS/FAIL, follow-ups.`,
|
|
286
|
+
'Step 5: only on PASS, `update_plan` to `completed` AND `memory_task_update` with outcome. On FAIL, keep `in_progress`, surface failing output, `memory_task_update` with blocker.',
|
|
287
|
+
].join('\n'), ctx.repl.runAgentTurn);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
case '/approve':
|
|
291
|
+
{
|
|
292
|
+
const slug = args[0] || getCurrentWorkflow(agent.workspaceRoot);
|
|
293
|
+
if (!slug) {
|
|
294
|
+
console.log(chalk.red('\nNo current workflow. Use /spec or /feature-dev first, or /approve <slug>.\n'));
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
const spec = readArtifact(agent.workspaceRoot, slug, ARTIFACT.spec);
|
|
298
|
+
if (!spec) {
|
|
299
|
+
console.log(chalk.red(`\nWorkflow "${slug}" has no spec.md yet. Run /spec or /feature-dev first.\n`));
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
const next = updateWorkflowStatus(agent.workspaceRoot, slug, 'in-progress');
|
|
303
|
+
if (!next) {
|
|
304
|
+
console.log(chalk.red(`\nWorkflow "${slug}" not found.\n`));
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
console.log(chalk.green(`\n✓ Approved workflow "${slug}". Status: in-progress.`));
|
|
308
|
+
console.log(chalk.gray('Kicking off implementation phase…\n'));
|
|
309
|
+
const tasksPath = artifactRelativePath(agent.workspaceRoot, slug, ARTIFACT.tasks);
|
|
310
|
+
const walkPath = artifactRelativePath(agent.workspaceRoot, slug, ARTIFACT.walkthrough);
|
|
311
|
+
ctx.repl.runAgentTurn(`The user just approved workflow \`${slug}\`. Begin implementation now.\n\n` +
|
|
312
|
+
`1. If \`${tasksPath}\` does not exist yet, read \`${artifactRelativePath(agent.workspaceRoot, slug, ARTIFACT.spec)}\` and \`write_file\` a complete tasks.md (vertical slices, S/M-sized, with acceptance criteria) before doing anything else.\n` +
|
|
313
|
+
`2. Pick the first pending task from tasks.md and call \`update_plan\` to mark it in_progress.\n` +
|
|
314
|
+
`3. \`spawn_agent\` role=worker access=write to implement it. Pass any relevant recalled record IDs via seedRecordIds.\n` +
|
|
315
|
+
`4. After the worker completes, \`spawn_agent\` role=verifier access=shell to run tests/typechecks.\n` +
|
|
316
|
+
`5. Append a section to \`${walkPath}\` (read+write) recording the outcome.\n` +
|
|
317
|
+
`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
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
case '/workflows':
|
|
321
|
+
{
|
|
322
|
+
const workflows = listWorkflows(agent.workspaceRoot);
|
|
323
|
+
console.log(chalk.bold('\nDurable Workflows'));
|
|
324
|
+
if (workflows.length === 0) {
|
|
325
|
+
console.log(chalk.yellow(' (none yet — try /spec or /feature-dev)'));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
const currentSlug = getCurrentWorkflow(agent.workspaceRoot);
|
|
329
|
+
for (const w of workflows) {
|
|
330
|
+
const marker = w.slug === currentSlug ? chalk.green(' ← current') : '';
|
|
331
|
+
console.log(` ${chalk.cyan(w.slug)} [${chalk.gray(w.status)}] ${chalk.gray(w.kind)}${marker}`);
|
|
332
|
+
console.log(` ${w.title}`);
|
|
333
|
+
const hasSpec = !!readArtifact(agent.workspaceRoot, w.slug, ARTIFACT.spec);
|
|
334
|
+
const hasTasks = !!readArtifact(agent.workspaceRoot, w.slug, ARTIFACT.tasks);
|
|
335
|
+
const hasWalk = !!readArtifact(agent.workspaceRoot, w.slug, ARTIFACT.walkthrough);
|
|
336
|
+
console.log(chalk.gray(` spec.md:${hasSpec ? '✓' : '·'} tasks.md:${hasTasks ? '✓' : '·'} walkthrough.md:${hasWalk ? '✓' : '·'}`));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
console.log();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
case '/skill':
|
|
343
|
+
{
|
|
344
|
+
const skillName = args[0];
|
|
345
|
+
const userInput = args.slice(1).join(' ').trim();
|
|
346
|
+
if (!skillName) {
|
|
347
|
+
console.log(chalk.red('\nUsage: /skill <skill-name> [input]\n'));
|
|
348
|
+
console.log(chalk.gray('Mapped slash commands:'));
|
|
349
|
+
for (const [slash, name] of Object.entries(SLASH_TO_SKILL)) {
|
|
350
|
+
console.log(` ${chalk.cyan(slash.padEnd(18))} → ${chalk.green(name)}`);
|
|
351
|
+
}
|
|
352
|
+
console.log();
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
await runSkillByName(agent, mcpClient, skillName, userInput, undefined, ctx.repl.runAgentTurn);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
case '/goal':
|
|
359
|
+
{
|
|
360
|
+
const arg = args.join(' ').trim();
|
|
361
|
+
const ws = agent.workspaceRoot;
|
|
362
|
+
const sk = agent.sessionKey;
|
|
363
|
+
const showStatus = (g) => {
|
|
364
|
+
if (!g) {
|
|
365
|
+
console.log(chalk.yellow('\nNo active goal. Set one with: /goal <outcome statement>\n'));
|
|
366
|
+
console.log(chalk.gray('Outcome-first format works best:'));
|
|
367
|
+
console.log(chalk.gray(' /goal <desired end state> verified by <evidence> while preserving <constraints>.\n'));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const statusLabel = g.status.replace('_', ' ');
|
|
371
|
+
const status = g.status === 'active' ? chalk.green(statusLabel)
|
|
372
|
+
: g.status === 'paused' ? chalk.yellow(statusLabel)
|
|
373
|
+
: g.status === 'complete' ? chalk.cyan(statusLabel)
|
|
374
|
+
: g.status === 'usage_limited' ? chalk.yellow(statusLabel)
|
|
375
|
+
: chalk.red(statusLabel);
|
|
376
|
+
console.log(chalk.bold('\nGoal'));
|
|
377
|
+
console.log(` Status: ${status}`);
|
|
378
|
+
console.log(` Outcome: ${chalk.cyan(g.text)}`);
|
|
379
|
+
console.log(` Iterations: ${g.budget.iterationsUsed}/${g.budget.maxIterations} used`);
|
|
380
|
+
if (g.budget.maxTokens) {
|
|
381
|
+
console.log(` Tokens: ${(g.budget.tokensUsed ?? 0).toLocaleString()}/${g.budget.maxTokens.toLocaleString()} used`);
|
|
382
|
+
}
|
|
383
|
+
console.log(` Started: ${chalk.gray(g.startedAt)}`);
|
|
384
|
+
if (g.completedAt)
|
|
385
|
+
console.log(` Completed: ${chalk.gray(g.completedAt)}`);
|
|
386
|
+
if (g.blockedReason)
|
|
387
|
+
console.log(` Reason: ${chalk.gray(g.blockedReason)}`);
|
|
388
|
+
console.log(chalk.gray('\nSubcommands: /goal <text> | pause | resume | complete | clear | budget <n> | tokens <n> | edit <field> <value>\n'));
|
|
389
|
+
};
|
|
390
|
+
if (!arg || arg === 'show') {
|
|
391
|
+
showStatus(readGoal(ws, sk));
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
if (arg === 'clear') {
|
|
395
|
+
clearGoal(ws, sk);
|
|
396
|
+
agent.refreshSystemPrompt();
|
|
397
|
+
console.log(chalk.green('\n✓ Goal cleared.\n'));
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
if (arg === 'pause') {
|
|
401
|
+
const g = pauseGoal(ws, sk);
|
|
402
|
+
if (!g)
|
|
403
|
+
console.log(chalk.yellow('\nNo active goal to pause.\n'));
|
|
404
|
+
else {
|
|
405
|
+
agent.refreshSystemPrompt();
|
|
406
|
+
console.log(chalk.yellow(`\n⏸ Goal paused. No auto-continuation until /goal resume.\n`));
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
if (arg === 'resume') {
|
|
411
|
+
const g = resumeGoal(ws, sk);
|
|
412
|
+
if (!g) {
|
|
413
|
+
console.log(chalk.yellow('\nNo goal to resume.\n'));
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
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
|
+
agent.refreshSystemPrompt();
|
|
421
|
+
console.log(chalk.green(`\n▶ Goal resumed (${g.budget.iterationsUsed}/${g.budget.maxIterations} used). Starting next iteration…\n`));
|
|
422
|
+
// Fire the next iteration immediately so the user doesn't have to type
|
|
423
|
+
// a "proceed" message — the whole point of /goal is autonomy.
|
|
424
|
+
ctx.repl.runAgentTurn(buildGoalKickoffPrompt(g, 'resume'));
|
|
425
|
+
return true; // runAgentTurn owns its prompt cycle
|
|
426
|
+
}
|
|
427
|
+
if (arg === 'complete') {
|
|
428
|
+
const g = completeGoal(ws, sk, 'Marked complete manually by user.');
|
|
429
|
+
if (!g)
|
|
430
|
+
console.log(chalk.yellow('\nNo goal to mark complete.\n'));
|
|
431
|
+
else {
|
|
432
|
+
agent.refreshSystemPrompt();
|
|
433
|
+
console.log(chalk.green(`\n🎯 Goal marked complete.\n`));
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
if (arg.startsWith('budget')) {
|
|
438
|
+
const n = Number(arg.replace(/^budget\s*/, '').trim());
|
|
439
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
440
|
+
console.log(chalk.red('\nUsage: /goal budget <positive integer>\n'));
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
const g = setGoalBudget(ws, sk, Math.floor(n));
|
|
444
|
+
if (!g)
|
|
445
|
+
console.log(chalk.yellow('\nNo goal to update.\n'));
|
|
446
|
+
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
|
+
agent.refreshSystemPrompt();
|
|
453
|
+
console.log(chalk.green(`\n✓ Iteration budget set to ${g.budget.maxIterations} (${g.budget.iterationsUsed} already used).\n`));
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
if (arg.startsWith('tokens')) {
|
|
458
|
+
// /goal tokens <N> — set the token cap (0 to clear)
|
|
459
|
+
const n = Number(arg.replace(/^tokens\s*/, '').trim());
|
|
460
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
461
|
+
console.log(chalk.red('\nUsage: /goal tokens <non-negative integer> (0 to clear the token cap)\n'));
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
const g = setGoalTokenBudget(ws, sk, Math.floor(n));
|
|
465
|
+
if (!g) {
|
|
466
|
+
console.log(chalk.yellow('\nNo goal to update.\n'));
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
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
|
+
agent.refreshSystemPrompt();
|
|
474
|
+
if (n === 0) {
|
|
475
|
+
console.log(chalk.green('\n✓ Token budget cleared (iteration cap still applies).\n'));
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
console.log(chalk.green(`\n✓ Token budget set to ${g.budget.maxTokens?.toLocaleString()} (${(g.budget.tokensUsed ?? 0).toLocaleString()} already used).\n`));
|
|
479
|
+
}
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
if (arg.startsWith('edit')) {
|
|
483
|
+
// /goal edit text <new text>
|
|
484
|
+
// /goal edit status <active|paused|complete|blocked|usage_limited>
|
|
485
|
+
// /goal edit budget <N>
|
|
486
|
+
// /goal edit tokens <N>
|
|
487
|
+
const rest = arg.replace(/^edit\s*/, '').trim();
|
|
488
|
+
const [field, ...valueParts] = rest.split(/\s+/);
|
|
489
|
+
const value = valueParts.join(' ').trim();
|
|
490
|
+
if (!field || !value) {
|
|
491
|
+
console.log(chalk.red('\nUsage: /goal edit <field> <value>'));
|
|
492
|
+
console.log(chalk.gray(' fields: text | status | budget | tokens\n'));
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
let g;
|
|
497
|
+
if (field === 'text') {
|
|
498
|
+
g = editGoal(ws, sk, { text: value });
|
|
499
|
+
}
|
|
500
|
+
else if (field === 'status') {
|
|
501
|
+
const allowed = ['active', 'paused', 'complete', 'blocked', 'usage_limited'];
|
|
502
|
+
if (!allowed.includes(value)) {
|
|
503
|
+
console.log(chalk.red(`\nUnknown status "${value}". Allowed: ${allowed.join(', ')}\n`));
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
g = editGoal(ws, sk, { status: value });
|
|
507
|
+
}
|
|
508
|
+
else if (field === 'budget') {
|
|
509
|
+
const n = Number(value);
|
|
510
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
511
|
+
console.log(chalk.red('\n/goal edit budget <positive integer>\n'));
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
g = editGoal(ws, sk, { maxIterations: Math.floor(n) });
|
|
515
|
+
}
|
|
516
|
+
else if (field === 'tokens') {
|
|
517
|
+
const n = Number(value);
|
|
518
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
519
|
+
console.log(chalk.red('\n/goal edit tokens <non-negative integer>\n'));
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
g = editGoal(ws, sk, { maxTokens: Math.floor(n) });
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
console.log(chalk.red(`\nUnknown edit field "${field}". Allowed: text | status | budget | tokens\n`));
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
if (!g) {
|
|
529
|
+
console.log(chalk.yellow('\nNo goal to edit. Set one first with /goal <text>.\n'));
|
|
530
|
+
}
|
|
531
|
+
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
|
+
agent.refreshSystemPrompt();
|
|
537
|
+
console.log(chalk.green(`\n✓ Updated.\n`));
|
|
538
|
+
showStatus(g);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
if (err instanceof GoalTooLongError) {
|
|
543
|
+
console.log(chalk.red(`\n✗ ${err.message}\n`));
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
console.log(chalk.red(`\n✗ ${err?.message ?? err}\n`));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
// Anything else is a new goal text — attempt set with conflict
|
|
552
|
+
// detection. If a non-complete goal is already active we throw
|
|
553
|
+
// GoalConflictError and prompt the user before overwriting; a
|
|
554
|
+
// complete goal is replaced silently (the prior work is done, this
|
|
555
|
+
// is just starting fresh and the prompt would be noise).
|
|
556
|
+
let goal;
|
|
557
|
+
try {
|
|
558
|
+
goal = setGoal(ws, arg, sk);
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
if (err instanceof GoalTooLongError) {
|
|
562
|
+
console.log(chalk.red(`\n✗ ${err.message}`));
|
|
563
|
+
console.log(chalk.gray(` Tip: a goal is a 1–3 sentence outcome statement, not a chat log. Max ${GOAL_TEXT_MAX_CHARS} chars.\n`));
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
if (err instanceof GoalConflictError) {
|
|
567
|
+
const existing = err.existing;
|
|
568
|
+
console.log(chalk.yellow(`\n⚠️ A goal is already ${existing.status.replace('_', ' ')}:`));
|
|
569
|
+
console.log(` ${chalk.cyan(existing.text)}`);
|
|
570
|
+
console.log(` ${chalk.gray(`${existing.budget.iterationsUsed}/${existing.budget.maxIterations} iterations used`)}`);
|
|
571
|
+
const confirmed = await askYesNo('Replace it with the new objective? (y/N) ', false);
|
|
572
|
+
if (!confirmed) {
|
|
573
|
+
console.log(chalk.gray('\nKeeping the current goal. Use `/goal edit text <new>` to change just the wording.\n'));
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
// Force-replace.
|
|
577
|
+
try {
|
|
578
|
+
goal = setGoal(ws, arg, sk, { force: true });
|
|
579
|
+
}
|
|
580
|
+
catch (err2) {
|
|
581
|
+
console.log(chalk.red(`\n✗ ${err2?.message ?? err2}\n`));
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
agent.refreshSystemPrompt();
|
|
590
|
+
console.log(chalk.green(`\n✓ Goal set: ${chalk.cyan(goal.text)}`));
|
|
591
|
+
console.log(chalk.gray(`Budget: ${goal.budget.maxIterations} iterations. The CLI will auto-continue after each turn until the agent calls goal_complete / goal_blocked or you /goal pause | clear.`));
|
|
592
|
+
console.log(chalk.gray('Kicking off iteration 1 now — type anything to cancel.\n'));
|
|
593
|
+
// Fire the first turn immediately so /goal doesn't require a "proceed"
|
|
594
|
+
// follow-up. The post-turn continuation loop in runAgentTurn handles
|
|
595
|
+
// iterations 2..N.
|
|
596
|
+
ctx.repl.runAgentTurn(buildGoalKickoffPrompt(goal, 'start'));
|
|
597
|
+
return true; // runAgentTurn owns its prompt cycle
|
|
598
|
+
}
|
|
599
|
+
case '/loop':
|
|
600
|
+
{
|
|
601
|
+
const arg0 = args[0];
|
|
602
|
+
if (!arg0 || arg0 === 'status') {
|
|
603
|
+
const state = getLoopState();
|
|
604
|
+
if (!state)
|
|
605
|
+
console.log(chalk.yellow('\nNo loop running.\n'));
|
|
606
|
+
else {
|
|
607
|
+
console.log(chalk.bold('\nLoop state'));
|
|
608
|
+
console.log(` Prompt: ${chalk.cyan(state.prompt)}`);
|
|
609
|
+
console.log(` Interval: ${chalk.gray(`${state.intervalMs}ms`)}`);
|
|
610
|
+
console.log(` Iterations: ${chalk.gray(state.iterations.toString())}`);
|
|
611
|
+
if (state.lastFiredAt)
|
|
612
|
+
console.log(` Last fired: ${chalk.gray(state.lastFiredAt)}`);
|
|
613
|
+
if (state.lastError)
|
|
614
|
+
console.log(` Last error: ${chalk.red(state.lastError)}`);
|
|
615
|
+
console.log(chalk.gray('\n Stop with /loop stop\n'));
|
|
616
|
+
}
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
if (arg0 === 'stop') {
|
|
620
|
+
const ok = stopLoop();
|
|
621
|
+
console.log(ok ? chalk.green('\n✓ Loop stopped.\n') : chalk.yellow('\nNo loop was running.\n'));
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
const intervalMs = parseInterval(arg0);
|
|
625
|
+
const loopPrompt = args.slice(intervalMs ? 1 : 0).join(' ').trim();
|
|
626
|
+
if (!intervalMs || !loopPrompt) {
|
|
627
|
+
console.log(chalk.red('\nUsage: /loop <interval> <prompt>'));
|
|
628
|
+
console.log(chalk.gray(' e.g. /loop 30s /review'));
|
|
629
|
+
console.log(chalk.gray(' /loop 5m check the deploy status\n'));
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
const result = startLoop(loopPrompt, intervalMs, async () => {
|
|
633
|
+
// Each tick queues the loop's prompt as if the user typed it. We use
|
|
634
|
+
// the REPL's processing flag to avoid stomping on a turn the user
|
|
635
|
+
// started manually.
|
|
636
|
+
if (ctx.repl.isProcessing())
|
|
637
|
+
return;
|
|
638
|
+
console.log(chalk.gray(`\n⟲ Loop tick (iteration ${(getLoopState()?.iterations ?? 0)})`));
|
|
639
|
+
rl.write(`${loopPrompt}\n`);
|
|
640
|
+
});
|
|
641
|
+
if (result.started) {
|
|
642
|
+
console.log(chalk.green(`\n✓ Loop started — "${loopPrompt}" every ${intervalMs}ms.`));
|
|
643
|
+
console.log(chalk.gray(' Stop with /loop stop.\n'));
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
console.log(chalk.red(`\nLoop not started: ${result.reason}\n`));
|
|
647
|
+
}
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
case '/continue':
|
|
651
|
+
{
|
|
652
|
+
const last = agent.lastUserPrompt;
|
|
653
|
+
if (!last) {
|
|
654
|
+
console.log(chalk.yellow('\nNothing to continue — no prior prompt this session. Just type your next message.\n'));
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
// Inspect child-agent state up front so /continue gives the LLM
|
|
658
|
+
// concrete instructions instead of a vague "wait for children". Without
|
|
659
|
+
// this, the model frequently text-replies "I am waiting…" without ever
|
|
660
|
+
// calling wait_agent and the turn just hangs the user.
|
|
661
|
+
reconcileStale(agent.workspaceRoot);
|
|
662
|
+
const allChildren = listSessions(agent.workspaceRoot);
|
|
663
|
+
const running = allChildren.filter((s) => s.status === 'pending' || s.status === 'running');
|
|
664
|
+
const recentlyDone = allChildren
|
|
665
|
+
.filter((s) => s.status === 'completed' || s.status === 'failed')
|
|
666
|
+
.sort((a, b) => (b.completedAt ?? '').localeCompare(a.completedAt ?? ''))
|
|
667
|
+
.slice(0, 5);
|
|
668
|
+
const sections = [
|
|
669
|
+
`You were in the middle of working on: "${last}"`,
|
|
670
|
+
'',
|
|
671
|
+
];
|
|
672
|
+
if (running.length > 0) {
|
|
673
|
+
const ids = running.map((s) => `${s.id} (${s.role}, ${s.status})`).join(', ');
|
|
674
|
+
sections.push(`## Children still running`, `${running.length} child agent${running.length === 1 ? '' : 's'} have NOT finished yet: ${ids}.`, `**You MUST call \`wait_agent\` on each one** before producing a final answer. Do not respond with prose like "I am waiting" — that is a no-op. Issue the tool calls now in this turn.`, '');
|
|
675
|
+
}
|
|
676
|
+
if (recentlyDone.length > 0) {
|
|
677
|
+
const lines = recentlyDone.map((s) => `- ${s.id} (${s.role}): ${s.status}${s.error ? ` — ${s.error}` : ''}`);
|
|
678
|
+
sections.push(`## Children that already finished`, `Use \`read_agent_transcript\` (or the cached finalOutput in \`list_agents\`) to incorporate their work:`, ...lines, '');
|
|
679
|
+
}
|
|
680
|
+
if (running.length === 0 && recentlyDone.length === 0) {
|
|
681
|
+
sections.push('No child agents are tracked. Pick up where you left off.', '');
|
|
682
|
+
}
|
|
683
|
+
sections.push(agent.lastTurnHitLoopLimit
|
|
684
|
+
? 'You ran out of tool-loop iterations before producing a final answer. Resume now: drain any pending children, then finish writing the workflow artifacts (`spec.md` / `tasks.md` / `walkthrough.md`) before giving a summary.'
|
|
685
|
+
: 'Resume the workflow. Synthesize whatever children produced, then finish writing the artifacts the workflow expects (`spec.md` / `tasks.md` / `walkthrough.md`).');
|
|
686
|
+
ctx.repl.runAgentTurn(sections.join('\n'));
|
|
687
|
+
return true; // runAgentTurn handles its own prompt cycle
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
}
|