@kinqs/brainrouter-cli 0.3.5 → 0.3.6

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.
Files changed (49) hide show
  1. package/.env.example +55 -48
  2. package/bin/cli.cjs +71 -0
  3. package/dist/agent/agent.d.ts +212 -2
  4. package/dist/agent/agent.js +428 -38
  5. package/dist/cli/banner.d.ts +60 -0
  6. package/dist/cli/banner.js +199 -0
  7. package/dist/cli/cliPrompt.d.ts +69 -0
  8. package/dist/cli/cliPrompt.js +287 -0
  9. package/dist/cli/commands/_helpers.js +6 -6
  10. package/dist/cli/commands/guard.js +75 -10
  11. package/dist/cli/commands/mcp.d.ts +17 -0
  12. package/dist/cli/commands/mcp.js +121 -0
  13. package/dist/cli/commands/memory.js +2 -2
  14. package/dist/cli/commands/obs.js +22 -22
  15. package/dist/cli/commands/session.js +13 -5
  16. package/dist/cli/commands/ui.js +97 -45
  17. package/dist/cli/commands/workflow.d.ts +18 -0
  18. package/dist/cli/commands/workflow.js +314 -43
  19. package/dist/cli/repl.js +219 -132
  20. package/dist/cli/spinner.d.ts +34 -0
  21. package/dist/cli/spinner.js +36 -0
  22. package/dist/cli/statusline.d.ts +67 -0
  23. package/dist/cli/statusline.js +204 -0
  24. package/dist/cli/theme.d.ts +79 -0
  25. package/dist/cli/theme.js +106 -0
  26. package/dist/cli/whereView.d.ts +81 -0
  27. package/dist/cli/whereView.js +245 -0
  28. package/dist/config/config.d.ts +40 -0
  29. package/dist/config/config.js +45 -73
  30. package/dist/index.js +80 -13
  31. package/dist/memory/briefing.d.ts +10 -0
  32. package/dist/memory/briefing.js +69 -1
  33. package/dist/prompt/breadthHint.d.ts +5 -0
  34. package/dist/prompt/breadthHint.js +44 -0
  35. package/dist/prompt/systemPrompt.d.ts +34 -0
  36. package/dist/prompt/systemPrompt.js +124 -108
  37. package/dist/runtime/dangerousCommand.d.ts +53 -0
  38. package/dist/runtime/dangerousCommand.js +105 -0
  39. package/dist/runtime/mcpClient.d.ts +38 -1
  40. package/dist/runtime/mcpClient.js +90 -2
  41. package/dist/state/goalStore.d.ts +98 -17
  42. package/dist/state/goalStore.js +132 -42
  43. package/dist/state/preferencesStore.d.ts +67 -3
  44. package/dist/state/preferencesStore.js +84 -1
  45. package/dist/state/workflowArtifacts.d.ts +63 -2
  46. package/dist/state/workflowArtifacts.js +120 -8
  47. package/dist/tests/_helpers.d.ts +31 -0
  48. package/dist/tests/_helpers.js +91 -0
  49. package/package.json +5 -4
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Single source of truth for "is this shell command destructive enough that we
3
+ * must confirm even in /mode fast?"
4
+ *
5
+ * Used by:
6
+ * - agent.ts `run_command`: in `executionMode === 'fast'` we skip the
7
+ * `askYesNo` prompt for everyday commands, but route through askYesNo
8
+ * anyway when this returns true.
9
+ * - tests: invariant that fast mode ≠ unconditional auto-approve.
10
+ *
11
+ * Heuristic, not a sandbox. The real blast-radius limiter is
12
+ * `BRAINROUTER_SANDBOX=on`. This list exists so that a typo
13
+ * (`rm -rf /` instead of `rm -rf ./build`) doesn't get auto-approved
14
+ * because the user happened to be in fast mode.
15
+ *
16
+ * Patterns are conservative on purpose: false-positives cost one extra y/N
17
+ * prompt; false-negatives cost a wiped disk. Add a pattern when you spot one
18
+ * — do not remove existing entries without a replacement.
19
+ */
20
+ const DANGEROUS_PATTERNS = [
21
+ // Recursive / forced deletions
22
+ /\brm\s+(?:-[a-zA-Z]*[rRfF][a-zA-Z]*|--recursive\b|--force\b)/,
23
+ // Anything piped/awk'd into a shell — too easy to hide an `rm` inside.
24
+ /\|\s*(?:sh|bash|zsh|fish)\b/,
25
+ // Disk imaging / zeroing
26
+ /\bdd\s+(?:if|of|bs|count)=/,
27
+ /\bmkfs(?:\.[a-z0-9]+)?\b/,
28
+ /\bfdisk\b/,
29
+ /\bshred\b/,
30
+ // Wide-open permission flips
31
+ /\bchmod\s+(?:-R\s+)?(?:[0-7]*[7]{2,3}|a\+w)\b/,
32
+ /\bchown\s+-R\b/,
33
+ // Privilege escalation
34
+ /\bsudo\b/,
35
+ /\bsu\s+-/,
36
+ // Forced or destructive git operations
37
+ /\bgit\s+push\s+(?:-f|--force)/,
38
+ /\bgit\s+reset\s+--hard/,
39
+ /\bgit\s+clean\s+-[a-zA-Z]*[fF]/,
40
+ /\bgit\s+checkout\s+--\s/,
41
+ /\bgit\s+branch\s+-D\b/,
42
+ // Package-manager mutators that touch the global tree or remove deps
43
+ /\bnpm\s+(?:uninstall|unpublish)\b/,
44
+ /\b(?:yarn|pnpm)\s+remove\b/,
45
+ // Process / system control
46
+ /\bkillall\b/,
47
+ /\bkill\s+-9\b/,
48
+ /\b(?:shutdown|reboot|halt|poweroff)\b/,
49
+ // Outbound exec-from-network — the classic curl|sh exfil/exec pattern
50
+ /\b(?:curl|wget|fetch)\b[^|]*\|\s*(?:sh|bash|zsh)\b/,
51
+ // Database wipes
52
+ /\bDROP\s+(?:DATABASE|TABLE|SCHEMA)\b/i,
53
+ /\bTRUNCATE\s+TABLE\b/i,
54
+ // Docker / k8s wipes
55
+ /\bdocker\s+system\s+prune\b/,
56
+ /\bdocker\s+(?:rm|rmi)\s+-f/,
57
+ /\bkubectl\s+delete\b/,
58
+ ];
59
+ /**
60
+ * Returns true when the command matches any pattern that fast mode should
61
+ * still gate through `askYesNo`. The check is a single-pass regex sweep
62
+ * against the literal command string — no shell parsing, no env expansion.
63
+ *
64
+ * The trailing wildcard semantics matter: `rm -rf foo` matches, `rm-rf` does
65
+ * not (word boundary), `rmdir` does not (different keyword). When in doubt,
66
+ * lean toward returning true: the cost of an extra y/N is much smaller than
67
+ * the cost of accidentally letting a destructive command through.
68
+ */
69
+ export function isDangerousCommand(command) {
70
+ if (!command)
71
+ return false;
72
+ const normalized = command.trim();
73
+ if (!normalized)
74
+ return false;
75
+ return DANGEROUS_PATTERNS.some((pattern) => pattern.test(normalized));
76
+ }
77
+ /**
78
+ * Pure decision for "what should happen when the agent calls `run_command`?"
79
+ * Split out of `agent.ts` so the policy is unit-testable without TTY mocking.
80
+ *
81
+ * - Silent children cannot answer a y/N prompt. We auto-approve only when
82
+ * the parent has opted in via `executionMode === 'fast'` AND the command
83
+ * is not in the dangerous set. Dangerous commands in silent children are
84
+ * always denied — there is no human to confirm the blast radius.
85
+ * - Interactive parents in `fast` mode skip the prompt for safe commands
86
+ * and still gate dangerous ones through `askYesNo`. In `planning` mode
87
+ * every command routes through `askYesNo`.
88
+ *
89
+ * The `executionMode === 'fast'` check is the single source of truth for
90
+ * "yolo-ish" behavior — the legacy `autoApproveShell` flag is migrated into
91
+ * `executionMode === 'fast'` on first read of `preferencesStore` so new
92
+ * callers do not need to consult both.
93
+ */
94
+ export function resolveRunCommandApproval(prefs, command, opts) {
95
+ const fastMode = prefs.executionMode === 'fast';
96
+ const dangerous = isDangerousCommand(command);
97
+ if (opts.silent) {
98
+ if (dangerous)
99
+ return 'deny-silent';
100
+ return fastMode ? 'auto-approve' : 'deny-silent';
101
+ }
102
+ if (fastMode && !dangerous)
103
+ return 'auto-approve';
104
+ return 'ask';
105
+ }
@@ -10,10 +10,31 @@ export declare class McpClientWrapper {
10
10
  * blowing up, which the agent's existing try/catch wrappers already handle.
11
11
  */
12
12
  private connected;
13
+ /**
14
+ * 10a: cached identity. Set once by `detectMcpIdentity` after the first
15
+ * successful `listTools()` (or by `connect` if the config + URL gave us
16
+ * a clear signal). The value drives status surfaces and the brain-offline
17
+ * prompt swap — distinguishes "our brain went down" from "a random
18
+ * third-party MCP went down" once item 11's multi-MCP support lands.
19
+ */
20
+ private identity;
21
+ private serverName?;
13
22
  constructor();
14
23
  /** Whether this wrapper has an active MCP transport. */
15
24
  isConnected(): boolean;
16
- connect(serverConfig: ServerConfig, llmConfig?: LLMConfig): Promise<void>;
25
+ /** 10a: who is this MCP? Set by `detectMcpIdentity`; 'unknown' before first list. */
26
+ getIdentity(): 'brainrouter' | 'third-party' | 'unknown';
27
+ /** 10a: profile name passed at connect (`brainrouter` / `local-http` / etc.). */
28
+ getServerName(): string | undefined;
29
+ /**
30
+ * 10a: connect with an optional `name` so the wrapper can render identity
31
+ * tags ("BrainRouter MCP offline" vs "third-party MCP offline") without
32
+ * the caller threading it through every error path. The pre-10a single-
33
+ * arg form remains supported — callers that don't pass a name fall back
34
+ * to URL-pattern detection.
35
+ */
36
+ connect(serverConfig: ServerConfig, llmConfig?: LLMConfig, name?: string): Promise<void>;
37
+ private _connect;
17
38
  listTools(): Promise<{
18
39
  [x: string]: unknown;
19
40
  tools: {
@@ -154,3 +175,19 @@ export declare class McpClientWrapper {
154
175
  }>;
155
176
  close(): Promise<void>;
156
177
  }
178
+ /**
179
+ * 10a: figure out who an MCP profile belongs to from config metadata + name
180
+ * + URL alone, before any network call. Explicit `identity` wins; otherwise
181
+ * we check name prefix and URL host. Returns 'unknown' when nothing matches
182
+ * — the caller (currently `listTools`) falls back to tool-signature
183
+ * detection after the first successful enumeration.
184
+ *
185
+ * Detection cases:
186
+ * - explicit `identity: 'brainrouter'` or `identity: 'third-party'` → that.
187
+ * - profile name (case-insensitive) starts with `brainrouter` → brainrouter.
188
+ * - http URL hostname matches `*.brainrouter.cloud` / `*.brainrouter.dev`
189
+ * / `*.brainrouter.io` / `*.kinqs.brainrouter.*` → brainrouter.
190
+ * - stdio command basename matches `brainrouter` / `brainrouter-mcp` → brainrouter.
191
+ * - otherwise → unknown (let the tool-signature fallback decide).
192
+ */
193
+ export declare function resolveIdentityFromConfig(serverConfig: ServerConfig, name?: string): 'brainrouter' | 'third-party' | 'unknown';
@@ -13,6 +13,15 @@ export class McpClientWrapper {
13
13
  * blowing up, which the agent's existing try/catch wrappers already handle.
14
14
  */
15
15
  connected = false;
16
+ /**
17
+ * 10a: cached identity. Set once by `detectMcpIdentity` after the first
18
+ * successful `listTools()` (or by `connect` if the config + URL gave us
19
+ * a clear signal). The value drives status surfaces and the brain-offline
20
+ * prompt swap — distinguishes "our brain went down" from "a random
21
+ * third-party MCP went down" once item 11's multi-MCP support lands.
22
+ */
23
+ identity = 'unknown';
24
+ serverName;
16
25
  constructor() {
17
26
  this.client = new Client({ name: 'brainrouter-cli', version: '0.3.5' }, { capabilities: {} });
18
27
  }
@@ -20,7 +29,30 @@ export class McpClientWrapper {
20
29
  isConnected() {
21
30
  return this.connected;
22
31
  }
23
- async connect(serverConfig, llmConfig) {
32
+ /** 10a: who is this MCP? Set by `detectMcpIdentity`; 'unknown' before first list. */
33
+ getIdentity() {
34
+ return this.identity;
35
+ }
36
+ /** 10a: profile name passed at connect (`brainrouter` / `local-http` / etc.). */
37
+ getServerName() {
38
+ return this.serverName;
39
+ }
40
+ /**
41
+ * 10a: connect with an optional `name` so the wrapper can render identity
42
+ * tags ("BrainRouter MCP offline" vs "third-party MCP offline") without
43
+ * the caller threading it through every error path. The pre-10a single-
44
+ * arg form remains supported — callers that don't pass a name fall back
45
+ * to URL-pattern detection.
46
+ */
47
+ async connect(serverConfig, llmConfig, name) {
48
+ this.serverName = name;
49
+ // Resolve identity upfront from config metadata + name/URL patterns.
50
+ // The tool-signature fallback (memory_recall + list_skills) runs after
51
+ // the first successful `listTools` in `refreshIdentityFromTools`.
52
+ this.identity = resolveIdentityFromConfig(serverConfig, name);
53
+ return this._connect(serverConfig, llmConfig);
54
+ }
55
+ async _connect(serverConfig, llmConfig) {
24
56
  if (serverConfig.type === 'stdio') {
25
57
  if (!serverConfig.command) {
26
58
  throw new Error('Stdio server configuration missing "command".');
@@ -178,7 +210,22 @@ export class McpClientWrapper {
178
210
  // with only local tools instead of crashing when it tries to enumerate.
179
211
  if (!this.connected)
180
212
  return { tools: [] };
181
- return this.client.listTools({});
213
+ const res = await this.client.listTools({});
214
+ // 10a: tool-signature fallback for identity detection. If the config +
215
+ // URL didn't already pin the identity, the BrainRouter MCP exposes a
216
+ // distinctive pair (`memory_recall` AND `list_skills`) that no neutral
217
+ // third-party MCP will. Cache the result so the next list doesn't
218
+ // re-probe — identity is stable for the lifetime of a connection.
219
+ if (this.identity === 'unknown' && Array.isArray(res?.tools)) {
220
+ const names = new Set(res.tools.map((t) => t?.name));
221
+ if (names.has('memory_recall') && names.has('list_skills')) {
222
+ this.identity = 'brainrouter';
223
+ }
224
+ else {
225
+ this.identity = 'third-party';
226
+ }
227
+ }
228
+ return res;
182
229
  }
183
230
  async callTool(name, args) {
184
231
  // Offline mode: synthesize an error envelope that downstream consumers
@@ -232,3 +279,44 @@ export class McpClientWrapper {
232
279
  this.connected = false;
233
280
  }
234
281
  }
282
+ /**
283
+ * 10a: figure out who an MCP profile belongs to from config metadata + name
284
+ * + URL alone, before any network call. Explicit `identity` wins; otherwise
285
+ * we check name prefix and URL host. Returns 'unknown' when nothing matches
286
+ * — the caller (currently `listTools`) falls back to tool-signature
287
+ * detection after the first successful enumeration.
288
+ *
289
+ * Detection cases:
290
+ * - explicit `identity: 'brainrouter'` or `identity: 'third-party'` → that.
291
+ * - profile name (case-insensitive) starts with `brainrouter` → brainrouter.
292
+ * - http URL hostname matches `*.brainrouter.cloud` / `*.brainrouter.dev`
293
+ * / `*.brainrouter.io` / `*.kinqs.brainrouter.*` → brainrouter.
294
+ * - stdio command basename matches `brainrouter` / `brainrouter-mcp` → brainrouter.
295
+ * - otherwise → unknown (let the tool-signature fallback decide).
296
+ */
297
+ export function resolveIdentityFromConfig(serverConfig, name) {
298
+ if (serverConfig.identity === 'brainrouter' || serverConfig.identity === 'third-party') {
299
+ return serverConfig.identity;
300
+ }
301
+ if (name && /^brainrouter/i.test(name.trim())) {
302
+ return 'brainrouter';
303
+ }
304
+ if (serverConfig.type === 'http' && serverConfig.url) {
305
+ try {
306
+ const url = new URL(serverConfig.url);
307
+ if (/\.brainrouter\.(cloud|dev|io|com|app)$/i.test(url.hostname)) {
308
+ return 'brainrouter';
309
+ }
310
+ }
311
+ catch {
312
+ // Malformed URL; let later code surface the connection error.
313
+ }
314
+ }
315
+ if (serverConfig.type === 'stdio' && serverConfig.command) {
316
+ const base = serverConfig.command.split(/[/\\]/).pop() ?? '';
317
+ if (/^brainrouter(-mcp)?$/i.test(base)) {
318
+ return 'brainrouter';
319
+ }
320
+ }
321
+ return 'unknown';
322
+ }
@@ -24,12 +24,20 @@
24
24
  * "you ran out of room" from "user paused" from
25
25
  * "agent gave up."
26
26
  *
27
- * Storage (per-session bucket):
28
- * ~/.brainrouter/workspaces/<encoded>/cli/sessions/<encodedKey>/goal.json
27
+ * Storage (priority chain — see `resolveGoalScope`):
28
+ * 1. Workflow bound: `<workspace>/.brainrouter/workflows/<slug>/goal.json`
29
+ * (lives in the committable workflow folder so the goal travels with
30
+ * the spec / tasks / walkthrough).
31
+ * 2. No workflow, session-scoped:
32
+ * `~/.brainrouter/workspaces/<encoded>/cli/sessions/<encodedKey>/goal.json`
33
+ * 3. Back-compat (no workflow, no sessionKey):
34
+ * `~/.brainrouter/workspaces/<encoded>/cli/goal.json`
29
35
  *
30
- * Legacy fallback paths exist for sessions created before per-session
31
- * goal isolation was added. normalize() fills missing fields with defaults
32
- * so resumed sessions don't crash on first read.
36
+ * Session-scoped reads stay isolated (Item 1 invariant never fall back to
37
+ * a prior session's goal). Workflow-bound reads stay isolated by workflow
38
+ * (Item 3 invariant switching workflows swaps which goal you see).
39
+ * normalize() fills missing fields with defaults so resumed sessions don't
40
+ * crash on first read.
33
41
  */
34
42
  export type GoalStatus = 'active' | 'paused' | 'complete' | 'blocked' | 'usage_limited';
35
43
  /** A pausing status is one where continuation is halted but resumable. */
@@ -57,7 +65,25 @@ export interface Goal {
57
65
  completedAt?: string;
58
66
  blockedReason?: string;
59
67
  }
60
- export declare const DEFAULT_GOAL_BUDGET = 10;
68
+ /**
69
+ * Default iteration cap when the user doesn't pass one.
70
+ *
71
+ * Set to a very high number (effectively "unlimited" for any real task)
72
+ * rather than a tight 10. Rationale: the goal lifecycle has three
73
+ * independent safety nets that already prevent runaway loops —
74
+ * 1. Anti-spin — a turn that made zero tool calls doesn't continue
75
+ * 2. Repeat-loop — calling the same tool with identical args 3× errors
76
+ * 3. Manual stop — Ctrl-C, /goal pause, /goal clear
77
+ *
78
+ * A hard iteration cap on top of those is overly paternalistic for users
79
+ * running local models (no $ cost) and is easily lifted with /goal budget
80
+ * <n> when wanted. Display layers should treat any value >= UNLIMITED_THRESHOLD
81
+ * as "unlimited" for friendlier UX.
82
+ */
83
+ export declare const DEFAULT_GOAL_BUDGET = 1000000;
84
+ export declare const UNLIMITED_BUDGET_THRESHOLD = 100000;
85
+ /** Format helper — used by REPL display + status output. */
86
+ export declare function formatBudget(maxIterations: number): string;
61
87
  /**
62
88
  * Hard cap on the goal text length. A goal is supposed to be a 1–3 sentence
63
89
  * outcome statement; multi-thousand-character pastes (e.g. full chat logs)
@@ -82,6 +108,55 @@ export declare class GoalConflictError extends Error {
82
108
  readonly existing: Goal;
83
109
  constructor(existing: Goal);
84
110
  }
111
+ /**
112
+ * Where the agent's goal lives RIGHT NOW. The priority chain — adapted from
113
+ * openSrc/agentmemory's fallback-provider walk (guard clauses that early-
114
+ * return per layer rather than a single flat loop) — is:
115
+ *
116
+ * 1. workflow scope — a workflow is bound via `current-workflow.json`
117
+ * (the per-user CLI pointer). Goal lives at `<workflow>/goal.json`
118
+ * next to spec.md / tasks.md / meta.json. Switching workflows carries
119
+ * the goal with the folder.
120
+ * 2. session scope — no workflow bound but a sessionKey is supplied
121
+ * (the post-Item-1 default). Goal lives at
122
+ * `<cliStateDir>/sessions/<encodedKey>/goal.json` — strictly per
123
+ * session, never falls back to a different session's file.
124
+ * 3. legacy scope — no workflow, no sessionKey. Used by the very-old
125
+ * single-process call sites that haven't been migrated yet (and by
126
+ * back-compat reads of pre-0.3.5 workspace-level goal.json files).
127
+ *
128
+ * Every read/write entrypoint routes through this single resolver so the
129
+ * priority chain has exactly one decision point. Callers don't decide where
130
+ * to look; they get a path + scope tag and act on it.
131
+ */
132
+ export type GoalScope = {
133
+ scope: 'session';
134
+ sessionKey: string;
135
+ path: string;
136
+ } | {
137
+ scope: 'legacy';
138
+ path: string;
139
+ };
140
+ /**
141
+ * Resolve the on-disk location where the active goal for this CLI process
142
+ * lives.
143
+ *
144
+ * **Design (0.3.6 decouple-goal-from-workflow, supersedes Item 3):** goal
145
+ * is **always per-session**. Workflows are durable artifact folders
146
+ * (spec.md, tasks.md, walkthrough.md, meta.json) that have nothing to do
147
+ * with the agent's autonomy primitive. The earlier Item 3 design coupled
148
+ * the two by storing goal state inside `<workflow>/goal.json`, which
149
+ * meant any two CLI sessions in the same workspace that happened to land
150
+ * on the same workflow shared a goal — silently reintroducing the
151
+ * cross-session leak PR #26 had fixed. We removed that coupling
152
+ * entirely: workflows are storage, goals are runtime, no overlap.
153
+ *
154
+ * Priority chain:
155
+ * 1. Session-scoped — `<session>/goal.json`. The normal case.
156
+ * 2. Legacy — `<cli-state>/goal.json`. Only hit by callers without a
157
+ * sessionKey (rare; mostly tooling paths that pre-date Item 1).
158
+ */
159
+ export declare function resolveGoalScope(workspaceRoot: string, sessionKey?: string): GoalScope;
85
160
  export declare function readGoal(workspaceRoot: string, sessionKey?: string): Goal | null;
86
161
  /**
87
162
  * Set a new active goal. Refuses to overwrite an in-progress goal (active,
@@ -158,17 +233,23 @@ export declare function goalHasBudgetLeft(goal: Goal): boolean;
158
233
  * decision. We detect that ahead of time by checking before the tick.
159
234
  */
160
235
  export declare function goalIsOnFinalBudgetTurn(goal: Goal): boolean;
236
+ export interface FormatGoalBlockOptions {
237
+ /**
238
+ * Override the auto-detected final-budget-turn state. Useful for tests
239
+ * and for callers that want to force-render the wrap-up directive. When
240
+ * omitted, `formatGoalBlock` calls `goalIsOnFinalBudgetTurn(goal)` itself.
241
+ */
242
+ finalBudgetTurn?: boolean;
243
+ }
244
+ export declare function formatGoalBlock(goal: Goal, options?: FormatGoalBlockOptions): string;
161
245
  /**
162
- * Wrap-up steering message injected on the final-budget turn. The agent
163
- * loop pushes this into the chat history as a system message so the model
164
- * pivots from "continue investigating" to "consolidate and report." Plain
165
- * directive, no role-play.
246
+ * Drift / ready check used by the goal-continuation prompt. Compressed
247
+ * from the prose-heavy 4-paragraph form into a 2-line checklist as part
248
+ * of 9d's prompt deduplication the goal text, status, and budget are
249
+ * now owned by the goal-anchor system message; the continuation prompt
250
+ * carries only the per-turn drift check + a pointer to the anchor.
166
251
  *
167
- * The message specifically reports WHICH cap is tight (iterations, tokens,
168
- * or both) so the model doesn't get told "one turn left" when it actually
169
- * has many iterations remaining but is near the token cap, or vice versa.
170
- * Earlier versions hardcoded the iteration framing even when only the
171
- * token heuristic tripped, which misled the model on token-budgeted runs.
252
+ * Distinct from `buildGoalKickoffPrompt` (in `commands/_helpers.ts`),
253
+ * which is the FIRST-turn prompt fired by `/goal <text>` and `/goal resume`.
172
254
  */
173
- export declare function buildBudgetSteeringMessage(goal: Goal): string;
174
- export declare function formatGoalBlock(goal: Goal): string;
255
+ export declare function buildGoalContinuationPrompt(goal: Goal, lastPrompt: string, lastAnswer: string): string;