@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.
- package/.env.example +55 -48
- package/bin/cli.cjs +71 -0
- package/dist/agent/agent.d.ts +212 -2
- package/dist/agent/agent.js +428 -38
- package/dist/cli/banner.d.ts +60 -0
- package/dist/cli/banner.js +199 -0
- package/dist/cli/cliPrompt.d.ts +69 -0
- package/dist/cli/cliPrompt.js +287 -0
- package/dist/cli/commands/_helpers.js +6 -6
- package/dist/cli/commands/guard.js +75 -10
- package/dist/cli/commands/mcp.d.ts +17 -0
- package/dist/cli/commands/mcp.js +121 -0
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/obs.js +22 -22
- package/dist/cli/commands/session.js +13 -5
- package/dist/cli/commands/ui.js +97 -45
- package/dist/cli/commands/workflow.d.ts +18 -0
- package/dist/cli/commands/workflow.js +314 -43
- package/dist/cli/repl.js +219 -132
- 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/config/config.d.ts +40 -0
- package/dist/config/config.js +45 -73
- package/dist/index.js +80 -13
- package/dist/memory/briefing.d.ts +10 -0
- package/dist/memory/briefing.js +69 -1
- package/dist/prompt/breadthHint.d.ts +5 -0
- package/dist/prompt/breadthHint.js +44 -0
- package/dist/prompt/systemPrompt.d.ts +34 -0
- package/dist/prompt/systemPrompt.js +124 -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 +90 -2
- 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 +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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
28
|
-
*
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
-
*
|
|
168
|
-
*
|
|
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
|
|
174
|
-
export declare function formatGoalBlock(goal: Goal): string;
|
|
255
|
+
export declare function buildGoalContinuationPrompt(goal: Goal, lastPrompt: string, lastAnswer: string): string;
|