@pugi/cli 0.1.0-beta.30 → 0.1.0-beta.31
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.
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD ↔ session review — Pugi α7 Wave 6 final (`/prd-check --session`,
|
|
3
|
+
* 2026-05-27).
|
|
4
|
+
*
|
|
5
|
+
* The shipped `/prd-check` surface verifies a PRD's acceptance criteria
|
|
6
|
+
* against committed artifacts on disk (file / test / doc / command /
|
|
7
|
+
* route verifiers — see `core/prd-check/verifiers.ts`). That mode is
|
|
8
|
+
* the canonical CI gate; it inspects a snapshot of the workspace and
|
|
9
|
+
* decides healthy / failing / unparsed.
|
|
10
|
+
*
|
|
11
|
+
* `--session` adds a complementary mode: ask "does the work I just
|
|
12
|
+
* did in this REPL session actually cover the PRD?" without requiring
|
|
13
|
+
* the acceptance criteria to be written in the structured shape the
|
|
14
|
+
* verifier expects. The mode:
|
|
15
|
+
*
|
|
16
|
+
* 1. Walks up from the current working directory looking for
|
|
17
|
+
* `PRD.md`, then `PRODUCT.md`, then `apps/<any>/PRODUCT.md`
|
|
18
|
+
* under the workspace root. The first hit wins; the rest are
|
|
19
|
+
* ignored. We stop at the filesystem root.
|
|
20
|
+
*
|
|
21
|
+
* 2. Reads the last N (default 20) events from `.pugi/events.jsonl`
|
|
22
|
+
* that represent operator-visible turns: user prompts, model
|
|
23
|
+
* responses, and tool calls with their results. Other event
|
|
24
|
+
* types (`session`, `hook`, …) are filtered so the prompt the
|
|
25
|
+
* subagent receives is dominated by intent + work-actually-done
|
|
26
|
+
* rather than infrastructure noise.
|
|
27
|
+
*
|
|
28
|
+
* 3. Builds a structured prompt for a cross-review subagent. The
|
|
29
|
+
* subagent is dispatched via the standard `dispatch()` surface
|
|
30
|
+
* (see `core/subagents/dispatcher.ts`) using the `pm` persona —
|
|
31
|
+
* the closest match to a PRD reviewer in today's roster. The
|
|
32
|
+
* subagent is instructed to return two newline-separated lists:
|
|
33
|
+
* `Satisfied:` and `Outstanding:`.
|
|
34
|
+
*
|
|
35
|
+
* 4. Parses the subagent's response into a structured envelope so
|
|
36
|
+
* the slash + the top-level CLI render the same output shape
|
|
37
|
+
* (Markdown sections in the TUI, JSON for scripts).
|
|
38
|
+
*
|
|
39
|
+
* The module is pure with respect to dependencies — every IO surface
|
|
40
|
+
* (`readPrdSearch`, `readSessionEvents`, `dispatchReview`) is
|
|
41
|
+
* accepted as an injected dep so the spec can drive every branch
|
|
42
|
+
* without a real filesystem or a network call. The default
|
|
43
|
+
* implementations live in `defaultSessionReviewDeps`.
|
|
44
|
+
*
|
|
45
|
+
* The subagent return contract is intentionally loose: any line
|
|
46
|
+
* starting with `Satisfied:` (case-insensitive) opens the satisfied
|
|
47
|
+
* section; any line starting with `Outstanding:` opens the
|
|
48
|
+
* outstanding section. Bulleted lines (`-`, `*`, `•`) inside each
|
|
49
|
+
* section land as list items. Free prose between sections is
|
|
50
|
+
* captured as the `note` field on the envelope so the reviewer can
|
|
51
|
+
* still attach a sentence of context.
|
|
52
|
+
*
|
|
53
|
+
* Cross-review subagent: this module routes to whatever persona
|
|
54
|
+
* resolves the "PRD reviewer" role at dispatch time. In α7 Wave 6
|
|
55
|
+
* that is the `pm` persona via `dispatch({ role: 'pm', … })`. Once
|
|
56
|
+
* a dedicated reviewer persona lands (project tracking ID:
|
|
57
|
+
* cross-review subagent track), swapping it requires only changing
|
|
58
|
+
* `DEFAULT_REVIEW_ROLE` below.
|
|
59
|
+
*/
|
|
60
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
61
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
62
|
+
/**
|
|
63
|
+
* Names of PRD-style files we will try to locate. Order matters —
|
|
64
|
+
* `PRD.md` wins over `PRODUCT.md` so a project that has both an
|
|
65
|
+
* authored PRD and a legacy PRODUCT brief points at the PRD.
|
|
66
|
+
*/
|
|
67
|
+
const PRD_CANDIDATE_NAMES = ['PRD.md', 'PRODUCT.md'];
|
|
68
|
+
/**
|
|
69
|
+
* Per-app product brief glob. We scan `apps/*` under the workspace
|
|
70
|
+
* root for a top-level `PRODUCT.md` — this matches the layout the
|
|
71
|
+
* Pugi monorepo uses (`apps/admin-api/PRODUCT.md`, etc.). Walk is
|
|
72
|
+
* shallow on purpose to keep the search bounded.
|
|
73
|
+
*/
|
|
74
|
+
const APPS_DIR = 'apps';
|
|
75
|
+
/** Default number of NDJSON turns to surface to the reviewer. */
|
|
76
|
+
export const DEFAULT_TURN_COUNT = 20;
|
|
77
|
+
/**
|
|
78
|
+
* Subagent role we ask to play the PRD reviewer. `reviewer` is the
|
|
79
|
+
* canonical slot in the dispatcher's role enum (see
|
|
80
|
+
* `packages/pugi-sdk/src/subagent-contracts.ts`) — every dispatch
|
|
81
|
+
* resolves a role to a persona at runtime via the isolation matrix.
|
|
82
|
+
* The constant is exported so callers can override (e.g. route to
|
|
83
|
+
* `verifier` for a stricter pass).
|
|
84
|
+
*/
|
|
85
|
+
export const DEFAULT_REVIEW_ROLE = 'reviewer';
|
|
86
|
+
/**
|
|
87
|
+
* Run the `--session` mode end-to-end. The function emits status
|
|
88
|
+
* lines through `deps.onStatus` (the slash dispatcher wires this to
|
|
89
|
+
* `appendSystemLine`) and returns the structured envelope.
|
|
90
|
+
*/
|
|
91
|
+
export async function runSessionReview(workspaceRoot, deps, options = {}) {
|
|
92
|
+
const turnCount = options.turnCount ?? DEFAULT_TURN_COUNT;
|
|
93
|
+
const status = deps.onStatus ?? (() => undefined);
|
|
94
|
+
status('Locating PRD...');
|
|
95
|
+
const prdPath = deps.resolvePrd(workspaceRoot);
|
|
96
|
+
if (prdPath === null) {
|
|
97
|
+
return {
|
|
98
|
+
command: 'prd-check',
|
|
99
|
+
mode: 'session',
|
|
100
|
+
status: 'no_prd',
|
|
101
|
+
prdPath: null,
|
|
102
|
+
prdTitle: null,
|
|
103
|
+
turnsRead: 0,
|
|
104
|
+
satisfied: [],
|
|
105
|
+
outstanding: [],
|
|
106
|
+
note: '',
|
|
107
|
+
rawReview: null,
|
|
108
|
+
reason: `No PRD.md or PRODUCT.md found walking up from ${workspaceRoot}.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const prdSource = safeRead(prdPath);
|
|
112
|
+
if (prdSource === null) {
|
|
113
|
+
return {
|
|
114
|
+
command: 'prd-check',
|
|
115
|
+
mode: 'session',
|
|
116
|
+
status: 'no_prd',
|
|
117
|
+
prdPath: relative(workspaceRoot, prdPath) || prdPath,
|
|
118
|
+
prdTitle: null,
|
|
119
|
+
turnsRead: 0,
|
|
120
|
+
satisfied: [],
|
|
121
|
+
outstanding: [],
|
|
122
|
+
note: '',
|
|
123
|
+
rawReview: null,
|
|
124
|
+
reason: `PRD file at ${prdPath} is unreadable.`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const prdTitle = extractTitle(prdSource);
|
|
128
|
+
status(`Reading last ${turnCount} session turns...`);
|
|
129
|
+
const rawEvents = deps.readEventsLog(workspaceRoot);
|
|
130
|
+
const turns = rawEvents === null ? [] : parseTurns(rawEvents, turnCount);
|
|
131
|
+
status('Reviewing against PRD...');
|
|
132
|
+
const prompt = buildReviewPrompt({
|
|
133
|
+
prdPath,
|
|
134
|
+
prdTitle,
|
|
135
|
+
prdSource,
|
|
136
|
+
turns,
|
|
137
|
+
workspaceRoot,
|
|
138
|
+
});
|
|
139
|
+
const outcome = await deps.dispatchReview({
|
|
140
|
+
role: DEFAULT_REVIEW_ROLE,
|
|
141
|
+
prompt,
|
|
142
|
+
workspaceRoot,
|
|
143
|
+
});
|
|
144
|
+
if (!outcome.ok) {
|
|
145
|
+
return {
|
|
146
|
+
command: 'prd-check',
|
|
147
|
+
mode: 'session',
|
|
148
|
+
status: 'dispatch_failed',
|
|
149
|
+
prdPath: relative(workspaceRoot, prdPath) || prdPath,
|
|
150
|
+
prdTitle,
|
|
151
|
+
turnsRead: turns.length,
|
|
152
|
+
satisfied: [],
|
|
153
|
+
outstanding: [],
|
|
154
|
+
note: '',
|
|
155
|
+
rawReview: null,
|
|
156
|
+
reason: outcome.reason,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const parsed = parseReviewResponse(outcome.text);
|
|
160
|
+
return {
|
|
161
|
+
command: 'prd-check',
|
|
162
|
+
mode: 'session',
|
|
163
|
+
status: 'ok',
|
|
164
|
+
prdPath: relative(workspaceRoot, prdPath) || prdPath,
|
|
165
|
+
prdTitle,
|
|
166
|
+
turnsRead: turns.length,
|
|
167
|
+
satisfied: parsed.satisfied,
|
|
168
|
+
outstanding: parsed.outstanding,
|
|
169
|
+
note: parsed.note,
|
|
170
|
+
rawReview: outcome.text,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Render the envelope as plain-text lines for the TUI / shell. The
|
|
175
|
+
* slash + the top-level CLI both use this so the surface stays
|
|
176
|
+
* single-sourced; the structured envelope (above) feeds `--json`.
|
|
177
|
+
*/
|
|
178
|
+
export function renderSessionReview(result) {
|
|
179
|
+
if (result.status === 'no_prd') {
|
|
180
|
+
return result.reason ?? 'No PRD found.';
|
|
181
|
+
}
|
|
182
|
+
if (result.status === 'dispatch_failed') {
|
|
183
|
+
return `PRD review dispatch failed: ${result.reason ?? 'unknown'}`;
|
|
184
|
+
}
|
|
185
|
+
const lines = [];
|
|
186
|
+
lines.push(`PRD review (${result.turnsRead} turn(s) read from .pugi/events.jsonl)`);
|
|
187
|
+
if (result.prdPath !== null) {
|
|
188
|
+
const titlePart = result.prdTitle === null ? '' : ` — ${result.prdTitle}`;
|
|
189
|
+
lines.push(`Source: ${result.prdPath}${titlePart}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push('');
|
|
192
|
+
if (result.satisfied.length === 0) {
|
|
193
|
+
lines.push('Satisfied: (none reported)');
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
lines.push('Satisfied:');
|
|
197
|
+
for (const item of result.satisfied)
|
|
198
|
+
lines.push(` - ${item}`);
|
|
199
|
+
}
|
|
200
|
+
lines.push('');
|
|
201
|
+
if (result.outstanding.length === 0) {
|
|
202
|
+
lines.push('Outstanding: (none reported)');
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
lines.push('Outstanding:');
|
|
206
|
+
for (const item of result.outstanding)
|
|
207
|
+
lines.push(` - ${item}`);
|
|
208
|
+
}
|
|
209
|
+
if (result.note.length > 0) {
|
|
210
|
+
lines.push('');
|
|
211
|
+
lines.push(`Note: ${result.note}`);
|
|
212
|
+
}
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Compose the prompt handed to the reviewer subagent. The prompt is
|
|
217
|
+
* deterministic — input order matters for prompt cache hits so we
|
|
218
|
+
* keep the layout stable: PRD source first, session turns second,
|
|
219
|
+
* forcing-question schema last.
|
|
220
|
+
*/
|
|
221
|
+
export function buildReviewPrompt(input) {
|
|
222
|
+
const parts = [];
|
|
223
|
+
parts.push('You are a PRD reviewer. Read the PRD below, then read the recent session turns, then decide which requirements are SATISFIED by the work-so-far and which remain OUTSTANDING.');
|
|
224
|
+
parts.push('');
|
|
225
|
+
parts.push('Reply in EXACTLY this shape:');
|
|
226
|
+
parts.push('');
|
|
227
|
+
parts.push('Satisfied:');
|
|
228
|
+
parts.push('- <requirement>');
|
|
229
|
+
parts.push('- <requirement>');
|
|
230
|
+
parts.push('');
|
|
231
|
+
parts.push('Outstanding:');
|
|
232
|
+
parts.push('- <requirement>');
|
|
233
|
+
parts.push('- <requirement>');
|
|
234
|
+
parts.push('');
|
|
235
|
+
parts.push('Each list item is one PRD requirement, paraphrased to fit a single line. Do not invent requirements that are not in the PRD. If the session shows no work toward a requirement, list it Outstanding.');
|
|
236
|
+
parts.push('');
|
|
237
|
+
parts.push('--- PRD start ---');
|
|
238
|
+
parts.push(`Path: ${relative(input.workspaceRoot, input.prdPath) || input.prdPath}`);
|
|
239
|
+
if (input.prdTitle !== null)
|
|
240
|
+
parts.push(`Title: ${input.prdTitle}`);
|
|
241
|
+
parts.push('');
|
|
242
|
+
parts.push(input.prdSource);
|
|
243
|
+
parts.push('--- PRD end ---');
|
|
244
|
+
parts.push('');
|
|
245
|
+
parts.push(`--- Last ${input.turns.length} session turn(s) start ---`);
|
|
246
|
+
for (const turn of input.turns) {
|
|
247
|
+
parts.push(formatTurn(turn));
|
|
248
|
+
}
|
|
249
|
+
parts.push('--- session turns end ---');
|
|
250
|
+
return parts.join('\n');
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Walk up from cwd looking for the first PRD / PRODUCT file. Also
|
|
254
|
+
* scans `apps/*` under the workspace root once we find a directory
|
|
255
|
+
* that contains an `apps` folder. Returns absolute path or null.
|
|
256
|
+
*/
|
|
257
|
+
export function defaultResolvePrd(cwd) {
|
|
258
|
+
let current = resolve(cwd);
|
|
259
|
+
for (let i = 0; i < 32; i += 1) {
|
|
260
|
+
for (const name of PRD_CANDIDATE_NAMES) {
|
|
261
|
+
const candidate = join(current, name);
|
|
262
|
+
if (existsSync(candidate) && safeIsFile(candidate)) {
|
|
263
|
+
return candidate;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// If this directory holds an apps/ folder, scan one level deep
|
|
267
|
+
// for app-scoped PRODUCT.md. We do NOT keep walking up past this
|
|
268
|
+
// point — the apps/ folder marks the workspace root in our
|
|
269
|
+
// monorepo layout.
|
|
270
|
+
const appsDir = join(current, APPS_DIR);
|
|
271
|
+
if (existsSync(appsDir) && safeIsDirectory(appsDir)) {
|
|
272
|
+
const appHit = scanAppsForProduct(appsDir);
|
|
273
|
+
if (appHit !== null)
|
|
274
|
+
return appHit;
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const parent = dirname(current);
|
|
278
|
+
if (parent === current)
|
|
279
|
+
break;
|
|
280
|
+
current = parent;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Default events log reader. Returns the raw file contents or null
|
|
286
|
+
* when the log is absent — `.pugi/events.jsonl` only exists when
|
|
287
|
+
* the workspace has had at least one session.
|
|
288
|
+
*/
|
|
289
|
+
export function defaultReadEventsLog(workspaceRoot) {
|
|
290
|
+
const path = join(workspaceRoot, '.pugi', 'events.jsonl');
|
|
291
|
+
return safeRead(path);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Default cross-review dispatcher. Routes through the standard
|
|
295
|
+
* subagents `dispatch()` surface in stub-mode (no engine client) so
|
|
296
|
+
* the slash + the CLI top-level have a working default that does not
|
|
297
|
+
* require a live admin-api connection. When an engine client is in
|
|
298
|
+
* scope (callers supply one via injection from the runtime cli.ts
|
|
299
|
+
* layer once auth is resolved), `dispatch()` will route to the real
|
|
300
|
+
* cross-review subagent.
|
|
301
|
+
*
|
|
302
|
+
* Imported dynamically so test paths that inject a stub never pull
|
|
303
|
+
* the dispatcher graph into the spec.
|
|
304
|
+
*/
|
|
305
|
+
export const defaultDispatchReview = async (input) => {
|
|
306
|
+
try {
|
|
307
|
+
const { dispatch, inMemoryDispatcherContext } = await import('../subagents/dispatcher.js');
|
|
308
|
+
// The dispatcher's role enum is the canonical schema; `as` keeps
|
|
309
|
+
// the caller's role string flowing through without re-importing
|
|
310
|
+
// the schema here (the schema parse inside `dispatch()` will
|
|
311
|
+
// reject if it does not match — that is the contract gate).
|
|
312
|
+
const task = {
|
|
313
|
+
id: `prd-review-${Date.now()}`,
|
|
314
|
+
role: input.role,
|
|
315
|
+
prompt: input.prompt,
|
|
316
|
+
permissionMode: 'plan',
|
|
317
|
+
};
|
|
318
|
+
const ctx = inMemoryDispatcherContext({
|
|
319
|
+
sessionId: `prd-review-session-${Date.now()}`,
|
|
320
|
+
workspaceRoot: input.workspaceRoot,
|
|
321
|
+
sink: [],
|
|
322
|
+
});
|
|
323
|
+
const outcome = await dispatch(task, ctx);
|
|
324
|
+
if (!outcome || typeof outcome !== 'object') {
|
|
325
|
+
return { ok: false, reason: 'dispatcher returned an unexpected shape' };
|
|
326
|
+
}
|
|
327
|
+
if (typeof outcome.summary === 'string' && outcome.summary.length > 0) {
|
|
328
|
+
return { ok: true, text: outcome.summary };
|
|
329
|
+
}
|
|
330
|
+
return { ok: false, reason: 'dispatcher response carried no reviewable summary' };
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
334
|
+
return { ok: false, reason: message };
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
/** Convenience bundle for callers that want the real-IO defaults. */
|
|
338
|
+
export function defaultSessionReviewDeps(overrides = {}) {
|
|
339
|
+
return {
|
|
340
|
+
resolvePrd: overrides.resolvePrd ?? defaultResolvePrd,
|
|
341
|
+
readEventsLog: overrides.readEventsLog ?? defaultReadEventsLog,
|
|
342
|
+
dispatchReview: overrides.dispatchReview ?? defaultDispatchReview,
|
|
343
|
+
...(overrides.onStatus !== undefined ? { onStatus: overrides.onStatus } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Parse NDJSON, filter to operator-visible turns, and return the
|
|
348
|
+
* last N in chronological order. Malformed lines are silently
|
|
349
|
+
* dropped — the events log is appended to from multiple processes
|
|
350
|
+
* and partial writes during shutdown can leave a trailing line.
|
|
351
|
+
*/
|
|
352
|
+
export function parseTurns(raw, max) {
|
|
353
|
+
const lines = raw.split('\n').filter((line) => line.trim().length > 0);
|
|
354
|
+
const events = [];
|
|
355
|
+
for (const line of lines) {
|
|
356
|
+
try {
|
|
357
|
+
const parsed = JSON.parse(line);
|
|
358
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
359
|
+
events.push(parsed);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Drop malformed lines.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const visible = [];
|
|
367
|
+
for (const event of events) {
|
|
368
|
+
const role = classifyEvent(event);
|
|
369
|
+
if (role === null)
|
|
370
|
+
continue;
|
|
371
|
+
visible.push({
|
|
372
|
+
index: visible.length,
|
|
373
|
+
role,
|
|
374
|
+
summary: summariseEvent(event, role),
|
|
375
|
+
raw: event,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (visible.length <= max) {
|
|
379
|
+
return visible.map((t, idx) => ({ ...t, index: idx }));
|
|
380
|
+
}
|
|
381
|
+
return visible.slice(visible.length - max).map((t, idx) => ({ ...t, index: idx }));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Pull a "Satisfied:" / "Outstanding:" pair out of the subagent text.
|
|
385
|
+
* Both labels are matched case-insensitively at line start. Bulleted
|
|
386
|
+
* items use any of `- `, `* `, or `• `. Free prose before / between
|
|
387
|
+
* sections lands in `note` (concatenated with a single space so the
|
|
388
|
+
* renderer can decide on wrapping).
|
|
389
|
+
*/
|
|
390
|
+
export function parseReviewResponse(text) {
|
|
391
|
+
const lines = text.split('\n');
|
|
392
|
+
const satisfied = [];
|
|
393
|
+
const outstanding = [];
|
|
394
|
+
const noteParts = [];
|
|
395
|
+
let section = 'none';
|
|
396
|
+
for (const raw of lines) {
|
|
397
|
+
const line = raw.trim();
|
|
398
|
+
if (line.length === 0)
|
|
399
|
+
continue;
|
|
400
|
+
const header = matchSectionHeader(line);
|
|
401
|
+
if (header !== null) {
|
|
402
|
+
section = header;
|
|
403
|
+
// Capture any inline item after the colon, e.g. "Satisfied: foo".
|
|
404
|
+
const inline = stripHeaderPrefix(line);
|
|
405
|
+
if (inline.length > 0) {
|
|
406
|
+
pushItem(inline, section, satisfied, outstanding);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const bulleted = matchBulleted(line);
|
|
411
|
+
if (bulleted !== null) {
|
|
412
|
+
pushItem(bulleted, section, satisfied, outstanding);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (section === 'none') {
|
|
416
|
+
noteParts.push(line);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
// Continuation of the previous bulleted item — append.
|
|
420
|
+
const target = section === 'satisfied' ? satisfied : outstanding;
|
|
421
|
+
if (target.length > 0) {
|
|
422
|
+
target[target.length - 1] = `${target[target.length - 1]} ${line}`;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
noteParts.push(line);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return { satisfied, outstanding, note: noteParts.join(' ') };
|
|
430
|
+
}
|
|
431
|
+
/* ------------------------------ helpers ------------------------------ */
|
|
432
|
+
function safeRead(path) {
|
|
433
|
+
try {
|
|
434
|
+
return readFileSync(path, 'utf8');
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function safeIsFile(path) {
|
|
441
|
+
try {
|
|
442
|
+
return statSync(path).isFile();
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function safeIsDirectory(path) {
|
|
449
|
+
try {
|
|
450
|
+
return statSync(path).isDirectory();
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function scanAppsForProduct(appsDir) {
|
|
457
|
+
let entries;
|
|
458
|
+
try {
|
|
459
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
460
|
+
entries = require('node:fs').readdirSync(appsDir);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
entries.sort();
|
|
466
|
+
for (const entry of entries) {
|
|
467
|
+
const candidate = join(appsDir, entry, 'PRODUCT.md');
|
|
468
|
+
if (existsSync(candidate) && safeIsFile(candidate))
|
|
469
|
+
return candidate;
|
|
470
|
+
const prdCandidate = join(appsDir, entry, 'PRD.md');
|
|
471
|
+
if (existsSync(prdCandidate) && safeIsFile(prdCandidate))
|
|
472
|
+
return prdCandidate;
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
function extractTitle(source) {
|
|
477
|
+
for (const raw of source.split('\n')) {
|
|
478
|
+
const line = raw.trim();
|
|
479
|
+
if (line.length === 0)
|
|
480
|
+
continue;
|
|
481
|
+
if (line.startsWith('# '))
|
|
482
|
+
return line.slice(2).trim();
|
|
483
|
+
if (line.startsWith('#'))
|
|
484
|
+
return line.replace(/^#+/, '').trim();
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
function classifyEvent(event) {
|
|
490
|
+
const type = typeof event.type === 'string' ? event.type : '';
|
|
491
|
+
if (type === 'user' || type === 'user_message' || type === 'prompt')
|
|
492
|
+
return 'user';
|
|
493
|
+
if (type === 'assistant' || type === 'model' || type === 'response' || type === 'completion') {
|
|
494
|
+
return 'model';
|
|
495
|
+
}
|
|
496
|
+
if (type === 'tool_call' || type === 'tool_result' || type === 'file_mutation')
|
|
497
|
+
return 'tool';
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
function summariseEvent(event, role) {
|
|
501
|
+
const text = typeof event.text === 'string' ? event.text : '';
|
|
502
|
+
const tool = typeof event.tool === 'string' ? event.tool : '';
|
|
503
|
+
const status = typeof event.status === 'string' ? event.status : '';
|
|
504
|
+
const path = typeof event.path === 'string' ? event.path : '';
|
|
505
|
+
const op = typeof event.operation === 'string' ? event.operation : '';
|
|
506
|
+
switch (role) {
|
|
507
|
+
case 'user':
|
|
508
|
+
return clip(text, 480);
|
|
509
|
+
case 'model':
|
|
510
|
+
return clip(text, 800);
|
|
511
|
+
case 'tool': {
|
|
512
|
+
if (event.type === 'tool_call')
|
|
513
|
+
return `tool_call ${tool || '?'}`;
|
|
514
|
+
if (event.type === 'tool_result')
|
|
515
|
+
return `tool_result ${tool || '?'} ${status}`.trim();
|
|
516
|
+
if (event.type === 'file_mutation')
|
|
517
|
+
return `file_mutation ${op} ${path}`.trim();
|
|
518
|
+
return `tool ${tool}`;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function clip(text, max) {
|
|
523
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
524
|
+
if (oneLine.length <= max)
|
|
525
|
+
return oneLine;
|
|
526
|
+
return `${oneLine.slice(0, max - 1)}…`;
|
|
527
|
+
}
|
|
528
|
+
function formatTurn(turn) {
|
|
529
|
+
return `[${turn.index + 1}] ${turn.role}: ${turn.summary}`;
|
|
530
|
+
}
|
|
531
|
+
function matchSectionHeader(line) {
|
|
532
|
+
const lower = line.toLowerCase();
|
|
533
|
+
if (/^satisfied\b/.test(lower))
|
|
534
|
+
return 'satisfied';
|
|
535
|
+
if (/^outstanding\b/.test(lower))
|
|
536
|
+
return 'outstanding';
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
function stripHeaderPrefix(line) {
|
|
540
|
+
const colon = line.indexOf(':');
|
|
541
|
+
if (colon === -1)
|
|
542
|
+
return '';
|
|
543
|
+
return line.slice(colon + 1).trim();
|
|
544
|
+
}
|
|
545
|
+
function matchBulleted(line) {
|
|
546
|
+
const match = /^(?:[-*•]|\d+\.)\s+(.*)$/u.exec(line);
|
|
547
|
+
return match === null ? null : (match[1] ?? '').trim();
|
|
548
|
+
}
|
|
549
|
+
function pushItem(item, section, satisfied, outstanding) {
|
|
550
|
+
if (item.length === 0)
|
|
551
|
+
return;
|
|
552
|
+
if (section === 'satisfied')
|
|
553
|
+
satisfied.push(item);
|
|
554
|
+
else if (section === 'outstanding')
|
|
555
|
+
outstanding.push(item);
|
|
556
|
+
}
|
|
557
|
+
//# sourceMappingURL=session-review.js.map
|
|
@@ -1052,6 +1052,15 @@ export class ReplSession {
|
|
|
1052
1052
|
// `/prd-check` and `pugi prd-check`. Dynamic-import the
|
|
1053
1053
|
// module to keep the parser + verifier graph out of the
|
|
1054
1054
|
// REPL hot path.
|
|
1055
|
+
//
|
|
1056
|
+
// Wave 6 final (2026-05-27): the runner now also honours
|
|
1057
|
+
// `--session` mode (orthogonal to the verifier graph — walks
|
|
1058
|
+
// up for PRD.md, reads NDJSON turns, dispatches a cross-
|
|
1059
|
+
// review subagent). We stream the runner's status lines
|
|
1060
|
+
// directly to the system pane so the operator sees
|
|
1061
|
+
// "Locating PRD..." / "Reviewing against PRD..." while the
|
|
1062
|
+
// dispatch is in flight, then the structured Satisfied /
|
|
1063
|
+
// Outstanding lists when it lands.
|
|
1055
1064
|
try {
|
|
1056
1065
|
const { parsePrdCheckArgs, runPrdCheckCommand } = await import('../../runtime/commands/prd-check.js');
|
|
1057
1066
|
const parsed = parsePrdCheckArgs(verdict.args, { jsonDefault: false });
|
|
@@ -1059,7 +1068,7 @@ export class ReplSession {
|
|
|
1059
1068
|
this.appendSystemLine(`/prd-check: ${parsed.error}`);
|
|
1060
1069
|
return verdict;
|
|
1061
1070
|
}
|
|
1062
|
-
|
|
1071
|
+
let sawOutput = false;
|
|
1063
1072
|
await runPrdCheckCommand({
|
|
1064
1073
|
cwd: process.cwd(),
|
|
1065
1074
|
...(parsed.prdPath !== undefined ? { prdPath: parsed.prdPath } : {}),
|
|
@@ -1074,13 +1083,13 @@ export class ReplSession {
|
|
|
1074
1083
|
knownCommands: new Set(),
|
|
1075
1084
|
writeOutput: (_payload, text) => {
|
|
1076
1085
|
const trimmed = text.replace(/\n+$/u, '');
|
|
1077
|
-
if (trimmed.length > 0)
|
|
1078
|
-
|
|
1086
|
+
if (trimmed.length > 0) {
|
|
1087
|
+
this.appendSystemLine(trimmed);
|
|
1088
|
+
sawOutput = true;
|
|
1089
|
+
}
|
|
1079
1090
|
},
|
|
1080
1091
|
});
|
|
1081
|
-
|
|
1082
|
-
this.appendSystemLine(line);
|
|
1083
|
-
if (lines.length === 0) {
|
|
1092
|
+
if (!sawOutput) {
|
|
1084
1093
|
this.appendSystemLine('/prd-check: no output.');
|
|
1085
1094
|
}
|
|
1086
1095
|
}
|
|
@@ -1474,6 +1483,40 @@ export class ReplSession {
|
|
|
1474
1483
|
}
|
|
1475
1484
|
return verdict;
|
|
1476
1485
|
}
|
|
1486
|
+
case 'undo': {
|
|
1487
|
+
// Wave 6 final (2026-05-27): graduated from stub. The runtime
|
|
1488
|
+
// command `runUndoCommand` already exists with full Aider walk-
|
|
1489
|
+
// back semantics — single-step revert of the most recent
|
|
1490
|
+
// successful `write` / `edit` / `multi_edit` tool result, with
|
|
1491
|
+
// an mtime+hash gate that refuses to overwrite uncommitted
|
|
1492
|
+
// operator work. We open a fresh PugiSession against the cwd
|
|
1493
|
+
// so the inverse-mutation audit lands on the same NDJSON
|
|
1494
|
+
// events stream the REPL writes to; dynamic-import keeps the
|
|
1495
|
+
// runner + git plumbing out of the REPL hot path.
|
|
1496
|
+
try {
|
|
1497
|
+
const [{ runUndoCommand }, { openSession }] = await Promise.all([
|
|
1498
|
+
import('../../runtime/commands/undo.js'),
|
|
1499
|
+
import('../session.js'),
|
|
1500
|
+
]);
|
|
1501
|
+
const workspaceRoot = process.cwd();
|
|
1502
|
+
const session = openSession(workspaceRoot);
|
|
1503
|
+
this.appendSystemLine('Reverting last write...');
|
|
1504
|
+
await runUndoCommand([], {
|
|
1505
|
+
workspaceRoot,
|
|
1506
|
+
session,
|
|
1507
|
+
writeOutput: (_payload, text) => {
|
|
1508
|
+
const trimmed = text.replace(/\n+$/u, '');
|
|
1509
|
+
if (trimmed.length > 0)
|
|
1510
|
+
this.appendSystemLine(trimmed);
|
|
1511
|
+
},
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
catch (error) {
|
|
1515
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1516
|
+
this.appendSystemLine(`/undo failed: ${message}`);
|
|
1517
|
+
}
|
|
1518
|
+
return verdict;
|
|
1519
|
+
}
|
|
1477
1520
|
case 'stub': {
|
|
1478
1521
|
this.appendSystemLine(verdict.message);
|
|
1479
1522
|
return verdict;
|
|
@@ -51,7 +51,11 @@ export const SLASH_STUB_MESSAGES = Object.freeze({
|
|
|
51
51
|
// β4 Sl7 (2026-05-26): /mcp graduated from stub to a real handler
|
|
52
52
|
// that forwards to `runMcpCommand`. Stub message removed from the
|
|
53
53
|
// exhaustive record so the type narrows correctly.
|
|
54
|
-
|
|
54
|
+
//
|
|
55
|
+
// Wave 6 final (2026-05-27): /undo graduated from stub to a real
|
|
56
|
+
// handler that forwards to `runUndoCommand` (Aider walk-back —
|
|
57
|
+
// single-step revert of the last mutating tool result, with
|
|
58
|
+
// mtime+hash external-modification gate). Stub message removed.
|
|
55
59
|
});
|
|
56
60
|
export const SLASH_COMMAND_HELP = Object.freeze([
|
|
57
61
|
// Workforce dispatch
|
|
@@ -89,12 +93,12 @@ export const SLASH_COMMAND_HELP = Object.freeze([
|
|
|
89
93
|
{ name: 'theme', args: '[name] [--persist|--reset|--list]', gloss: 'TUI color palette (default / dark / light / colorblind)', group: 'Settings' },
|
|
90
94
|
{ name: 'onboarding', args: '[--reset|--non-interactive]', gloss: 'First-run wizard — auth / mode / style / MCP / telemetry (leak L25)', group: 'Settings' },
|
|
91
95
|
{ name: 'vim', args: '[on|off|status]', gloss: 'Toggle vim-style modal editing in the input buffer (leak L26)', group: 'Settings' },
|
|
92
|
-
{ name: 'undo', args: '', gloss: '
|
|
96
|
+
{ name: 'undo', args: '', gloss: 'Revert the last successful write / edit / multi_edit (Aider walk-back, Wave 6)', group: 'Settings' },
|
|
93
97
|
// Meta
|
|
94
98
|
{ name: 'help', args: '', gloss: 'Show this help overlay', group: 'Meta' },
|
|
95
99
|
{ name: 'version', args: '', gloss: 'Show CLI version', group: 'Meta' },
|
|
96
100
|
{ name: 'doctor', args: '', gloss: 'Environment health report (auth · API · Node · disk · MCP · …)', group: 'Meta' },
|
|
97
|
-
{ name: 'prd-check', args: '<prd-path | --all> [--json]', gloss: 'Verify PRD
|
|
101
|
+
{ name: 'prd-check', args: '<prd-path | --all | --session> [--json]', gloss: 'Verify PRD against code (default) or session work (--session, Wave 6 final)', group: 'Meta' },
|
|
98
102
|
{ name: 'chain', args: '<new|status|next|show|export|list> [...args]', gloss: 'Artifact chain — PRD → ADR → mindmap → ER → sequence → tests → code (Wave 6)', group: 'Meta' },
|
|
99
103
|
{ name: 'stickers', args: '', gloss: 'show Pugi brand stickers (gimmick)', group: 'Meta' },
|
|
100
104
|
{ name: 'feedback', args: '', gloss: 'file a bug / feature / general comment without leaving the REPL', group: 'Meta' },
|
|
@@ -553,10 +557,18 @@ export function parseSlashCommand(input) {
|
|
|
553
557
|
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
554
558
|
return { kind: 'update', args: tokens };
|
|
555
559
|
}
|
|
560
|
+
case 'undo': {
|
|
561
|
+
// Wave 6 final (2026-05-27): graduated from stub. Tail args are
|
|
562
|
+
// ignored — `runUndoCommand` is parameterless (single-step revert
|
|
563
|
+
// of the most recent successful mutating tool result). Multiple
|
|
564
|
+
// undos = stack of single-step undos. Re-do is not yet
|
|
565
|
+
// implemented; the runner reports that in the operator-facing
|
|
566
|
+
// message after each successful undo.
|
|
567
|
+
return { kind: 'undo' };
|
|
568
|
+
}
|
|
556
569
|
case 'memory':
|
|
557
570
|
case 'config':
|
|
558
|
-
case 'budget':
|
|
559
|
-
case 'undo': {
|
|
571
|
+
case 'budget': {
|
|
560
572
|
const stubName = name;
|
|
561
573
|
return {
|
|
562
574
|
kind: 'stub',
|
package/dist/runtime/cli.js
CHANGED
|
@@ -1695,10 +1695,11 @@ const COMMAND_HELP_BODIES = {
|
|
|
1695
1695
|
'engine adapter. Safe to run anywhere; no network calls.',
|
|
1696
1696
|
],
|
|
1697
1697
|
'prd-check': [
|
|
1698
|
-
'pugi prd-check <prd-path> | --all — Wave 6 verified-deliverable gate.',
|
|
1698
|
+
'pugi prd-check <prd-path> | --all | --session — Wave 6 verified-deliverable gate.',
|
|
1699
1699
|
'',
|
|
1700
|
+
'DEFAULT MODE — verify acceptance criteria against committed artifacts.',
|
|
1700
1701
|
'Reads a markdown PRD, parses the acceptance-criteria section, and',
|
|
1701
|
-
'runs verifiers
|
|
1702
|
+
'runs verifiers:',
|
|
1702
1703
|
' file:<path> fs.existsSync',
|
|
1703
1704
|
' test:<spec> spec file exists + has ≥1 test()/it() block',
|
|
1704
1705
|
' doc:<path> doc exists + has > 100 chars',
|
|
@@ -1708,6 +1709,13 @@ const COMMAND_HELP_BODIES = {
|
|
|
1708
1709
|
' --all Scan docs/prd/**.md instead of one file.',
|
|
1709
1710
|
' --json Emit a structured envelope to stdout.',
|
|
1710
1711
|
'',
|
|
1712
|
+
'SESSION MODE (Wave 6 final) — review the live session against the PRD.',
|
|
1713
|
+
'Walks up for PRD.md or apps/<app>/PRODUCT.md, reads the last 20 turns',
|
|
1714
|
+
'from .pugi/events.jsonl, and dispatches a cross-review subagent to',
|
|
1715
|
+
'list which requirements are SATISFIED and which remain OUTSTANDING.',
|
|
1716
|
+
'',
|
|
1717
|
+
' --session Run the session-review mode (no <path>, no --all).',
|
|
1718
|
+
'',
|
|
1711
1719
|
'Exit codes: 0 healthy · 1 failing · 2 unparsed / arg error.',
|
|
1712
1720
|
],
|
|
1713
1721
|
status: [
|
|
@@ -36,6 +36,7 @@ import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
36
36
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
37
37
|
import { parsePrd } from '../../core/prd-check/parser.js';
|
|
38
38
|
import { buildEnvelope, exitCodeFor, renderTable, } from '../../core/prd-check/reporter.js';
|
|
39
|
+
import { defaultSessionReviewDeps, renderSessionReview, runSessionReview, } from '../../core/prd-check/session-review.js';
|
|
39
40
|
import { createDefaultDeps, verifyAll, } from '../../core/prd-check/verifiers.js';
|
|
40
41
|
const DEFAULT_PRD_DIR = 'docs/prd';
|
|
41
42
|
/**
|
|
@@ -44,6 +45,39 @@ const DEFAULT_PRD_DIR = 'docs/prd';
|
|
|
44
45
|
* and sets `process.exitCode` to the worst verdict across the set.
|
|
45
46
|
*/
|
|
46
47
|
export async function runPrdCheckCommand(ctx) {
|
|
48
|
+
// Wave 6 final (2026-05-27): session-review mode. Orthogonal to
|
|
49
|
+
// the verifier-graph mode — no PRD path resolution, no command
|
|
50
|
+
// registry. The runner walks up for PRD.md, reads the NDJSON
|
|
51
|
+
// events log, and dispatches a cross-review subagent.
|
|
52
|
+
if (ctx.flags.session) {
|
|
53
|
+
const status = (line) => {
|
|
54
|
+
// Emit status lines through the output sink so the slash
|
|
55
|
+
// surface can render them inline. The status payload is
|
|
56
|
+
// intentionally lightweight — only the human-facing text.
|
|
57
|
+
ctx.writeOutput({
|
|
58
|
+
command: 'prd-check',
|
|
59
|
+
overall: 'healthy',
|
|
60
|
+
envelopes: [],
|
|
61
|
+
}, line);
|
|
62
|
+
};
|
|
63
|
+
const sessionDeps = ctx.sessionDeps ??
|
|
64
|
+
defaultSessionReviewDeps({ onStatus: status });
|
|
65
|
+
const review = await runSessionReview(ctx.cwd, sessionDeps);
|
|
66
|
+
const overall = review.status === 'ok'
|
|
67
|
+
? review.outstanding.length === 0
|
|
68
|
+
? 'healthy'
|
|
69
|
+
: 'failing'
|
|
70
|
+
: 'unparsed';
|
|
71
|
+
const result = {
|
|
72
|
+
command: 'prd-check',
|
|
73
|
+
overall,
|
|
74
|
+
envelopes: [],
|
|
75
|
+
sessionReview: review,
|
|
76
|
+
};
|
|
77
|
+
ctx.writeOutput(result, renderSessionReview(review));
|
|
78
|
+
process.exitCode = exitCodeFor(overall);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
47
81
|
const paths = resolveTargets(ctx);
|
|
48
82
|
if (paths.length === 0) {
|
|
49
83
|
const result = {
|
|
@@ -199,6 +233,7 @@ export function combineOverall(verdicts) {
|
|
|
199
233
|
export function parsePrdCheckArgs(args, options) {
|
|
200
234
|
let json = options.jsonDefault;
|
|
201
235
|
let all = false;
|
|
236
|
+
let session = false;
|
|
202
237
|
let prdPath;
|
|
203
238
|
for (const arg of args) {
|
|
204
239
|
if (arg === '--json') {
|
|
@@ -209,6 +244,10 @@ export function parsePrdCheckArgs(args, options) {
|
|
|
209
244
|
all = true;
|
|
210
245
|
continue;
|
|
211
246
|
}
|
|
247
|
+
if (arg === '--session') {
|
|
248
|
+
session = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
212
251
|
if (arg.startsWith('--')) {
|
|
213
252
|
return { ok: false, error: `unknown flag: ${arg}` };
|
|
214
253
|
}
|
|
@@ -217,10 +256,21 @@ export function parsePrdCheckArgs(args, options) {
|
|
|
217
256
|
}
|
|
218
257
|
prdPath = arg;
|
|
219
258
|
}
|
|
220
|
-
|
|
259
|
+
// Wave 6 final (2026-05-27): `--session` is mutually exclusive with
|
|
260
|
+
// the verifier modes — it walks up for a PRD, reads NDJSON turns,
|
|
261
|
+
// and dispatches a subagent. No <path>, no --all, no command-
|
|
262
|
+
// registry inputs. Validating up-front gives operators a clear
|
|
263
|
+
// error instead of a silent fall-through.
|
|
264
|
+
if (session && (all || prdPath !== undefined)) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
error: 'cannot combine --session with --all or <path>',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (!session && !all && prdPath === undefined) {
|
|
221
271
|
return {
|
|
222
272
|
ok: false,
|
|
223
|
-
error: 'pugi prd-check <prd-path> | --all (pass a PRD path
|
|
273
|
+
error: 'pugi prd-check <prd-path> | --all | --session (pass a PRD path, --all to scan docs/prd/**.md, or --session to review the live session against PRD.md)',
|
|
224
274
|
};
|
|
225
275
|
}
|
|
226
276
|
if (all && prdPath !== undefined) {
|
|
@@ -228,7 +278,7 @@ export function parsePrdCheckArgs(args, options) {
|
|
|
228
278
|
}
|
|
229
279
|
return {
|
|
230
280
|
ok: true,
|
|
231
|
-
flags: { json, all },
|
|
281
|
+
flags: { json, all, session },
|
|
232
282
|
...(prdPath !== undefined ? { prdPath } : {}),
|
|
233
283
|
};
|
|
234
284
|
}
|
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.31');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.31",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"undici": "^8.3.0",
|
|
55
55
|
"zod": "^3.23.0",
|
|
56
56
|
"@pugi/personas": "0.1.2",
|
|
57
|
-
"@pugi/sdk": "0.1.0-beta.
|
|
57
|
+
"@pugi/sdk": "0.1.0-beta.31"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.0.0",
|