@pugi/cli 0.1.0-beta.17 → 0.1.0-beta.19
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/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +196 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +442 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/engine/native-pugi.js +20 -0
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +251 -49
- package/dist/core/file-cache.js +113 -1
- package/dist/core/mcp/client.js +66 -6
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/permissions/gate.js +187 -0
- package/dist/core/permissions/index.js +18 -0
- package/dist/core/permissions/mode.js +102 -0
- package/dist/core/permissions/state.js +160 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/repl/session.js +261 -9
- package/dist/core/repl/slash-commands.js +67 -4
- package/dist/runtime/cli.js +153 -58
- package/dist/runtime/commands/compact.js +296 -0
- package/dist/runtime/commands/doctor.js +369 -0
- package/dist/runtime/commands/mcp.js +290 -3
- package/dist/runtime/commands/permissions.js +87 -0
- package/dist/runtime/commands/status.js +178 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tools/agent-tool.js +18 -4
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/file-tools.js +57 -14
- package/dist/tools/registry.js +7 -0
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +54 -0
- package/dist/tui/conversation-pane.js +68 -7
- package/dist/tui/doctor-table.js +31 -0
- package/dist/tui/status-table.js +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STATUS snapshot — concise session-state probe for `pugi status` /
|
|
3
|
+
* in-REPL `/status` (Leak L34, 2026-05-27).
|
|
4
|
+
*
|
|
5
|
+
* Different from `pugi doctor` (which probes ENVIRONMENT health —
|
|
6
|
+
* Node version, disk space, auth round-trip, MCP servers, …). The
|
|
7
|
+
* status snapshot answers the operator question "what is THIS
|
|
8
|
+
* Pugi session doing right now?" — current session id + age, cwd,
|
|
9
|
+
* permission mode, CLI version, cumulative token usage, active
|
|
10
|
+
* dispatches, last command, compact boundary count, auth identity.
|
|
11
|
+
*
|
|
12
|
+
* # Fail-soft contract
|
|
13
|
+
*
|
|
14
|
+
* Every field is gathered via try/catch. A missing dep (permission
|
|
15
|
+
* state module not yet present, no `.pugi/agent-progress/` dir,
|
|
16
|
+
* no `~/.pugi/credentials.json`) is **never** an error — it
|
|
17
|
+
* degrades to `null` for nullable fields or a sentinel label
|
|
18
|
+
* (`"unknown"`, `"n/a"`, `0`) for required ones. The snapshot
|
|
19
|
+
* never throws; the renderer can always print a complete table.
|
|
20
|
+
*
|
|
21
|
+
* The module is intentionally dep-injected so the spec can drive
|
|
22
|
+
* every fail-soft branch without standing up `fs`, the credential
|
|
23
|
+
* store, or a real process clock. The `runStatusCommand` wrapper
|
|
24
|
+
* in `runtime/commands/status.ts` plugs in production deps.
|
|
25
|
+
*
|
|
26
|
+
* # Field semantics
|
|
27
|
+
*
|
|
28
|
+
* sessionId / sessionAgeMs : taken verbatim from caller when in
|
|
29
|
+
* REPL mode (the slash dispatcher passes the live id +
|
|
30
|
+
* `briefStartedAtEpochMs`); else best-effort from the on-disk
|
|
31
|
+
* `.pugi/events.jsonl` mtime, OR null when no prior dispatch is
|
|
32
|
+
* logged in the workspace.
|
|
33
|
+
*
|
|
34
|
+
* cwd : pure function of the caller's `ctx.cwd`. Never null.
|
|
35
|
+
*
|
|
36
|
+
* permissionMode : reads the optional permissions state module
|
|
37
|
+
* when available (L6 in the leak-parity roadmap). On any
|
|
38
|
+
* import failure the field degrades to "unknown" so this
|
|
39
|
+
* command lands before L6 without a compile-time coupling.
|
|
40
|
+
*
|
|
41
|
+
* cliVersion : injected literal so the build's PUGI_CLI_VERSION
|
|
42
|
+
* constant is the single source of truth.
|
|
43
|
+
*
|
|
44
|
+
* tokensUsed : injected from the caller — REPL mode passes the
|
|
45
|
+
* live `sessionTokensIn + sessionTokensOut` total; the
|
|
46
|
+
* top-level `pugi status` invocation has no REPL state so
|
|
47
|
+
* this field stays null and the renderer prints "n/a".
|
|
48
|
+
*
|
|
49
|
+
* activeDispatches / completedDispatches : counted by scanning
|
|
50
|
+
* `.pugi/agent-progress/*.json` and classifying by the
|
|
51
|
+
* `status` field. Missing dir → 0/0 (no crash).
|
|
52
|
+
*
|
|
53
|
+
* lastCommand : injected from the REPL when available; null on
|
|
54
|
+
* the top-level shell path so the renderer prints "n/a".
|
|
55
|
+
*
|
|
56
|
+
* compactBoundaries : counts lines in `.pugi/events.jsonl`
|
|
57
|
+
* where the event `kind === 'compact.boundary'`. Missing file
|
|
58
|
+
* → 0. Wired against Memory Phase 0 NDJSON; downgrade to 0 if
|
|
59
|
+
* the file is malformed.
|
|
60
|
+
*
|
|
61
|
+
* auth : resolved via the same credential helper `pugi whoami`
|
|
62
|
+
* uses. Returns either an `{ apiUrl, apiKey, label?, tier? }`
|
|
63
|
+
* summary or null when the operator is not authenticated. We
|
|
64
|
+
* do NOT make a network call here — the tier is read from
|
|
65
|
+
* local creds metadata; live tier lookup belongs to
|
|
66
|
+
* `pugi whoami --remote` (separate command).
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Collect the full snapshot. The function is synchronous because
|
|
70
|
+
* every field source is local (process state, fs, injected
|
|
71
|
+
* resolvers) — no network calls.
|
|
72
|
+
*/
|
|
73
|
+
export function collectStatusSnapshot(deps) {
|
|
74
|
+
const fields = [];
|
|
75
|
+
// Session id + age. REPL-mode caller passes both; top-level
|
|
76
|
+
// shell path passes neither и we fall back to the on-disk NDJSON
|
|
77
|
+
// tail mtime so the operator still sees a useful age figure for
|
|
78
|
+
// the most-recent workspace activity.
|
|
79
|
+
fields.push(buildSessionField(deps));
|
|
80
|
+
// Working directory. Pure function of caller's ctx — always
|
|
81
|
+
// available (sentinel only if caller passed an empty string,
|
|
82
|
+
// which the renderer treats as "unknown").
|
|
83
|
+
fields.push({
|
|
84
|
+
key: 'workdir',
|
|
85
|
+
label: 'Workdir',
|
|
86
|
+
value: deps.cwd.length > 0 ? deps.cwd : 'unknown',
|
|
87
|
+
available: deps.cwd.length > 0,
|
|
88
|
+
});
|
|
89
|
+
// Permission mode. Fail-soft — degrades к "unknown" until L6
|
|
90
|
+
// lands the permissions/state module.
|
|
91
|
+
fields.push(buildPermissionModeField(deps));
|
|
92
|
+
// Pugi CLI version. The build-time constant is the single
|
|
93
|
+
// source of truth; sanitised upstream (sanitizeSemver in
|
|
94
|
+
// runtime/version.ts) so we never see `workspace:*` here.
|
|
95
|
+
fields.push({
|
|
96
|
+
key: 'cli',
|
|
97
|
+
label: 'CLI',
|
|
98
|
+
value: deps.cliVersion,
|
|
99
|
+
available: deps.cliVersion !== '0.0.0-unknown',
|
|
100
|
+
});
|
|
101
|
+
// Token usage. REPL caller passes the live total; shell path
|
|
102
|
+
// degrades к "n/a" (no REPL state in a fresh process).
|
|
103
|
+
fields.push(buildTokenField(deps));
|
|
104
|
+
// Active + completed dispatches scanned from
|
|
105
|
+
// `.pugi/agent-progress/*.json`. Missing dir → 0 active, 0
|
|
106
|
+
// completed (no crash).
|
|
107
|
+
fields.push(buildDispatchField(deps));
|
|
108
|
+
// Last command. REPL caller passes the most-recent operator
|
|
109
|
+
// line + its timestamp; shell path degrades к "n/a".
|
|
110
|
+
fields.push(buildLastCommandField(deps));
|
|
111
|
+
// Compact boundary marker count. Read from
|
|
112
|
+
// `.pugi/events.jsonl` per Memory Phase 0; missing file → 0.
|
|
113
|
+
fields.push(buildCompactField(deps));
|
|
114
|
+
// Auth identity. Read from the credential resolver — local-only
|
|
115
|
+
// (no network call); the live tier comes from the stored
|
|
116
|
+
// credential metadata.
|
|
117
|
+
fields.push(buildAuthField(deps));
|
|
118
|
+
// Connection mode. Today simply mirrors the credential source
|
|
119
|
+
// (env vs file) so the operator can see at a glance whether
|
|
120
|
+
// they are in CI-env mode or a logged-in shell. Future wiring
|
|
121
|
+
// overlays the REPL's live connection state when invoked from
|
|
122
|
+
// the slash dispatcher.
|
|
123
|
+
fields.push(buildConnectionField(deps));
|
|
124
|
+
return {
|
|
125
|
+
command: 'status',
|
|
126
|
+
fields,
|
|
127
|
+
meta: {
|
|
128
|
+
cliVersion: deps.cliVersion,
|
|
129
|
+
nodeVersion: process.version,
|
|
130
|
+
cwd: deps.cwd,
|
|
131
|
+
capturedAt: new Date(deps.now()).toISOString(),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/* -------------------------- field builders -------------------------- */
|
|
136
|
+
function buildSessionField(deps) {
|
|
137
|
+
if (deps.liveSessionId && deps.liveSessionId.length > 0) {
|
|
138
|
+
const ageMs = typeof deps.sessionStartedAtEpochMs === 'number' && deps.sessionStartedAtEpochMs > 0
|
|
139
|
+
? Math.max(0, deps.now() - deps.sessionStartedAtEpochMs)
|
|
140
|
+
: null;
|
|
141
|
+
const ageLabel = ageMs === null ? '' : ` (${formatAgeShort(ageMs)})`;
|
|
142
|
+
return {
|
|
143
|
+
key: 'session',
|
|
144
|
+
label: 'Session',
|
|
145
|
+
value: `${shortId(deps.liveSessionId)}${ageLabel}`,
|
|
146
|
+
available: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Top-level shell path: best-effort tail of .pugi/events.jsonl.
|
|
150
|
+
const eventsPath = `${deps.cwd}/.pugi/events.jsonl`;
|
|
151
|
+
try {
|
|
152
|
+
if (!deps.fs.existsSync(eventsPath)) {
|
|
153
|
+
return {
|
|
154
|
+
key: 'session',
|
|
155
|
+
label: 'Session',
|
|
156
|
+
value: 'unbound (no prior dispatch in this workspace)',
|
|
157
|
+
available: false,
|
|
158
|
+
note: '.pugi/events.jsonl not found',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const stats = deps.fs.statSync(eventsPath);
|
|
162
|
+
const ageMs = Math.max(0, deps.now() - stats.mtimeMs);
|
|
163
|
+
return {
|
|
164
|
+
key: 'session',
|
|
165
|
+
label: 'Session',
|
|
166
|
+
value: `unbound · last activity ${formatAgeShort(ageMs)} ago`,
|
|
167
|
+
// Shell path never has a live id — flag as unavailable so the
|
|
168
|
+
// renderer can dim the row.
|
|
169
|
+
available: false,
|
|
170
|
+
note: 'shell-mode: showing last NDJSON activity instead of a live id',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
key: 'session',
|
|
176
|
+
label: 'Session',
|
|
177
|
+
value: 'unknown',
|
|
178
|
+
available: false,
|
|
179
|
+
note: errorNote(error),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function buildPermissionModeField(deps) {
|
|
184
|
+
try {
|
|
185
|
+
const mode = deps.resolvePermissionMode();
|
|
186
|
+
if (mode && mode.length > 0) {
|
|
187
|
+
return {
|
|
188
|
+
key: 'mode',
|
|
189
|
+
label: 'Mode',
|
|
190
|
+
value: mode,
|
|
191
|
+
available: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
key: 'mode',
|
|
196
|
+
label: 'Mode',
|
|
197
|
+
value: 'unknown',
|
|
198
|
+
available: false,
|
|
199
|
+
note: 'permission state module not yet present (lands with L6)',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
key: 'mode',
|
|
205
|
+
label: 'Mode',
|
|
206
|
+
value: 'unknown',
|
|
207
|
+
available: false,
|
|
208
|
+
note: errorNote(error),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function buildTokenField(deps) {
|
|
213
|
+
if (typeof deps.liveTokensUsed === 'number' && deps.liveTokensUsed >= 0) {
|
|
214
|
+
return {
|
|
215
|
+
key: 'tokens',
|
|
216
|
+
label: 'Tokens',
|
|
217
|
+
value: `~${formatThousands(deps.liveTokensUsed)} used this session`,
|
|
218
|
+
available: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
key: 'tokens',
|
|
223
|
+
label: 'Tokens',
|
|
224
|
+
value: 'n/a (REPL not active)',
|
|
225
|
+
available: false,
|
|
226
|
+
note: 'token counter only available inside a live REPL session',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function buildDispatchField(deps) {
|
|
230
|
+
const dir = `${deps.cwd}/.pugi/agent-progress`;
|
|
231
|
+
try {
|
|
232
|
+
if (!deps.fs.existsSync(dir)) {
|
|
233
|
+
return {
|
|
234
|
+
key: 'dispatches',
|
|
235
|
+
label: 'Dispatches',
|
|
236
|
+
value: '0 active, 0 completed',
|
|
237
|
+
available: false,
|
|
238
|
+
note: '.pugi/agent-progress/ not present',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const entries = deps.fs.readdirSync(dir);
|
|
242
|
+
let active = 0;
|
|
243
|
+
let completed = 0;
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
if (!entry.endsWith('.json'))
|
|
246
|
+
continue;
|
|
247
|
+
try {
|
|
248
|
+
const raw = deps.fs.readFileSync(`${dir}/${entry}`, 'utf8');
|
|
249
|
+
const parsed = JSON.parse(raw);
|
|
250
|
+
const status = typeof parsed.status === 'string' ? parsed.status.toLowerCase() : null;
|
|
251
|
+
const completedAt = typeof parsed.completedAt === 'string' ? parsed.completedAt : null;
|
|
252
|
+
if (status === 'complete' || status === 'completed' || completedAt) {
|
|
253
|
+
completed += 1;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
active += 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Malformed progress JSON shouldn't crash the snapshot.
|
|
261
|
+
// Count it as active so the operator sees the row + can
|
|
262
|
+
// open the file manually.
|
|
263
|
+
active += 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
key: 'dispatches',
|
|
268
|
+
label: 'Dispatches',
|
|
269
|
+
value: `${active} active, ${completed} completed`,
|
|
270
|
+
available: true,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
key: 'dispatches',
|
|
276
|
+
label: 'Dispatches',
|
|
277
|
+
value: '0 active, 0 completed',
|
|
278
|
+
available: false,
|
|
279
|
+
note: errorNote(error),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function buildLastCommandField(deps) {
|
|
284
|
+
if (typeof deps.lastCommand === 'string' && deps.lastCommand.trim().length > 0) {
|
|
285
|
+
const cmd = truncate(deps.lastCommand.trim(), 60);
|
|
286
|
+
const agoLabel = typeof deps.lastCommandAtEpochMs === 'number' && deps.lastCommandAtEpochMs > 0
|
|
287
|
+
? ` (${formatAgeShort(Math.max(0, deps.now() - deps.lastCommandAtEpochMs))} ago)`
|
|
288
|
+
: '';
|
|
289
|
+
return {
|
|
290
|
+
key: 'lastCommand',
|
|
291
|
+
label: 'Last cmd',
|
|
292
|
+
value: `${cmd}${agoLabel}`,
|
|
293
|
+
available: true,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
key: 'lastCommand',
|
|
298
|
+
label: 'Last cmd',
|
|
299
|
+
value: 'n/a',
|
|
300
|
+
available: false,
|
|
301
|
+
note: 'no command observed in this session',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function buildCompactField(deps) {
|
|
305
|
+
const eventsPath = `${deps.cwd}/.pugi/events.jsonl`;
|
|
306
|
+
try {
|
|
307
|
+
if (!deps.fs.existsSync(eventsPath)) {
|
|
308
|
+
return {
|
|
309
|
+
key: 'compact',
|
|
310
|
+
label: 'Compact',
|
|
311
|
+
value: '0 boundary markers',
|
|
312
|
+
available: false,
|
|
313
|
+
note: '.pugi/events.jsonl not present',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const raw = deps.fs.readFileSync(eventsPath, 'utf8');
|
|
317
|
+
let count = 0;
|
|
318
|
+
for (const line of raw.split('\n')) {
|
|
319
|
+
const trimmed = line.trim();
|
|
320
|
+
if (trimmed.length === 0)
|
|
321
|
+
continue;
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(trimmed);
|
|
324
|
+
const kind = typeof parsed.kind === 'string' ? parsed.kind : null;
|
|
325
|
+
const type = typeof parsed.type === 'string' ? parsed.type : null;
|
|
326
|
+
if (kind === 'compact.boundary' || type === 'compact.boundary') {
|
|
327
|
+
count += 1;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// Malformed line in NDJSON is not a snapshot failure.
|
|
332
|
+
// Skip and keep counting.
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
key: 'compact',
|
|
337
|
+
label: 'Compact',
|
|
338
|
+
value: `${count} boundary marker${count === 1 ? '' : 's'}`,
|
|
339
|
+
available: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
return {
|
|
344
|
+
key: 'compact',
|
|
345
|
+
label: 'Compact',
|
|
346
|
+
value: '0 boundary markers',
|
|
347
|
+
available: false,
|
|
348
|
+
note: errorNote(error),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function buildAuthField(deps) {
|
|
353
|
+
try {
|
|
354
|
+
const cred = deps.resolveCredential();
|
|
355
|
+
if (!cred) {
|
|
356
|
+
return {
|
|
357
|
+
key: 'auth',
|
|
358
|
+
label: 'Auth',
|
|
359
|
+
value: 'not signed in',
|
|
360
|
+
available: false,
|
|
361
|
+
note: 'run `pugi login` к authenticate',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const identity = cred.identity ?? cred.label ?? cred.apiUrl;
|
|
365
|
+
const tier = cred.tier ? ` (tier: ${cred.tier})` : '';
|
|
366
|
+
return {
|
|
367
|
+
key: 'auth',
|
|
368
|
+
label: 'Auth',
|
|
369
|
+
value: `${identity}${tier}`,
|
|
370
|
+
available: true,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
return {
|
|
375
|
+
key: 'auth',
|
|
376
|
+
label: 'Auth',
|
|
377
|
+
value: 'unknown',
|
|
378
|
+
available: false,
|
|
379
|
+
note: errorNote(error),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function buildConnectionField(deps) {
|
|
384
|
+
try {
|
|
385
|
+
const cred = deps.resolveCredential();
|
|
386
|
+
if (!cred) {
|
|
387
|
+
return {
|
|
388
|
+
key: 'connection',
|
|
389
|
+
label: 'Connection',
|
|
390
|
+
value: 'offline (no credential)',
|
|
391
|
+
available: false,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
key: 'connection',
|
|
396
|
+
label: 'Connection',
|
|
397
|
+
value: cred.apiUrl,
|
|
398
|
+
available: true,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return {
|
|
403
|
+
key: 'connection',
|
|
404
|
+
label: 'Connection',
|
|
405
|
+
value: 'unknown',
|
|
406
|
+
available: false,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/* ---------------------------- formatters ---------------------------- */
|
|
411
|
+
export function formatAgeShort(ms) {
|
|
412
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
413
|
+
return '0s';
|
|
414
|
+
if (ms < 60_000)
|
|
415
|
+
return `${Math.round(ms / 1000)} sec`;
|
|
416
|
+
if (ms < 3_600_000)
|
|
417
|
+
return `${Math.round(ms / 60_000)} min`;
|
|
418
|
+
if (ms < 86_400_000)
|
|
419
|
+
return `${Math.round(ms / 3_600_000)} hr`;
|
|
420
|
+
return `${Math.round(ms / 86_400_000)} day`;
|
|
421
|
+
}
|
|
422
|
+
export function formatThousands(value) {
|
|
423
|
+
if (!Number.isFinite(value) || value < 0)
|
|
424
|
+
return '0';
|
|
425
|
+
return Math.round(value).toLocaleString('en-US');
|
|
426
|
+
}
|
|
427
|
+
export function shortId(id) {
|
|
428
|
+
// The full ULID / UUID is awkward in a table cell. Keep the
|
|
429
|
+
// first 13 chars (long enough к stay collision-free across the
|
|
430
|
+
// recent-sessions list, short enough к share at a glance).
|
|
431
|
+
return id.length > 13 ? id.slice(0, 13) : id;
|
|
432
|
+
}
|
|
433
|
+
export function truncate(text, max) {
|
|
434
|
+
if (text.length <= max)
|
|
435
|
+
return text;
|
|
436
|
+
return `${text.slice(0, Math.max(1, max - 1))}…`;
|
|
437
|
+
}
|
|
438
|
+
function errorNote(error) {
|
|
439
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
440
|
+
return `field unavailable: ${message}`;
|
|
441
|
+
}
|
|
442
|
+
//# sourceMappingURL=status-snapshot.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WORKSPACE probe — verifies `.pugi/` exists, is writable, and is not
|
|
3
|
+
* littered with stale lock files. Optional NDJSON session log presence
|
|
4
|
+
* is reported as additional context but never the basis for a verdict
|
|
5
|
+
* change (it is created on first dispatch, not at init time).
|
|
6
|
+
*
|
|
7
|
+
* The probe owns its fs surface so the spec can run in a tmp sandbox.
|
|
8
|
+
*/
|
|
9
|
+
export function probeWorkspace(ctx, fs) {
|
|
10
|
+
const pugiDir = `${ctx.cwd}/.pugi`;
|
|
11
|
+
if (!fs.existsSync(pugiDir)) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'WORKSPACE',
|
|
14
|
+
status: 'warn',
|
|
15
|
+
detail: `.pugi/ not initialised in ${ctx.cwd}`,
|
|
16
|
+
remediation: 'Run `pugi init` to scaffold the workspace',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
let isDir = false;
|
|
20
|
+
try {
|
|
21
|
+
isDir = fs.statSync(pugiDir).isDirectory();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {
|
|
25
|
+
name: 'WORKSPACE',
|
|
26
|
+
status: 'error',
|
|
27
|
+
detail: `.pugi/ stat failed in ${ctx.cwd}`,
|
|
28
|
+
remediation: 'Re-create the directory: `rm -rf .pugi && pugi init`',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (!isDir) {
|
|
32
|
+
return {
|
|
33
|
+
name: 'WORKSPACE',
|
|
34
|
+
status: 'error',
|
|
35
|
+
detail: `${pugiDir} exists but is not a directory`,
|
|
36
|
+
remediation: 'Remove the file at .pugi and run `pugi init`',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
fs.accessSync(pugiDir, fs.W_OK);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return {
|
|
44
|
+
name: 'WORKSPACE',
|
|
45
|
+
status: 'error',
|
|
46
|
+
detail: `.pugi/ is not writable for the current user`,
|
|
47
|
+
remediation: `chown / chmod the directory so the current user can write it`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Best-effort: report session log presence as detail context. Absence
|
|
51
|
+
// is normal (events.jsonl is created lazily) so it never moves the
|
|
52
|
+
// verdict.
|
|
53
|
+
const eventLogPresent = fs.existsSync(`${pugiDir}/events.jsonl`);
|
|
54
|
+
const detail = eventLogPresent
|
|
55
|
+
? `.pugi/ writable, events.jsonl present`
|
|
56
|
+
: `.pugi/ writable (events.jsonl created on first dispatch)`;
|
|
57
|
+
return {
|
|
58
|
+
name: 'WORKSPACE',
|
|
59
|
+
status: 'ok',
|
|
60
|
+
detail,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leak L17 — `pugi doctor` diagnostics types.
|
|
3
|
+
*
|
|
4
|
+
* The doctor command probes the local environment + remote API +
|
|
5
|
+
* workspace state and produces a structured health report. Each probe
|
|
6
|
+
* runs independently; one probe's failure NEVER cascades to another.
|
|
7
|
+
*
|
|
8
|
+
* Status semantics:
|
|
9
|
+
* - `ok` : probe verified the expected state.
|
|
10
|
+
* - `warn` : non-blocking signal (stale CLI, low-but-not-empty disk,
|
|
11
|
+
* missing optional config, etc.). Overall verdict still
|
|
12
|
+
* passes the gate.
|
|
13
|
+
* - `error` : a real problem the operator must fix before Pugi will
|
|
14
|
+
* work end-to-end (auth missing, API unreachable, .pugi/
|
|
15
|
+
* unwritable, Node version below floor, disk full).
|
|
16
|
+
* - `skipped` : prerequisite for the probe is absent (e.g. MCP probe
|
|
17
|
+
* when no mcp.json exists). Does NOT count against the
|
|
18
|
+
* overall verdict.
|
|
19
|
+
*
|
|
20
|
+
* Layered design: this module owns NO I/O. Individual probe files own
|
|
21
|
+
* their I/O surface. The runner orchestrates them in parallel with a
|
|
22
|
+
* timeout + fail-isolation wrapper. The doctor command formats the
|
|
23
|
+
* results for human + JSON consumers.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Helper for the runner to compute the overall verdict from a probe
|
|
27
|
+
* set without leaking the algorithm into the doctor command. Any error
|
|
28
|
+
* → 'error'; any warn (no errors) → 'warning'; otherwise 'healthy'.
|
|
29
|
+
* Skipped probes do NOT influence the verdict.
|
|
30
|
+
*/
|
|
31
|
+
export function computeOverall(probes) {
|
|
32
|
+
let hasError = false;
|
|
33
|
+
let hasWarn = false;
|
|
34
|
+
for (const probe of probes) {
|
|
35
|
+
if (probe.status === 'error')
|
|
36
|
+
hasError = true;
|
|
37
|
+
else if (probe.status === 'warn')
|
|
38
|
+
hasWarn = true;
|
|
39
|
+
}
|
|
40
|
+
if (hasError)
|
|
41
|
+
return 'error';
|
|
42
|
+
if (hasWarn)
|
|
43
|
+
return 'warning';
|
|
44
|
+
return 'healthy';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compute the per-status counts in a single pass so renderers do not
|
|
48
|
+
* have to re-iterate. Surfaces in both the trailer line and the JSON
|
|
49
|
+
* envelope so downstream consumers can render a one-line summary
|
|
50
|
+
* without re-walking the probe array.
|
|
51
|
+
*/
|
|
52
|
+
export function countProbes(probes) {
|
|
53
|
+
const counts = { ok: 0, warn: 0, error: 0, skipped: 0 };
|
|
54
|
+
for (const probe of probes)
|
|
55
|
+
counts[probe.status] += 1;
|
|
56
|
+
return counts;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Exit-code map. Exposed for both the CLI handler and the spec so the
|
|
60
|
+
* contract stays in one place.
|
|
61
|
+
* 0 — healthy OR warnings only.
|
|
62
|
+
* 1 — internal crash (unhandled throw in the runner itself).
|
|
63
|
+
* 2 — at least one probe reported `error`.
|
|
64
|
+
*/
|
|
65
|
+
export function exitCodeFor(overall) {
|
|
66
|
+
if (overall === 'error')
|
|
67
|
+
return 2;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -17,6 +17,10 @@ import { CancellationToken } from '../repl/cancellation.js';
|
|
|
17
17
|
import { buildContextPrefix, spliceContextPrefix } from './context-prefix.js';
|
|
18
18
|
import { applyIntentMarker, classifyIntent } from './intent.js';
|
|
19
19
|
import { loadTraversedMarkdown } from '../context/markdown-traverse.js';
|
|
20
|
+
// α7 L11 (2026-05-27): per-session DenialTrackingState. One instance
|
|
21
|
+
// per `run()` so denials cluster by (tool, args) within the same
|
|
22
|
+
// command but do NOT leak across CLI invocations.
|
|
23
|
+
import { DenialTrackingState } from '../denial-tracking/state.js';
|
|
20
24
|
/**
|
|
21
25
|
* Real `NativePugiEngineAdapter`. Drives the Pugi CLI's tool-use loop:
|
|
22
26
|
*
|
|
@@ -157,6 +161,15 @@ export class NativePugiEngineAdapter {
|
|
|
157
161
|
readCache: new FileReadCache(),
|
|
158
162
|
cancellation,
|
|
159
163
|
};
|
|
164
|
+
// α7 L11 (2026-05-27): instantiate per-`run()` denial tracker. The
|
|
165
|
+
// executor records every refusal (PLAN_MODE_REFUSED, HOOK_BLOCKED,
|
|
166
|
+
// OPERATOR_ABORTED, STALE_READ, unknown-tool, plan-mode agent) and
|
|
167
|
+
// the user-prompt assembler below splices a compact reminder when
|
|
168
|
+
// the same (tool, args) pair has been denied twice or more. The
|
|
169
|
+
// tracker is in-memory only — the audit ledger at
|
|
170
|
+
// `.pugi/events.jsonl` already captures the full per-event log for
|
|
171
|
+
// forensic replay; this surface is the model-facing aggregate.
|
|
172
|
+
const denialTracking = new DenialTrackingState();
|
|
160
173
|
// β1a r1 (budget wiring, 2026-05-26): swap the legacy SDK per-
|
|
161
174
|
// command budget lookup for the Pl9 `resolveBudget()` pipeline so
|
|
162
175
|
// `.pugi/settings.json::budgets.<command>` overrides actually take
|
|
@@ -516,6 +529,13 @@ export class NativePugiEngineAdapter {
|
|
|
516
529
|
// first-call permission prompt before dispatching upstream.
|
|
517
530
|
...(this.options.mcpRegistry ? { mcpRegistry: this.options.mcpRegistry } : {}),
|
|
518
531
|
...(this.options.mcpPrompt ? { mcpPrompt: this.options.mcpPrompt } : {}),
|
|
532
|
+
// α7 L11 (2026-05-27): per-`run()` denial tracker. Every
|
|
533
|
+
// refusal sentinel (PLAN_MODE_REFUSED, HOOK_BLOCKED,
|
|
534
|
+
// OPERATOR_ABORTED, STALE_READ, unknown-tool, plan-mode
|
|
535
|
+
// agent) is fingerprinted by (toolName, sha256(canonical
|
|
536
|
+
// args)) so the model's next-turn reminder surfaces the
|
|
537
|
+
// pattern instead of re-issuing the same refused call.
|
|
538
|
+
denialTracking,
|
|
519
539
|
}),
|
|
520
540
|
systemPrompt: systemPromptFor(kind),
|
|
521
541
|
// β5a R5+R6+P1: per-turn `<context>` prefix + intent marker
|