@pugi/cli 0.1.0-beta.24 → 0.1.0-beta.26
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/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/compact/summarizer.js +12 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/engine/native-pugi.js +67 -3
- package/dist/core/engine/tool-bridge.js +123 -3
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/memory-sync/queue.spec.js +105 -0
- package/dist/core/repl/session.js +73 -1
- package/dist/core/repl/slash-commands.js +20 -0
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/session.js +44 -0
- package/dist/core/settings.js +9 -0
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/runtime/cli.js +216 -0
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +25 -23
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/memory.spec.js +174 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tools/agent-tool.js +23 -0
- package/package.json +2 -2
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry batching queue — Wave 6 BIG TRACK 11 (PR-PUGI-OBSERVABILITY-STACK).
|
|
3
|
+
*
|
|
4
|
+
* The emitter (see `emitter.ts`) appends events to an in-memory buffer
|
|
5
|
+
* and a JSONL spill file. The queue's two-tier strategy:
|
|
6
|
+
*
|
|
7
|
+
* 1. In-memory buffer (`MAX_BUFFER`) → flushed every `FLUSH_INTERVAL_MS`
|
|
8
|
+
* OR on REPL exit OR when the buffer hits the cap.
|
|
9
|
+
* 2. JSONL spill (`<repoRoot>/.pugi/telemetry-queue.jsonl`) → drained
|
|
10
|
+
* on every flush attempt. Used when the in-memory buffer cannot
|
|
11
|
+
* reach the network (offline laptop, admin-api down).
|
|
12
|
+
*
|
|
13
|
+
* Failure semantics mirror the `feedback/queue.ts` pattern that landed
|
|
14
|
+
* in L21:
|
|
15
|
+
*
|
|
16
|
+
* - 200/201/204 → success, drop from spill
|
|
17
|
+
* - 404 → endpoint not deployed yet — keep
|
|
18
|
+
* - 5xx / network / abort → transient — keep + exponential backoff
|
|
19
|
+
* - other 4xx → permanent — drop (otherwise loop forever)
|
|
20
|
+
*
|
|
21
|
+
* The queue is intentionally simple: there are no concurrency primitives
|
|
22
|
+
* beyond filesystem `O_APPEND`. The CLI is single-process per REPL
|
|
23
|
+
* session; the JSONL spill survives a crash because every append is
|
|
24
|
+
* atomic at the OS level for line-sized writes on POSIX (Linux & macOS).
|
|
25
|
+
*
|
|
26
|
+
* Privacy:
|
|
27
|
+
*
|
|
28
|
+
* - Events drop into the queue only when telemetry consent ≠ `off`.
|
|
29
|
+
* The emitter consults `readTelemetryChoice()` before calling
|
|
30
|
+
* `enqueueTelemetry(...)`. This module does NOT re-check — keeping
|
|
31
|
+
* the consent gate at the emitter avoids double-decoding and
|
|
32
|
+
* centralises the audit point.
|
|
33
|
+
*
|
|
34
|
+
* - The spill file lives under `<repoRoot>/.pugi/` (workspace tier)
|
|
35
|
+
* so an operator who deletes the repo also wipes any unfortunate
|
|
36
|
+
* events that never made it to the server.
|
|
37
|
+
*/
|
|
38
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
39
|
+
import { dirname, resolve } from 'node:path';
|
|
40
|
+
import { randomUUID } from 'node:crypto';
|
|
41
|
+
import { PUGI_CLI_VERSION } from '../../runtime/version.js';
|
|
42
|
+
/** Defaults — tunable via env without redeploy. */
|
|
43
|
+
export const MAX_BUFFER = 50;
|
|
44
|
+
export const FLUSH_INTERVAL_MS = 15_000;
|
|
45
|
+
export const SPILL_FILE_NAME = 'telemetry-queue.jsonl';
|
|
46
|
+
/** Hard cap on the spill file (events, not bytes). Prevents pathologic
|
|
47
|
+
* growth on a laptop that is offline for weeks. */
|
|
48
|
+
export const SPILL_MAX_LINES = 5_000;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the absolute spill file path. Pure — exposed for spec parity.
|
|
51
|
+
*/
|
|
52
|
+
export function telemetryQueuePath(opts = {}) {
|
|
53
|
+
const root = opts.repoRoot ?? process.cwd();
|
|
54
|
+
const name = opts.spillFileName ?? SPILL_FILE_NAME;
|
|
55
|
+
return resolve(root, '.pugi', name);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Append one event to the on-disk spill. Atomic at the OS level for
|
|
59
|
+
* line-sized writes — multiple concurrent appenders never interleave
|
|
60
|
+
* half-records on POSIX. Caps the file at `SPILL_MAX_LINES` by silently
|
|
61
|
+
* dropping the OLDEST events (FIFO) on the rare overflow path.
|
|
62
|
+
*/
|
|
63
|
+
export function spillEvent(ev, opts = {}) {
|
|
64
|
+
const path = telemetryQueuePath(opts);
|
|
65
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
66
|
+
const line = `${JSON.stringify(ev)}\n`;
|
|
67
|
+
// Fast path: append-only. We only check the line count when the file
|
|
68
|
+
// already exists AND we suspect overflow. Reading + rewriting every
|
|
69
|
+
// append would dominate the cost.
|
|
70
|
+
if (existsSync(path)) {
|
|
71
|
+
const current = readFileSync(path, 'utf8');
|
|
72
|
+
const lineCount = countLines(current);
|
|
73
|
+
if (lineCount >= SPILL_MAX_LINES) {
|
|
74
|
+
// FIFO trim: keep the most-recent SPILL_MAX_LINES/2 events. The
|
|
75
|
+
// factor 2 amortises the rewrite across many appends.
|
|
76
|
+
const lines = current.split('\n').filter((l) => l.length > 0);
|
|
77
|
+
const keep = lines.slice(lines.length - Math.floor(SPILL_MAX_LINES / 2));
|
|
78
|
+
writeFileSync(path, `${keep.join('\n')}\n${line}`, 'utf8');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
appendFileSync(path, line, { encoding: 'utf8', mode: 0o600 });
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read + parse every spilled event. Returns the events plus a list of
|
|
86
|
+
* malformed lines (which are dropped silently — we never reject a
|
|
87
|
+
* parseable line just because an adjacent one is corrupt).
|
|
88
|
+
*/
|
|
89
|
+
export function readSpill(opts = {}) {
|
|
90
|
+
const path = telemetryQueuePath(opts);
|
|
91
|
+
if (!existsSync(path))
|
|
92
|
+
return { events: [], malformed: 0 };
|
|
93
|
+
const raw = readFileSync(path, 'utf8');
|
|
94
|
+
if (raw.length === 0)
|
|
95
|
+
return { events: [], malformed: 0 };
|
|
96
|
+
const events = [];
|
|
97
|
+
let malformed = 0;
|
|
98
|
+
for (const line of raw.split('\n')) {
|
|
99
|
+
if (line.length === 0)
|
|
100
|
+
continue;
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(line);
|
|
103
|
+
if (isTelemetryEvent(parsed)) {
|
|
104
|
+
events.push(parsed);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
malformed += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
malformed += 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { events, malformed };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Atomically rewrite the spill with the given events (the unsubmitted
|
|
118
|
+
* remainder after a partial-success flush). Writing through a sibling
|
|
119
|
+
* tempfile + rename keeps the spill consistent across a crash mid-flush.
|
|
120
|
+
*/
|
|
121
|
+
export function rewriteSpill(events, opts = {}) {
|
|
122
|
+
const path = telemetryQueuePath(opts);
|
|
123
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
124
|
+
if (events.length === 0) {
|
|
125
|
+
// Empty spill — write an empty file so the next read short-circuits.
|
|
126
|
+
writeFileSync(path, '', { encoding: 'utf8', mode: 0o600 });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const body = events.map((e) => JSON.stringify(e)).join('\n');
|
|
130
|
+
writeFileSync(path, `${body}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Type guard for inbound spill lines. Keeps the queue robust against a
|
|
134
|
+
* forward-incompatible event shape (e.g. a future version added a
|
|
135
|
+
* required field) — anything that fails the guard is treated as
|
|
136
|
+
* malformed and dropped on parse.
|
|
137
|
+
*/
|
|
138
|
+
export function isTelemetryEvent(value) {
|
|
139
|
+
if (!value || typeof value !== 'object')
|
|
140
|
+
return false;
|
|
141
|
+
const v = value;
|
|
142
|
+
return (typeof v.sessionId === 'string'
|
|
143
|
+
&& typeof v.cliVersion === 'string'
|
|
144
|
+
&& typeof v.command === 'string'
|
|
145
|
+
&& typeof v.kind === 'string'
|
|
146
|
+
&& typeof v.ts === 'string');
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Exponential-backoff schedule. Returns the next delay (in ms) given an
|
|
150
|
+
* attempt counter, capped at `MAX_BACKOFF_MS`. Pure — exposed for tests.
|
|
151
|
+
*
|
|
152
|
+
* attempt 0 → 1s
|
|
153
|
+
* attempt 1 → 2s
|
|
154
|
+
* attempt 2 → 4s
|
|
155
|
+
* attempt 5 → 32s
|
|
156
|
+
* attempt 7+ → 60s (cap)
|
|
157
|
+
*/
|
|
158
|
+
export const BACKOFF_BASE_MS = 1000;
|
|
159
|
+
export const MAX_BACKOFF_MS = 60_000;
|
|
160
|
+
export function backoffDelay(attempt) {
|
|
161
|
+
if (!Number.isFinite(attempt) || attempt < 0)
|
|
162
|
+
return BACKOFF_BASE_MS;
|
|
163
|
+
const exp = BACKOFF_BASE_MS * Math.pow(2, Math.floor(attempt));
|
|
164
|
+
return Math.min(MAX_BACKOFF_MS, exp);
|
|
165
|
+
}
|
|
166
|
+
const DEFAULT_FLUSH_TIMEOUT_MS = 8_000;
|
|
167
|
+
export function telemetryIngestUrl(apiUrl) {
|
|
168
|
+
const base = apiUrl.replace(/\/+$/u, '');
|
|
169
|
+
return `${base}/api/pugi/telemetry/event`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* POST one batch. Same result-variant contract as
|
|
173
|
+
* `feedback/submitter.submitFeedback`. Never throws.
|
|
174
|
+
*/
|
|
175
|
+
export async function postTelemetryBatch(events, config) {
|
|
176
|
+
if (events.length === 0) {
|
|
177
|
+
return { kind: 'ok', httpStatus: 204, accepted: 0, dropped: 0 };
|
|
178
|
+
}
|
|
179
|
+
const url = telemetryIngestUrl(config.apiUrl);
|
|
180
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
181
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_FLUSH_TIMEOUT_MS;
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
184
|
+
try {
|
|
185
|
+
const headers = {
|
|
186
|
+
'content-type': 'application/json',
|
|
187
|
+
'user-agent': `pugi-cli/${PUGI_CLI_VERSION}`,
|
|
188
|
+
};
|
|
189
|
+
if (config.apiKey)
|
|
190
|
+
headers['authorization'] = `Bearer ${config.apiKey}`;
|
|
191
|
+
const res = await fetchImpl(url, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers,
|
|
194
|
+
body: JSON.stringify({ events }),
|
|
195
|
+
signal: controller.signal,
|
|
196
|
+
});
|
|
197
|
+
const status = res.status;
|
|
198
|
+
if (status >= 200 && status < 300) {
|
|
199
|
+
let accepted = events.length;
|
|
200
|
+
let dropped = 0;
|
|
201
|
+
try {
|
|
202
|
+
const body = (await res.json());
|
|
203
|
+
if (typeof body.accepted === 'number')
|
|
204
|
+
accepted = body.accepted;
|
|
205
|
+
if (typeof body.dropped === 'number')
|
|
206
|
+
dropped = body.dropped;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Body absent / not JSON — server still acked 2xx, treat as full success.
|
|
210
|
+
}
|
|
211
|
+
return { kind: 'ok', httpStatus: status, accepted, dropped };
|
|
212
|
+
}
|
|
213
|
+
if (status === 404) {
|
|
214
|
+
return {
|
|
215
|
+
kind: 'transient',
|
|
216
|
+
reason: 'admin-api /api/pugi/telemetry/event not deployed yet',
|
|
217
|
+
httpStatus: status,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (status >= 500) {
|
|
221
|
+
return { kind: 'transient', reason: `server error ${status}`, httpStatus: status };
|
|
222
|
+
}
|
|
223
|
+
return { kind: 'permanent', reason: `client error ${status}`, httpStatus: status };
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
227
|
+
return { kind: 'transient', reason: `network: ${message}` };
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------
|
|
234
|
+
// Helpers
|
|
235
|
+
// ---------------------------------------------------------------------
|
|
236
|
+
function countLines(s) {
|
|
237
|
+
let n = 0;
|
|
238
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
239
|
+
if (s.charCodeAt(i) === 10)
|
|
240
|
+
n += 1;
|
|
241
|
+
}
|
|
242
|
+
return n;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Generate a session id for the REPL boot. UUID v4 — short enough to
|
|
246
|
+
* grep, long enough to be globally unique across concurrent processes.
|
|
247
|
+
*/
|
|
248
|
+
export function newSessionId() {
|
|
249
|
+
return randomUUID();
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=queue.js.map
|
package/dist/runtime/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { buildRuntimeConfig, fetchPersonaRoster, loadRuntimeConfig, openPugiSess
|
|
|
22
22
|
import { PUGI_TAGLINE } from '@pugi/personas';
|
|
23
23
|
import { resolveRoster, renderRosterTable } from './commands/roster.js';
|
|
24
24
|
import { runDelegateCommand } from './commands/delegate.js';
|
|
25
|
+
import { runDispatchCommand } from './commands/dispatch.js';
|
|
25
26
|
import { clearApiKey, DEFAULT_API_URL, listStoredCredentials, maskApiKey, normalizeApiUrl, purgeAllCredentials, readCredentialsFile, resolveActiveCredential, storeApiKey, switchActiveAccount, } from '../core/credentials.js';
|
|
26
27
|
import { resolveAndValidateEnvLogin, } from '../core/auth/env-provider.js';
|
|
27
28
|
import { runDeployCommand } from '../commands/deploy.js';
|
|
@@ -37,14 +38,22 @@ import { runReport } from './commands/report.js';
|
|
|
37
38
|
import { runDoctorCommand, defaultHome as defaultDoctorHome } from './commands/doctor.js';
|
|
38
39
|
import { runStatusCommand, defaultStatusHome, } from './commands/status.js';
|
|
39
40
|
import { runStickersCommand } from './commands/stickers.js';
|
|
41
|
+
import { runRepoMapCommand } from './commands/repo-map.js';
|
|
40
42
|
import { runReleaseNotesCommand, defaultReleaseNotesHome, } from './commands/release-notes.js';
|
|
41
43
|
import { runUndoCommand } from './commands/undo.js';
|
|
42
44
|
import { runCompactCommand } from './commands/compact.js';
|
|
45
|
+
import { runRewindCommand } from './commands/rewind.js';
|
|
46
|
+
import { runSessionsCommand } from './commands/sessions.js';
|
|
47
|
+
// Day 4 ADR-0063: persona-memory operator surface (list / recall / write /
|
|
48
|
+
// forget / sync). The runner is shared by `pugi memory` top-level and the
|
|
49
|
+
// in-REPL `/memory` slash so the two surfaces stay single-sourced.
|
|
50
|
+
import { runMemoryCommand } from './commands/memory.js';
|
|
43
51
|
import { runBudgetCommand } from './commands/budget.js';
|
|
44
52
|
import { BARE_MODE_BANNER, isBareMode, setBareMode, } from '../core/bare-mode/index.js';
|
|
45
53
|
import { runCostCommand } from './commands/cost.js';
|
|
46
54
|
import { runShareCommand } from './commands/share.js';
|
|
47
55
|
import { runSkillsCommand } from './commands/skills.js';
|
|
56
|
+
import { runHooksCommand } from './commands/hooks.js';
|
|
48
57
|
import { installDefaultSkills } from '../core/skills/defaults.js';
|
|
49
58
|
import { runAgentsCommand } from './commands/agents.js';
|
|
50
59
|
import { runLspCommand } from './commands/lsp.js';
|
|
@@ -90,9 +99,15 @@ const handlers = {
|
|
|
90
99
|
config: dispatchConfig,
|
|
91
100
|
cost: dispatchCost,
|
|
92
101
|
delegate: dispatchDelegate,
|
|
102
|
+
// Leak L10 (2026-05-27): `pugi dispatch list-cache-refs` /
|
|
103
|
+
// `clear-cache-refs` operate on `.pugi/cache-refs/` — the persisted
|
|
104
|
+
// prompt-cache inheritance handles for fork-subagent dispatches. The
|
|
105
|
+
// handler module lives in commands/dispatch.ts so the table stays narrow.
|
|
106
|
+
dispatch: dispatchSubagentCacheRefs,
|
|
93
107
|
deploy: dispatchDeploy,
|
|
94
108
|
doctor,
|
|
95
109
|
explain: runEngineTask('explain'),
|
|
110
|
+
hooks: dispatchHooks,
|
|
96
111
|
fix: runEngineTask('fix'),
|
|
97
112
|
handoff,
|
|
98
113
|
help,
|
|
@@ -103,6 +118,10 @@ const handlers = {
|
|
|
103
118
|
logout,
|
|
104
119
|
lsp: dispatchLsp,
|
|
105
120
|
mcp: dispatchMcp,
|
|
121
|
+
// ADR-0063 Day 4: `pugi memory list|recall|write|forget|sync`. Routes
|
|
122
|
+
// to `runMemoryCommand` (admin-api `/api/persona-memory` + offline
|
|
123
|
+
// queue at `~/.pugi/memory-queue.jsonl`).
|
|
124
|
+
memory: dispatchMemory,
|
|
106
125
|
patch: dispatchPatch,
|
|
107
126
|
permissions: dispatchPermissions,
|
|
108
127
|
perms: dispatchPermissions,
|
|
@@ -127,6 +146,14 @@ const handlers = {
|
|
|
127
146
|
skills: dispatchSkills,
|
|
128
147
|
status,
|
|
129
148
|
stickers,
|
|
149
|
+
// Leak L28 (2026-05-27): `pugi repo-map` walks the source tree,
|
|
150
|
+
// extracts top-level function / class / interface / type / enum
|
|
151
|
+
// declarations + JSDoc summaries, caches the result in
|
|
152
|
+
// `.pugi/repo-map.json`, and renders the compact markdown listing.
|
|
153
|
+
// Same builder powers the engine boot-time system-prompt injection
|
|
154
|
+
// — running the CLI command shows the operator EXACTLY what the
|
|
155
|
+
// engine would see.
|
|
156
|
+
'repo-map': dispatchRepoMap,
|
|
130
157
|
// Leak L21 (2026-05-27): in-CLI feedback collector. Shares the
|
|
131
158
|
// same handler as the in-REPL `/feedback` slash; the wrapper just
|
|
132
159
|
// routes TTY vs non-TTY before mounting Ink.
|
|
@@ -148,6 +175,11 @@ const handlers = {
|
|
|
148
175
|
vim: dispatchVim,
|
|
149
176
|
undo: dispatchUndo,
|
|
150
177
|
compact: dispatchCompact,
|
|
178
|
+
// Leak L9 (2026-05-27): `pugi rewind [N | --to <id>]` rolls the
|
|
179
|
+
// conversation back to a checkpoint by appending a tombstone marker
|
|
180
|
+
// to the NDJSON event log. The slash counterpart `/rewind` forwards
|
|
181
|
+
// to the same runner via session.ts.
|
|
182
|
+
rewind: dispatchRewind,
|
|
151
183
|
// L19 (2026-05-27): `pugi usage` is an alias of `pugi cost` — same
|
|
152
184
|
// handler, same flags. Operators trained on Claude Code expect either
|
|
153
185
|
// verb to surface the per-model token + USD table.
|
|
@@ -332,6 +364,41 @@ async function dispatchPrivacy(args, flags, _session) {
|
|
|
332
364
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
333
365
|
});
|
|
334
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* ADR-0063 Day 4 — `pugi memory <sub>` top-level dispatcher.
|
|
369
|
+
*
|
|
370
|
+
* Forwards to the shared `runMemoryCommand` runner. Exit codes:
|
|
371
|
+
*
|
|
372
|
+
* - 0 — happy paths (listed / recalled / written / forgot / synced /
|
|
373
|
+
* queued_offline / sync_noop / sync_partial)
|
|
374
|
+
* - 1 — unauthenticated / feature_disabled / unknown_sub
|
|
375
|
+
* - 2 — invalid_args
|
|
376
|
+
*
|
|
377
|
+
* `forget_not_found` exits 0 because the operator-visible behaviour
|
|
378
|
+
* (the memory is gone) matches their intent; the JSON envelope still
|
|
379
|
+
* carries the `forget_not_found` status flag for scripted callers.
|
|
380
|
+
*/
|
|
381
|
+
async function dispatchMemory(args, flags, _session) {
|
|
382
|
+
const result = await runMemoryCommand(args, {
|
|
383
|
+
workspaceRoot: process.cwd(),
|
|
384
|
+
json: flags.json,
|
|
385
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
386
|
+
});
|
|
387
|
+
switch (result.status) {
|
|
388
|
+
case 'unauthenticated':
|
|
389
|
+
case 'feature_disabled':
|
|
390
|
+
case 'unknown_sub':
|
|
391
|
+
process.exitCode = 1;
|
|
392
|
+
return;
|
|
393
|
+
case 'invalid_args':
|
|
394
|
+
process.exitCode = 2;
|
|
395
|
+
return;
|
|
396
|
+
default:
|
|
397
|
+
// 'listed' | 'recalled' | 'written' | 'queued_offline' | 'forgot' |
|
|
398
|
+
// 'forget_not_found' | 'synced' | 'sync_partial' | 'sync_noop' — exit 0.
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
335
402
|
/**
|
|
336
403
|
* Leak L18 (2026-05-27) — `pugi style` top-level dispatcher.
|
|
337
404
|
*
|
|
@@ -370,6 +437,31 @@ async function dispatchStyle(args, flags, _session) {
|
|
|
370
437
|
* The runner returns the code; we attach it to `process.exitCode` so
|
|
371
438
|
* subsequent dispatch wrappers do not clobber it on success.
|
|
372
439
|
*/
|
|
440
|
+
/**
|
|
441
|
+
* Leak L12 (2026-05-27) — `pugi hooks` top-level dispatcher (MVP).
|
|
442
|
+
*
|
|
443
|
+
* Two subcommands:
|
|
444
|
+
* - `pugi hooks list` — show configured hooks per event.
|
|
445
|
+
* - `pugi hooks doctor` — validate `~/.pugi/hooks-mvp.json`.
|
|
446
|
+
*
|
|
447
|
+
* MVP scope: 2 events of 8 (SessionStart + PreToolUse). Remaining 6
|
|
448
|
+
* events (PostToolUse, UserPromptSubmit, Stop, SubagentStop,
|
|
449
|
+
* PreCompact, Notification) deferred to fast-follow PR. The runner
|
|
450
|
+
* pattern established here is reusable for those events without
|
|
451
|
+
* touching this dispatcher.
|
|
452
|
+
*
|
|
453
|
+
* Exit codes:
|
|
454
|
+
* 0 -> happy path.
|
|
455
|
+
* 1 -> config present but invalid (doctor only).
|
|
456
|
+
* 2 -> argument error / unknown subcommand.
|
|
457
|
+
*/
|
|
458
|
+
async function dispatchHooks(args, flags, _session) {
|
|
459
|
+
const rc = await runHooksCommand(args, {
|
|
460
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
461
|
+
});
|
|
462
|
+
if (rc !== 0)
|
|
463
|
+
process.exitCode = rc;
|
|
464
|
+
}
|
|
373
465
|
async function dispatchTheme(args, flags, _session) {
|
|
374
466
|
const rc = await runThemeCommand(args, {
|
|
375
467
|
workspaceRoot: process.cwd(),
|
|
@@ -534,6 +626,31 @@ async function dispatchBudget(args, flags, _session) {
|
|
|
534
626
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
535
627
|
});
|
|
536
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Leak L9 (2026-05-27) — `pugi rewind [N | --to <id>]` rolls the
|
|
631
|
+
* conversation back to a checkpoint by appending a tombstone marker to
|
|
632
|
+
* the NDJSON event log. Append-only: events stay durable; `pugi
|
|
633
|
+
* sessions undo-rewind` reverses the operation. The slash `/rewind`
|
|
634
|
+
* forwards through this same runner via session.ts.
|
|
635
|
+
*/
|
|
636
|
+
async function dispatchRewind(args, flags, _session) {
|
|
637
|
+
const result = await runRewindCommand(args, {
|
|
638
|
+
workspaceRoot: process.cwd(),
|
|
639
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
640
|
+
});
|
|
641
|
+
if (result.status === 'failed_no_session'
|
|
642
|
+
|| result.status === 'failed_store') {
|
|
643
|
+
process.exitCode = 1;
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (result.status === 'failed_parse') {
|
|
647
|
+
process.exitCode = 2;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (result.status === 'noop_zero' || result.status === 'noop_empty') {
|
|
651
|
+
process.exitCode = 2;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
537
654
|
/**
|
|
538
655
|
* Leak L6 — `pugi permissions [mode] [--persist] [--confirm]`.
|
|
539
656
|
*
|
|
@@ -709,6 +826,19 @@ async function dispatchAgents(args, flags, _session) {
|
|
|
709
826
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
710
827
|
});
|
|
711
828
|
}
|
|
829
|
+
/**
|
|
830
|
+
* Leak L10 (2026-05-27): `pugi dispatch <sub>` — operator-facing
|
|
831
|
+
* inspection + GC for fork-subagent prompt-cache inherit refs
|
|
832
|
+
* (.pugi/cache-refs/). Delegates to the standalone runner in
|
|
833
|
+
* commands/dispatch.ts so the cli.ts table stays under control.
|
|
834
|
+
*/
|
|
835
|
+
async function dispatchSubagentCacheRefs(args, flags, _session) {
|
|
836
|
+
await runDispatchCommand(args, {
|
|
837
|
+
workspaceRoot: process.cwd(),
|
|
838
|
+
json: flags.json,
|
|
839
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
712
842
|
/**
|
|
713
843
|
* `pugi web <url>` — Sprint α6.15 Phase 1 quick-win subcommand.
|
|
714
844
|
*
|
|
@@ -995,6 +1125,10 @@ function parseArgs(argv) {
|
|
|
995
1125
|
// bare invocation only surfaces new sections. Opt-in to force the
|
|
996
1126
|
// full bundled changelog к re-render (clears the on-disk marker).
|
|
997
1127
|
reset: false,
|
|
1128
|
+
// Leak L28 — `--refresh` for `pugi repo-map`. Default off so a
|
|
1129
|
+
// bare invocation hits the cache when mtime + size match; opt-in
|
|
1130
|
+
// for a cold rebuild from the source tree.
|
|
1131
|
+
refresh: false,
|
|
998
1132
|
};
|
|
999
1133
|
const args = [];
|
|
1000
1134
|
// Leak L22: scan for `--bare` BEFORE the early-return short-circuits
|
|
@@ -1089,6 +1223,22 @@ function parseArgs(argv) {
|
|
|
1089
1223
|
// single consumer today.
|
|
1090
1224
|
flags.reset = true;
|
|
1091
1225
|
}
|
|
1226
|
+
else if (arg === '--refresh') {
|
|
1227
|
+
// Leak L28 — `pugi repo-map --refresh` busts the cache and
|
|
1228
|
+
// rebuilds the AST-light summary from a cold scan. Parsed
|
|
1229
|
+
// globally for symmetry with the rest of the flag grammar;
|
|
1230
|
+
// `runRepoMapCommand` is the single consumer today.
|
|
1231
|
+
flags.refresh = true;
|
|
1232
|
+
}
|
|
1233
|
+
else if (arg === '--format=json' || arg === '--format' && argv[index + 1] === 'json') {
|
|
1234
|
+
// Leak L28 — `pugi repo-map --format=json` is a per-command
|
|
1235
|
+
// synonym for the global `--json` flag. The L28 spec calls
|
|
1236
|
+
// out the `--format=json` shape explicitly so we accept it
|
|
1237
|
+
// verbatim and route through the existing JSON envelope.
|
|
1238
|
+
flags.json = true;
|
|
1239
|
+
if (arg === '--format')
|
|
1240
|
+
index += 1;
|
|
1241
|
+
}
|
|
1092
1242
|
else if (arg === '--decompose') {
|
|
1093
1243
|
// α6.8 EXTEND PR1: plan-only flag. Other engine commands ignore
|
|
1094
1244
|
// it. Parsed globally for symmetry with the rest of the flag
|
|
@@ -1470,6 +1620,17 @@ const COMMAND_HELP_BODIES = {
|
|
|
1470
1620
|
'Slugs (Tier 1 alpha 7.5): dev qa pm devops researcher analyst designer',
|
|
1471
1621
|
'frontend architect. `pugi roster` lists the live set.',
|
|
1472
1622
|
],
|
|
1623
|
+
dispatch: [
|
|
1624
|
+
'pugi dispatch <sub> — inspect + GC fork-subagent prompt-cache inherit refs.',
|
|
1625
|
+
'',
|
|
1626
|
+
' list-cache-refs Table of every active ref under .pugi/cache-refs/.',
|
|
1627
|
+
' clear-cache-refs [--older-than 1h] Evict refs older than the window (default 24h).',
|
|
1628
|
+
'',
|
|
1629
|
+
'Leak L10 (2026-05-27): when Mira spawns a child via the `agent` tool,',
|
|
1630
|
+
'a prompt-cache handle is persisted so the child loop can request',
|
|
1631
|
+
'parent-context reuse on the wire. These commands surface + clean up',
|
|
1632
|
+
'the persisted refs.',
|
|
1633
|
+
],
|
|
1473
1634
|
roster: [
|
|
1474
1635
|
'pugi roster — list the live Tier 1 personas + roles.',
|
|
1475
1636
|
],
|
|
@@ -1638,6 +1799,8 @@ async function help(args, flags, _session) {
|
|
|
1638
1799
|
'Persona dispatch (α7.5):',
|
|
1639
1800
|
' pugi roster List the live Tier 1 personas + roles.',
|
|
1640
1801
|
' pugi delegate <slug> "<brief>" Dispatch a brief to one specialist.',
|
|
1802
|
+
' pugi dispatch list-cache-refs Inspect fork-subagent prompt-cache inherit refs.',
|
|
1803
|
+
' pugi dispatch clear-cache-refs GC stale cache refs (--older-than 1h).',
|
|
1641
1804
|
'',
|
|
1642
1805
|
'Plan decomposition (α6.8):',
|
|
1643
1806
|
' pugi plan --decompose <idea> Split a high-level idea into 3-7 components.',
|
|
@@ -1796,6 +1959,27 @@ async function stickers(_args, flags, _session) {
|
|
|
1796
1959
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
1797
1960
|
});
|
|
1798
1961
|
}
|
|
1962
|
+
/**
|
|
1963
|
+
* `pugi repo-map` — Leak L28 (2026-05-27). Builds + caches the AST-
|
|
1964
|
+
* light symbol summary of the workspace. The handler is intentionally
|
|
1965
|
+
* thin: argv tail tokens are honoured for `--refresh` symmetry (the
|
|
1966
|
+
* global parser already sets `flags.refresh`, but accepting the flag
|
|
1967
|
+
* positionally lets `pugi repo-map refresh` work too — both forms
|
|
1968
|
+
* land в the same path). Exit code is always 0 (informational).
|
|
1969
|
+
*
|
|
1970
|
+
* The same builder is invoked lazily on engine boot when `--bare` is
|
|
1971
|
+
* not set; running the CLI command shows the operator EXACTLY what
|
|
1972
|
+
* the engine would inject into the system prompt.
|
|
1973
|
+
*/
|
|
1974
|
+
async function dispatchRepoMap(args, flags, _session) {
|
|
1975
|
+
const refresh = flags.refresh || args.includes('--refresh') || args.includes('refresh');
|
|
1976
|
+
await runRepoMapCommand({
|
|
1977
|
+
cwd: process.cwd(),
|
|
1978
|
+
refresh,
|
|
1979
|
+
json: flags.json,
|
|
1980
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1799
1983
|
/**
|
|
1800
1984
|
* `pugi feedback` — Leak L21 (2026-05-27). In-CLI feedback collector.
|
|
1801
1985
|
*
|
|
@@ -3262,6 +3446,25 @@ async function handoff(args, flags, session) {
|
|
|
3262
3446
|
writeOutput(flags, bundle, ['Pugi handoff bundle created', `Bundle: ${bundle.path}`].join('\n'));
|
|
3263
3447
|
}
|
|
3264
3448
|
async function sessions(args, flags, _session) {
|
|
3449
|
+
// L9 (2026-05-27): `pugi sessions undo-rewind [<session-id>]` rolls
|
|
3450
|
+
// back the latest /rewind by appending an inverse marker. Append-only,
|
|
3451
|
+
// reversible. Falls through to the legacy artifact-based handler when
|
|
3452
|
+
// the sub-command is not recognised.
|
|
3453
|
+
if (args[0] === 'undo-rewind') {
|
|
3454
|
+
const result = await runSessionsCommand(args, {
|
|
3455
|
+
workspaceRoot: process.cwd(),
|
|
3456
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
3457
|
+
});
|
|
3458
|
+
if (result) {
|
|
3459
|
+
if (result.status === 'failed_no_session' || result.status === 'failed_store') {
|
|
3460
|
+
process.exitCode = 1;
|
|
3461
|
+
}
|
|
3462
|
+
else if (result.status === 'noop_no_rewind') {
|
|
3463
|
+
process.exitCode = 2;
|
|
3464
|
+
}
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3265
3468
|
// α6.4: `pugi sessions --local` / `--search "query"` route to the
|
|
3266
3469
|
// local SessionStore. The default surface stays artifact-based for
|
|
3267
3470
|
// backward compat — operators who relied on the index.json view get
|
|
@@ -4188,6 +4391,19 @@ function runEngineTask(kind) {
|
|
|
4188
4391
|
process.stderr.write(`pugi ${label}: MCP registry shutdown reported error — ${error.message}\n`);
|
|
4189
4392
|
});
|
|
4190
4393
|
}
|
|
4394
|
+
// Leak L15 (2026-05-27) — tear down any LSP servers warmed up
|
|
4395
|
+
// by the post-edit diagnostics cache. The cache is per-process
|
|
4396
|
+
// and survives across multiple tool calls; without this hook a
|
|
4397
|
+
// `pugi code ...` invocation would leak a tsserver process when
|
|
4398
|
+
// the Node host exits. The dynamic import keeps the cache module
|
|
4399
|
+
// out of the cold path for runs that never touch LSP.
|
|
4400
|
+
try {
|
|
4401
|
+
const { stopAllLspClients } = await import('../core/lsp/cache.js');
|
|
4402
|
+
await stopAllLspClients();
|
|
4403
|
+
}
|
|
4404
|
+
catch (error) {
|
|
4405
|
+
process.stderr.write(`pugi ${label}: LSP cache shutdown reported error — ${error.message}\n`);
|
|
4406
|
+
}
|
|
4191
4407
|
}
|
|
4192
4408
|
};
|
|
4193
4409
|
}
|