@pugi/cli 0.1.0-alpha.10

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/run.js +2 -0
  4. package/dist/commands/jobs.js +245 -0
  5. package/dist/core/agents/loader.js +104 -0
  6. package/dist/core/agents/registry.js +69 -0
  7. package/dist/core/auto-open-browser.js +128 -0
  8. package/dist/core/bash-classifier.js +1001 -0
  9. package/dist/core/clipboard.js +70 -0
  10. package/dist/core/context/builder.js +114 -0
  11. package/dist/core/context/compaction-events.js +99 -0
  12. package/dist/core/context/compaction.js +602 -0
  13. package/dist/core/context/invariants.js +250 -0
  14. package/dist/core/context/markdown-loader.js +270 -0
  15. package/dist/core/credentials.js +355 -0
  16. package/dist/core/engine/adapter-runner.js +8 -0
  17. package/dist/core/engine/anvil-client.js +156 -0
  18. package/dist/core/engine/compaction-hook.js +154 -0
  19. package/dist/core/engine/index.js +12 -0
  20. package/dist/core/engine/native-pugi.js +369 -0
  21. package/dist/core/engine/noop.js +27 -0
  22. package/dist/core/engine/prompts.js +118 -0
  23. package/dist/core/engine/tool-bridge.js +313 -0
  24. package/dist/core/file-cache.js +29 -0
  25. package/dist/core/hooks.js +415 -0
  26. package/dist/core/index-store.js +260 -0
  27. package/dist/core/jobs/registry.js +462 -0
  28. package/dist/core/mcp/client.js +316 -0
  29. package/dist/core/mcp/registry.js +171 -0
  30. package/dist/core/mcp/trust.js +91 -0
  31. package/dist/core/path-security.js +63 -0
  32. package/dist/core/permission.js +309 -0
  33. package/dist/core/repl/cap-warning.js +91 -0
  34. package/dist/core/repl/clipboard-read.js +174 -0
  35. package/dist/core/repl/history-search.js +175 -0
  36. package/dist/core/repl/history.js +172 -0
  37. package/dist/core/repl/kill-ring.js +138 -0
  38. package/dist/core/repl/session.js +618 -0
  39. package/dist/core/repl/slash-commands.js +227 -0
  40. package/dist/core/repl/workspace-context.js +113 -0
  41. package/dist/core/session.js +258 -0
  42. package/dist/core/settings.js +59 -0
  43. package/dist/core/skills/loader.js +454 -0
  44. package/dist/core/skills/sources.js +480 -0
  45. package/dist/core/skills/trust.js +172 -0
  46. package/dist/core/subagents/dispatcher.js +258 -0
  47. package/dist/core/subagents/index.js +26 -0
  48. package/dist/core/subagents/spawn.js +86 -0
  49. package/dist/core/trust.js +109 -0
  50. package/dist/index.js +8 -0
  51. package/dist/runtime/cli.js +3405 -0
  52. package/dist/runtime/commands/agents.js +385 -0
  53. package/dist/runtime/commands/budget.js +192 -0
  54. package/dist/runtime/commands/config.js +231 -0
  55. package/dist/runtime/commands/privacy.js +107 -0
  56. package/dist/runtime/commands/skills.js +401 -0
  57. package/dist/runtime/commands/undo.js +329 -0
  58. package/dist/runtime/update-check.js +294 -0
  59. package/dist/tools/bash.js +660 -0
  60. package/dist/tools/file-tools.js +346 -0
  61. package/dist/tools/registry.js +25 -0
  62. package/dist/tools/web-fetch.js +535 -0
  63. package/dist/tui/agent-tree.js +66 -0
  64. package/dist/tui/conversation-pane.js +45 -0
  65. package/dist/tui/device-flow.js +142 -0
  66. package/dist/tui/input-box.js +474 -0
  67. package/dist/tui/login-picker.js +69 -0
  68. package/dist/tui/render.js +125 -0
  69. package/dist/tui/repl-render.js +240 -0
  70. package/dist/tui/repl-splash-art.js +64 -0
  71. package/dist/tui/repl-splash.js +111 -0
  72. package/dist/tui/repl.js +214 -0
  73. package/dist/tui/slash-palette.js +106 -0
  74. package/dist/tui/splash-data.js +61 -0
  75. package/dist/tui/splash.js +31 -0
  76. package/dist/tui/status-bar.js +71 -0
  77. package/dist/tui/update-banner.js +8 -0
  78. package/dist/tui/workspace-context.js +105 -0
  79. package/package.json +71 -0
@@ -0,0 +1,369 @@
1
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { defaultEngineBudgets, runEngineLoop, } from '@pugi/sdk';
4
+ import { FileReadCache } from '../file-cache.js';
5
+ import { loadSettings } from '../settings.js';
6
+ import { openSession, recordToolCall, recordToolResult } from '../session.js';
7
+ import { buildExecutor, buildToolsSchema } from './tool-bridge.js';
8
+ import { personaSlugFor, systemPromptFor } from './prompts.js';
9
+ /**
10
+ * Real `NativePugiEngineAdapter`. Drives the Pugi CLI's tool-use loop:
11
+ *
12
+ * 1. Pick a system prompt + persona based on the task kind
13
+ * (code/explain/fix/plan/build).
14
+ * 2. Build an OpenAI-shaped tools schema from the local tool registry,
15
+ * gated by plan-mode (read-only).
16
+ * 3. Open a workspace tool context (settings, session, read cache).
17
+ * 4. Drive `runEngineLoop` against an `EngineLoopClient` until the
18
+ * model returns a final text answer or the per-command budget is
19
+ * exhausted.
20
+ * 5. Surface every turn / tool call into both the engine event stream
21
+ * (consumer-visible status events) and the existing session log
22
+ * (`.pugi/events.jsonl`) so audit replay sees every step.
23
+ *
24
+ * The adapter is intentionally transport-agnostic. `client` is required
25
+ * at construction; the CLI builds an `AnvilEngineLoopClient` from the
26
+ * resolved credentials, tests inject a fixture client. The adapter
27
+ * NEVER reads `process.env.PUGI_API_KEY` itself — that lives one layer
28
+ * up so unit tests can construct the adapter with an in-memory client.
29
+ *
30
+ * The engine task → loop mapping:
31
+ * - `task.kind === 'build_task'` is mapped to the `build` command.
32
+ * - `task.prompt` is the user message.
33
+ * - `task.workspaceRoot` pins the workspace root for tool execution.
34
+ * - `task.permissionMode` is read by the existing permission module;
35
+ * the adapter itself only enforces the plan-mode tool gate which is
36
+ * keyed on `kind`, not on permissionMode.
37
+ */
38
+ export class NativePugiEngineAdapter {
39
+ options;
40
+ name = 'native-pugi';
41
+ /**
42
+ * Per-adapter scratch map: links the loop's tool_call id to the
43
+ * audit record id returned by `recordToolCall`. Code Reviewer P2
44
+ * retro 2026-05-23 moved this off the module scope — two adapters
45
+ * driven concurrently (cabinet UI + CLI on the same process) would
46
+ * otherwise share the same Map and a fast turn from adapter A
47
+ * could `.delete()` an entry that belonged to adapter B before its
48
+ * `onToolResult` fired, dropping audit pairing for adapter B.
49
+ * Keeping the Map per-instance contains the collision blast radius
50
+ * to a single `run()` invocation.
51
+ */
52
+ engineToolCallIds = new Map();
53
+ constructor(options) {
54
+ this.options = options;
55
+ }
56
+ async capabilities() {
57
+ return {
58
+ supportsStreaming: true,
59
+ supportsFileEdits: true,
60
+ supportsShell: true,
61
+ supportsLsp: false,
62
+ supportsSubagents: false,
63
+ };
64
+ }
65
+ async *run(task, ctx) {
66
+ const kind = toCommandKind(task.kind);
67
+ const root = task.workspaceRoot;
68
+ const session = this.options.session ?? openSession(root);
69
+ const settings = loadSettings(root);
70
+ const toolCtx = {
71
+ root,
72
+ settings,
73
+ session,
74
+ readCache: new FileReadCache(),
75
+ };
76
+ const budget = task.budget?.tokens
77
+ ? {
78
+ maxTokens: task.budget.tokens,
79
+ // The task-level budget only carries tokens; tool calls keep
80
+ // the per-command default so a careless caller cannot disable
81
+ // the call-count guard by overriding usd/tokens.
82
+ maxToolCalls: defaultEngineBudgets[kind].maxToolCalls,
83
+ }
84
+ : defaultEngineBudgets[kind];
85
+ yield {
86
+ type: 'status',
87
+ message: `Pugi engine starting: kind=${kind} budget=${budget.maxToolCalls} calls / ${budget.maxTokens} tokens`,
88
+ };
89
+ // Buffer status events emitted from inside the loop hooks. Async
90
+ // generators cannot yield from synchronous callbacks, so we collect
91
+ // them in a queue and drain after the loop call completes. The loop
92
+ // is short enough (≤ ~30 turns) that latency-to-stdout is acceptable
93
+ // — a follow-up PR can switch to an event emitter for true streaming.
94
+ const buffer = [];
95
+ // Track files mutated by the loop. We extract the path from the JSON
96
+ // arguments of every successful write/edit tool call; `bash` is left
97
+ // out because its filesystem footprint is opaque (a single command
98
+ // can touch dozens of paths via `make`, `pnpm build`, etc). The
99
+ // per-session events.jsonl already carries every file_mutation event
100
+ // for replay; this set is only the headline summary the CLI prints.
101
+ const filesChanged = new Set();
102
+ // Pending lookup: call.id → path extracted from arguments. We only
103
+ // commit to `filesChanged` when the corresponding onToolResult fires
104
+ // with `ok: true`, so a refused or failed edit does not surface as
105
+ // a phantom change in the operator summary.
106
+ const pendingMutations = new Map();
107
+ // Per-session events mirror — `.pugi/sessions/<id>/events.jsonl`.
108
+ // The existing global log at `.pugi/events.jsonl` is preserved as
109
+ // the audit-replay source of truth; this mirror is the easy-to-find
110
+ // per-run log for operators and the cabinet UI (Sprint 2B).
111
+ const sessionEventsPath = openSessionMirror(root, session.id);
112
+ const hooks = {
113
+ onTurnStart: (turnIndex, messageCount) => {
114
+ const msg = `turn ${turnIndex + 1}: requesting model (transcript=${messageCount} messages)`;
115
+ buffer.push({ type: 'status', message: msg });
116
+ appendSessionMirror(sessionEventsPath, { type: 'turn_start', turn: turnIndex + 1, transcript: messageCount });
117
+ },
118
+ onTurnComplete: (turnIndex, response) => {
119
+ if (response.stop === 'tool_use') {
120
+ const calls = response.assistantMessage.toolCalls ?? [];
121
+ buffer.push({
122
+ type: 'status',
123
+ message: `turn ${turnIndex + 1}: model requested ${calls.length} tool call(s)`,
124
+ });
125
+ appendSessionMirror(sessionEventsPath, {
126
+ type: 'turn_complete',
127
+ turn: turnIndex + 1,
128
+ stop: 'tool_use',
129
+ toolCalls: calls.length,
130
+ tokensUsed: response.tokensUsed,
131
+ });
132
+ }
133
+ else if (response.stop === 'text') {
134
+ buffer.push({
135
+ type: 'status',
136
+ message: `turn ${turnIndex + 1}: model returned final text (${response.content.length} chars)`,
137
+ });
138
+ appendSessionMirror(sessionEventsPath, {
139
+ type: 'turn_complete',
140
+ turn: turnIndex + 1,
141
+ stop: 'text',
142
+ contentLength: response.content.length,
143
+ tokensUsed: response.tokensUsed,
144
+ });
145
+ }
146
+ },
147
+ onToolCall: (call) => {
148
+ // Record under an `engine_tool` prefix so the audit log can
149
+ // distinguish loop-driven calls from direct CLI tool calls.
150
+ const id = recordToolCall(session, `engine:${call.name}`, call.arguments.slice(0, 200));
151
+ // Stash the audit id on the call for `onToolResult` to close.
152
+ this.engineToolCallIds.set(call.id, id);
153
+ // Extract a candidate path for write/edit so we can build the
154
+ // filesChanged summary if (and only if) the call succeeds. Bad
155
+ // JSON is harmless here — we ignore it and the executor surfaces
156
+ // the actual parse error to the model.
157
+ if (call.name === 'write' || call.name === 'edit') {
158
+ const path = extractPathArg(call.arguments);
159
+ if (path)
160
+ pendingMutations.set(call.id, path);
161
+ }
162
+ buffer.push({
163
+ type: 'status',
164
+ message: `tool_call: ${call.name}(${call.arguments.slice(0, 80)}${call.arguments.length > 80 ? '...' : ''})`,
165
+ });
166
+ appendSessionMirror(sessionEventsPath, {
167
+ type: 'tool_call',
168
+ tool: call.name,
169
+ callId: call.id,
170
+ argsPreview: call.arguments.slice(0, 200),
171
+ });
172
+ },
173
+ onToolResult: (call, result) => {
174
+ const auditId = this.engineToolCallIds.get(call.id);
175
+ if (auditId) {
176
+ if (result.ok) {
177
+ recordToolResult(session, auditId, 'success', result.content.slice(0, 200));
178
+ }
179
+ else {
180
+ recordToolResult(session, auditId, 'error', result.error.slice(0, 200));
181
+ }
182
+ this.engineToolCallIds.delete(call.id);
183
+ }
184
+ const pendingPath = pendingMutations.get(call.id);
185
+ if (pendingPath) {
186
+ if (result.ok)
187
+ filesChanged.add(pendingPath);
188
+ pendingMutations.delete(call.id);
189
+ }
190
+ buffer.push({
191
+ type: 'status',
192
+ message: result.ok
193
+ ? `tool_result: ${call.name} ok`
194
+ : `tool_result: ${call.name} error: ${result.error.slice(0, 120)}`,
195
+ });
196
+ appendSessionMirror(sessionEventsPath, {
197
+ type: 'tool_result',
198
+ tool: call.name,
199
+ callId: call.id,
200
+ ok: result.ok,
201
+ summary: result.ok ? result.content.slice(0, 200) : result.error.slice(0, 200),
202
+ });
203
+ },
204
+ };
205
+ let outcome;
206
+ try {
207
+ outcome = await runEngineLoop({
208
+ client: this.options.client,
209
+ executor: buildExecutor({ kind, ctx: toolCtx }),
210
+ systemPrompt: systemPromptFor(kind),
211
+ userPrompt: task.prompt,
212
+ tools: buildToolsSchema(kind),
213
+ budget,
214
+ personaSlug: personaSlugFor(kind),
215
+ hooks,
216
+ temperature: this.options.temperature ?? 0.2,
217
+ signal: ctx.signal,
218
+ });
219
+ }
220
+ catch (error) {
221
+ // Defensive — runEngineLoop wraps errors into status: failed, so
222
+ // this branch is only hit if the executor or hooks themselves
223
+ // throw uncaught. Surface as a failed result so the CLI exits
224
+ // non-zero rather than hanging.
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ yield {
227
+ type: 'result',
228
+ result: {
229
+ status: 'failed',
230
+ summary: `engine loop crashed: ${message}`,
231
+ filesChanged: [],
232
+ patchRefs: [],
233
+ testsRun: [],
234
+ risks: [`unhandled error in engine adapter: ${message}`],
235
+ eventRefs: [],
236
+ },
237
+ };
238
+ return;
239
+ }
240
+ // Drain status buffer first so consumers see the chronological order.
241
+ for (const event of buffer)
242
+ yield event;
243
+ // Translate the loop outcome into an EngineResult.
244
+ const status = outcome.status === 'completed'
245
+ ? 'done'
246
+ : outcome.status === 'failed'
247
+ ? 'failed'
248
+ : 'blocked';
249
+ const summaryPrefix = outcome.status === 'completed'
250
+ ? ''
251
+ : outcome.status === 'budget_exhausted'
252
+ ? '[budget_exhausted] '
253
+ : outcome.status === 'tool_refused'
254
+ ? '[plan_mode_refused] '
255
+ : '[failed] ';
256
+ const filesChangedList = Array.from(filesChanged).sort();
257
+ appendSessionMirror(sessionEventsPath, {
258
+ type: 'outcome',
259
+ status: outcome.status,
260
+ toolCallCount: outcome.toolCallCount,
261
+ turnsUsed: outcome.turnsUsed,
262
+ tokensUsed: outcome.tokensUsed,
263
+ filesChanged: filesChangedList,
264
+ reason: outcome.reason,
265
+ });
266
+ yield {
267
+ type: 'result',
268
+ result: {
269
+ status,
270
+ summary: `${summaryPrefix}${outcome.finalText || outcome.reason || 'no answer returned'}`,
271
+ filesChanged: filesChangedList,
272
+ patchRefs: [],
273
+ testsRun: [],
274
+ risks: outcome.status === 'completed'
275
+ ? []
276
+ : [outcome.reason ?? `outcome=${outcome.status}`],
277
+ eventRefs: [
278
+ `tool_calls=${outcome.toolCallCount}`,
279
+ `turns=${outcome.turnsUsed}`,
280
+ `tokens=${outcome.tokensUsed}`,
281
+ // `outcome=<status>` is a machine-readable echo so callers
282
+ // (cli.ts plan exit code, cabinet UI) can distinguish
283
+ // `budget_exhausted` from `tool_refused` without parsing
284
+ // the human-readable summary prefix. Code Reviewer P2
285
+ // retro 2026-05-23: plan exit code previously collapsed
286
+ // both blocked reasons into 0, which masked budget hits.
287
+ `outcome=${outcome.status}`,
288
+ `session=${session.id}`,
289
+ `ctx=${ctx.sessionId}`,
290
+ `mirror=${sessionEventsPath}`,
291
+ ],
292
+ },
293
+ };
294
+ }
295
+ }
296
+ /**
297
+ * Extract a workspace-relative path from a tool_call's JSON arguments.
298
+ * Used by the adapter hook layer to build the filesChanged summary at
299
+ * the end of the run. Returns `null` on bad JSON / missing field so the
300
+ * caller can quietly skip; the executor surfaces the real parse error
301
+ * to the model.
302
+ */
303
+ function extractPathArg(raw) {
304
+ if (!raw)
305
+ return null;
306
+ try {
307
+ const parsed = JSON.parse(raw);
308
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
309
+ const path = parsed.path;
310
+ if (typeof path === 'string' && path.length > 0)
311
+ return path;
312
+ }
313
+ }
314
+ catch {
315
+ // bad JSON — ignored here, the executor produces the canonical error.
316
+ }
317
+ return null;
318
+ }
319
+ /**
320
+ * Open the per-session events mirror at
321
+ * `<root>/.pugi/sessions/<sessionId>/events.jsonl` and return the path.
322
+ *
323
+ * The global audit log at `.pugi/events.jsonl` is the source of truth
324
+ * for replay; this mirror is the per-run convenience copy that the
325
+ * cabinet UI surfaces. Both share the same schema-of-strings format so
326
+ * the consumer can `jq` either file without translation. When `.pugi`
327
+ * does not exist yet (init-less run) we no-op and return an empty
328
+ * string; the hooks then write to nowhere.
329
+ */
330
+ function openSessionMirror(root, sessionId) {
331
+ const pugiDir = resolve(root, '.pugi');
332
+ if (!existsSync(pugiDir))
333
+ return '';
334
+ const sessionDir = resolve(pugiDir, 'sessions', sessionId);
335
+ try {
336
+ mkdirSync(sessionDir, { recursive: true });
337
+ }
338
+ catch {
339
+ return '';
340
+ }
341
+ return resolve(sessionDir, 'events.jsonl');
342
+ }
343
+ function appendSessionMirror(path, event) {
344
+ if (!path)
345
+ return;
346
+ const enriched = { timestamp: new Date().toISOString(), ...event };
347
+ try {
348
+ appendFileSync(path, `${JSON.stringify(enriched)}\n`, { encoding: 'utf8', mode: 0o600 });
349
+ }
350
+ catch {
351
+ // Mirror is best-effort — the global audit log already captured the
352
+ // tool_call / tool_result events via session.ts.
353
+ }
354
+ }
355
+ /**
356
+ * Map the SDK's engine task kind to a CLI command kind. The SDK uses
357
+ * `build_task` as the canonical name for what the CLI exposes as
358
+ * `pugi build`; everything else passes through.
359
+ */
360
+ function toCommandKind(kind) {
361
+ if (kind === 'build_task')
362
+ return 'build';
363
+ return kind;
364
+ }
365
+ // The per-adapter `engineToolCallIds` Map lives on the
366
+ // `NativePugiEngineAdapter` instance above — Code Reviewer P2 retro
367
+ // 2026-05-23 lifted it off the module scope to prevent collisions
368
+ // under parallel adapter runs (cabinet UI + CLI sharing one process).
369
+ //# sourceMappingURL=native-pugi.js.map
@@ -0,0 +1,27 @@
1
+ export class NoopEngineAdapter {
2
+ name = 'noop';
3
+ async capabilities() {
4
+ return {
5
+ supportsStreaming: false,
6
+ supportsFileEdits: false,
7
+ supportsShell: false,
8
+ supportsLsp: false,
9
+ supportsSubagents: false,
10
+ };
11
+ }
12
+ async *run(task, _ctx) {
13
+ yield {
14
+ type: 'result',
15
+ result: {
16
+ status: 'blocked',
17
+ summary: `No engine configured for ${task.kind}`,
18
+ filesChanged: [],
19
+ patchRefs: [],
20
+ testsRun: [],
21
+ risks: ['Engine adapter is not configured yet.'],
22
+ eventRefs: [],
23
+ },
24
+ };
25
+ }
26
+ }
27
+ //# sourceMappingURL=noop.js.map
@@ -0,0 +1,118 @@
1
+ import { getJobRegistry, summarizeJobsForPrompt, } from '../jobs/registry.js';
2
+ /**
3
+ * System prompts for each engine command. Each prompt:
4
+ * - Anchors the model in Pugi's local-first contract (ADR-0037).
5
+ * - Lists the tools the model may call (the registry is authoritative,
6
+ * but stating it inline prevents the model from inventing tool names
7
+ * when the tool schema is large).
8
+ * - Defines the deliverable shape so the model produces a useful final
9
+ * text answer instead of "here is what I did".
10
+ *
11
+ * The prompts are intentionally terse. Per gstack `engineering-standards`
12
+ * the persona system prompt comes from the runtime (Anvil bridge
13
+ * prepends `oes-dev` / Sigma prompt automatically when configured); these
14
+ * prompts ride on top and scope the model to the current command.
15
+ *
16
+ * Sprint α5.9 (ADR-0056 PR-PUGI-CLI-M1-GAP-J): the system prompt picks up
17
+ * a `BACKGROUND JOBS:` snapshot appended at the tail so the agent loop
18
+ * knows what background bash work is currently on watch and can avoid
19
+ * spawning a duplicate. The snapshot is sourced from `JobRegistry` and
20
+ * formatted by `summarizeJobsForPrompt` so the surface is single-sourced.
21
+ */
22
+ const COMMON_LOCAL_FIRST_PREAMBLE = [
23
+ 'You are the Pugi CLI agent running locally inside the operator\'s repository.',
24
+ 'The local filesystem is the source of truth. Every change you make is committed locally; nothing is uploaded by default (ADR-0037 local-first).',
25
+ 'You have a tool registry: read, write, edit, grep, glob, bash. Call tools to inspect and modify the workspace.',
26
+ 'Cite file paths relative to the workspace root. Keep edits minimal and reversible.',
27
+ 'When you are done, return a single final text answer that the operator can read on the CLI.',
28
+ ].join(' ');
29
+ const PLAN_TOOLS_NOTE = 'PLAN MODE IS READ-ONLY. You may call read, grep, glob. Calls to write, edit, or bash will be refused and end the run. Produce a written plan, not changes.';
30
+ const EDIT_FLOW_RULES = [
31
+ 'Before calling edit on a file you have not yet read this session, call read first — the edit tool fails otherwise.',
32
+ 'Prefer edit over write when changing existing files; write only for newly created files.',
33
+ 'After your last tool call, summarise what you changed and what the operator should review.',
34
+ ].join(' ');
35
+ export function systemPromptFor(kind) {
36
+ const base = baseSystemPromptFor(kind);
37
+ const snapshot = formatBackgroundJobsSnapshot(getJobRegistrySafely());
38
+ if (!snapshot)
39
+ return base;
40
+ return `${base}\n\n${snapshot}`;
41
+ }
42
+ function baseSystemPromptFor(kind) {
43
+ switch (kind) {
44
+ case 'code':
45
+ return [
46
+ COMMON_LOCAL_FIRST_PREAMBLE,
47
+ 'Command: `pugi code`. The operator gave you a feature request or refactor. Implement it end-to-end.',
48
+ EDIT_FLOW_RULES,
49
+ 'If the request is ambiguous, ask one clarifying question by returning a text answer instead of editing.',
50
+ ].join('\n\n');
51
+ case 'explain':
52
+ return [
53
+ COMMON_LOCAL_FIRST_PREAMBLE,
54
+ 'Command: `pugi explain`. The operator pointed you at code or a concept. Read what you need, then produce a walkthrough.',
55
+ 'You should not edit files. If you need to demonstrate a change, describe it in the final answer.',
56
+ 'Keep the explanation to the operator\'s level — concise, with file:line references where useful.',
57
+ ].join('\n\n');
58
+ case 'fix':
59
+ return [
60
+ COMMON_LOCAL_FIRST_PREAMBLE,
61
+ 'Command: `pugi fix`. The operator gave you a bug report or failing test. Investigate the root cause first, then apply the smallest patch that fixes it.',
62
+ EDIT_FLOW_RULES,
63
+ 'Surface the root cause in your final answer so the operator can verify the diagnosis is correct.',
64
+ ].join('\n\n');
65
+ case 'plan':
66
+ return [
67
+ COMMON_LOCAL_FIRST_PREAMBLE,
68
+ 'Command: `pugi plan`. The operator wants a plan, not an implementation.',
69
+ PLAN_TOOLS_NOTE,
70
+ 'Produce a numbered list of steps with rough file targets and a brief risk note. Cite the files you consulted.',
71
+ ].join('\n\n');
72
+ case 'build':
73
+ return [
74
+ COMMON_LOCAL_FIRST_PREAMBLE,
75
+ 'Command: `pugi build`. The operator wants you to scaffold a feature across multiple files.',
76
+ EDIT_FLOW_RULES,
77
+ 'Group related edits, run lint/test via bash where it adds confidence, and list every file you created or modified in the final answer.',
78
+ ].join('\n\n');
79
+ }
80
+ }
81
+ /**
82
+ * Builds the BACKGROUND JOBS snapshot block injected at the tail of
83
+ * the system prompt. Sync because the surrounding `systemPromptFor`
84
+ * builder is sync (the engine adapter does not await prompt
85
+ * assembly) and the JobRegistry's `listSync()` is essentially free
86
+ * (single JSON file read with sync fs primitives). Returns an empty
87
+ * string if the registry cannot be reached so the prompt assembly
88
+ * never crashes when the ledger is unavailable.
89
+ */
90
+ export function formatBackgroundJobsSnapshot(registry) {
91
+ if (!registry)
92
+ return '';
93
+ try {
94
+ const entries = registry.listSync();
95
+ return summarizeJobsForPrompt(entries);
96
+ }
97
+ catch {
98
+ return '';
99
+ }
100
+ }
101
+ function getJobRegistrySafely() {
102
+ try {
103
+ return getJobRegistry();
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ /**
110
+ * Anvil persona slug to invoke per command. Today every command routes
111
+ * to `oes-dev` (Sigma) — the Tier-2 reviewer persona already configured
112
+ * for Pugi runtime work. When we add per-command personas (e.g. `pugi-
113
+ * planner`, `pugi-coder`) this is the single switch to update.
114
+ */
115
+ export function personaSlugFor(_kind) {
116
+ return 'oes-dev';
117
+ }
118
+ //# sourceMappingURL=prompts.js.map