@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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/run.js +2 -0
- package/dist/commands/jobs.js +245 -0
- package/dist/core/agents/loader.js +104 -0
- package/dist/core/agents/registry.js +69 -0
- package/dist/core/auto-open-browser.js +128 -0
- package/dist/core/bash-classifier.js +1001 -0
- package/dist/core/clipboard.js +70 -0
- package/dist/core/context/builder.js +114 -0
- package/dist/core/context/compaction-events.js +99 -0
- package/dist/core/context/compaction.js +602 -0
- package/dist/core/context/invariants.js +250 -0
- package/dist/core/context/markdown-loader.js +270 -0
- package/dist/core/credentials.js +355 -0
- package/dist/core/engine/adapter-runner.js +8 -0
- package/dist/core/engine/anvil-client.js +156 -0
- package/dist/core/engine/compaction-hook.js +154 -0
- package/dist/core/engine/index.js +12 -0
- package/dist/core/engine/native-pugi.js +369 -0
- package/dist/core/engine/noop.js +27 -0
- package/dist/core/engine/prompts.js +118 -0
- package/dist/core/engine/tool-bridge.js +313 -0
- package/dist/core/file-cache.js +29 -0
- package/dist/core/hooks.js +415 -0
- package/dist/core/index-store.js +260 -0
- package/dist/core/jobs/registry.js +462 -0
- package/dist/core/mcp/client.js +316 -0
- package/dist/core/mcp/registry.js +171 -0
- package/dist/core/mcp/trust.js +91 -0
- package/dist/core/path-security.js +63 -0
- package/dist/core/permission.js +309 -0
- package/dist/core/repl/cap-warning.js +91 -0
- package/dist/core/repl/clipboard-read.js +174 -0
- package/dist/core/repl/history-search.js +175 -0
- package/dist/core/repl/history.js +172 -0
- package/dist/core/repl/kill-ring.js +138 -0
- package/dist/core/repl/session.js +618 -0
- package/dist/core/repl/slash-commands.js +227 -0
- package/dist/core/repl/workspace-context.js +113 -0
- package/dist/core/session.js +258 -0
- package/dist/core/settings.js +59 -0
- package/dist/core/skills/loader.js +454 -0
- package/dist/core/skills/sources.js +480 -0
- package/dist/core/skills/trust.js +172 -0
- package/dist/core/subagents/dispatcher.js +258 -0
- package/dist/core/subagents/index.js +26 -0
- package/dist/core/subagents/spawn.js +86 -0
- package/dist/core/trust.js +109 -0
- package/dist/index.js +8 -0
- package/dist/runtime/cli.js +3405 -0
- package/dist/runtime/commands/agents.js +385 -0
- package/dist/runtime/commands/budget.js +192 -0
- package/dist/runtime/commands/config.js +231 -0
- package/dist/runtime/commands/privacy.js +107 -0
- package/dist/runtime/commands/skills.js +401 -0
- package/dist/runtime/commands/undo.js +329 -0
- package/dist/runtime/update-check.js +294 -0
- package/dist/tools/bash.js +660 -0
- package/dist/tools/file-tools.js +346 -0
- package/dist/tools/registry.js +25 -0
- package/dist/tools/web-fetch.js +535 -0
- package/dist/tui/agent-tree.js +66 -0
- package/dist/tui/conversation-pane.js +45 -0
- package/dist/tui/device-flow.js +142 -0
- package/dist/tui/input-box.js +474 -0
- package/dist/tui/login-picker.js +69 -0
- package/dist/tui/render.js +125 -0
- package/dist/tui/repl-render.js +240 -0
- package/dist/tui/repl-splash-art.js +64 -0
- package/dist/tui/repl-splash.js +111 -0
- package/dist/tui/repl.js +214 -0
- package/dist/tui/slash-palette.js +106 -0
- package/dist/tui/splash-data.js +61 -0
- package/dist/tui/splash.js +31 -0
- package/dist/tui/status-bar.js +71 -0
- package/dist/tui/update-banner.js +8 -0
- package/dist/tui/workspace-context.js +105 -0
- 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
|