@pugi/cli 0.1.0-beta.3 → 0.1.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -40
  3. package/bin/run.js +33 -1
  4. package/dist/commands/jobs-watch.js +201 -0
  5. package/dist/commands/jobs.js +15 -0
  6. package/dist/core/agent-progress/cleanup.js +134 -0
  7. package/dist/core/agent-progress/schema.js +144 -0
  8. package/dist/core/agent-progress/writer.js +101 -0
  9. package/dist/core/artifact-chain/dispatcher.js +148 -0
  10. package/dist/core/artifact-chain/exporter.js +164 -0
  11. package/dist/core/artifact-chain/state.js +243 -0
  12. package/dist/core/artifact-chain/steps.js +169 -0
  13. package/dist/core/auth/env-provider.js +238 -0
  14. package/dist/core/auto-update/channels.js +122 -0
  15. package/dist/core/auto-update/checker.js +241 -0
  16. package/dist/core/auto-update/state.js +235 -0
  17. package/dist/core/bare-mode/index.js +107 -0
  18. package/dist/core/checkpoint/resumer.js +149 -0
  19. package/dist/core/checkpoint/rewinder.js +291 -0
  20. package/dist/core/compact/auto-trigger.js +96 -0
  21. package/dist/core/compact/buffer-rewriter.js +115 -0
  22. package/dist/core/compact/summarizer.js +208 -0
  23. package/dist/core/compact/token-counter.js +108 -0
  24. package/dist/core/consensus/diff-capture.js +73 -0
  25. package/dist/core/context/index.js +7 -0
  26. package/dist/core/context/markdown-traverse.js +255 -0
  27. package/dist/core/cost/rate-card.js +129 -0
  28. package/dist/core/cost/tracker.js +221 -0
  29. package/dist/core/denial-tracking/index.js +8 -0
  30. package/dist/core/denial-tracking/state.js +264 -0
  31. package/dist/core/diagnostics/probe-runner.js +93 -0
  32. package/dist/core/diagnostics/probes/api.js +46 -0
  33. package/dist/core/diagnostics/probes/auth.js +86 -0
  34. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  35. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  36. package/dist/core/diagnostics/probes/config.js +72 -0
  37. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  38. package/dist/core/diagnostics/probes/disk.js +81 -0
  39. package/dist/core/diagnostics/probes/git.js +65 -0
  40. package/dist/core/diagnostics/probes/mcp.js +75 -0
  41. package/dist/core/diagnostics/probes/node.js +59 -0
  42. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  43. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  44. package/dist/core/diagnostics/probes/session.js +74 -0
  45. package/dist/core/diagnostics/probes/status-snapshot.js +442 -0
  46. package/dist/core/diagnostics/probes/workspace.js +63 -0
  47. package/dist/core/diagnostics/types.js +70 -0
  48. package/dist/core/dispatch/cache-cleanup.js +197 -0
  49. package/dist/core/dispatch/cache-handoff.js +295 -0
  50. package/dist/core/edits/dispatch.js +218 -2
  51. package/dist/core/edits/journal.js +199 -0
  52. package/dist/core/edits/layer-d-ast.js +557 -14
  53. package/dist/core/edits/verify-hook.js +273 -0
  54. package/dist/core/edits/worktree.js +111 -18
  55. package/dist/core/engine/anvil-client.js +115 -5
  56. package/dist/core/engine/budgets.js +89 -0
  57. package/dist/core/engine/context-prefix.js +155 -0
  58. package/dist/core/engine/intent.js +260 -0
  59. package/dist/core/engine/native-pugi.js +852 -210
  60. package/dist/core/engine/prompts.js +89 -6
  61. package/dist/core/engine/strip-internal-fields.js +124 -0
  62. package/dist/core/engine/tool-bridge.js +972 -33
  63. package/dist/core/feedback/queue.js +177 -0
  64. package/dist/core/feedback/submitter.js +145 -0
  65. package/dist/core/file-cache.js +113 -1
  66. package/dist/core/hooks/events.js +44 -0
  67. package/dist/core/hooks/index.js +15 -0
  68. package/dist/core/hooks/registry.js +213 -0
  69. package/dist/core/hooks/runner.js +236 -0
  70. package/dist/core/init/scaffold.js +195 -0
  71. package/dist/core/lsp/cache.js +105 -0
  72. package/dist/core/lsp/client.js +174 -29
  73. package/dist/core/lsp/language-detect.js +66 -0
  74. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  75. package/dist/core/mcp/client.js +75 -6
  76. package/dist/core/mcp/http-server.js +553 -0
  77. package/dist/core/mcp/permission.js +190 -0
  78. package/dist/core/mcp/registry.js +24 -2
  79. package/dist/core/mcp/server-tools.js +219 -0
  80. package/dist/core/mcp/server.js +397 -0
  81. package/dist/core/memory/dual-write.js +416 -0
  82. package/dist/core/memory/dual-write.spec.js +297 -0
  83. package/dist/core/memory/phase1-kinds.js +20 -0
  84. package/dist/core/memory-sync/queue.js +158 -0
  85. package/dist/core/memory-sync/queue.spec.js +105 -0
  86. package/dist/core/onboarding/marker.js +111 -0
  87. package/dist/core/onboarding/telemetry-state.js +108 -0
  88. package/dist/core/output-style/presets.js +176 -0
  89. package/dist/core/output-style/state.js +185 -0
  90. package/dist/core/permissions/gate.js +187 -0
  91. package/dist/core/permissions/index.js +18 -0
  92. package/dist/core/permissions/mode.js +102 -0
  93. package/dist/core/permissions/state.js +215 -0
  94. package/dist/core/permissions/tool-class.js +93 -0
  95. package/dist/core/prd-check/parser.js +215 -0
  96. package/dist/core/prd-check/reporter.js +127 -0
  97. package/dist/core/prd-check/verifiers.js +223 -0
  98. package/dist/core/pugi-md/context-injector.js +76 -0
  99. package/dist/core/pugi-md/walk-up.js +207 -0
  100. package/dist/core/release-notes/parser.js +241 -0
  101. package/dist/core/release-notes/state.js +116 -0
  102. package/dist/core/repl/codebase-survey.js +308 -0
  103. package/dist/core/repl/history.js +11 -1
  104. package/dist/core/repl/init-interview.js +457 -0
  105. package/dist/core/repl/model-pricing.js +135 -0
  106. package/dist/core/repl/onboarding-state.js +297 -0
  107. package/dist/core/repl/session.js +1486 -30
  108. package/dist/core/repl/slash-commands.js +345 -9
  109. package/dist/core/repl/store/session-store.js +31 -2
  110. package/dist/core/repl/workspace-context.js +22 -0
  111. package/dist/core/repo-map/build.js +125 -0
  112. package/dist/core/repo-map/cache.js +185 -0
  113. package/dist/core/repo-map/extractor.js +254 -0
  114. package/dist/core/repo-map/formatter.js +145 -0
  115. package/dist/core/repo-map/scanner.js +211 -0
  116. package/dist/core/retry-budget/budget.js +284 -0
  117. package/dist/core/retry-budget/index.js +5 -0
  118. package/dist/core/session.js +44 -0
  119. package/dist/core/settings.js +80 -0
  120. package/dist/core/share/formatter.js +271 -0
  121. package/dist/core/share/redactor.js +221 -0
  122. package/dist/core/share/uploader.js +267 -0
  123. package/dist/core/skills/defaults.js +457 -0
  124. package/dist/core/subagents/dispatcher-real.js +600 -0
  125. package/dist/core/subagents/dispatcher.js +113 -24
  126. package/dist/core/subagents/index.js +18 -5
  127. package/dist/core/subagents/isolation-matrix.js +213 -0
  128. package/dist/core/subagents/spawn.js +19 -4
  129. package/dist/core/telemetry/emitter.js +229 -0
  130. package/dist/core/telemetry/queue.js +251 -0
  131. package/dist/core/theme/context.js +91 -0
  132. package/dist/core/theme/presets.js +228 -0
  133. package/dist/core/theme/state.js +181 -0
  134. package/dist/core/todos/invariant.js +10 -0
  135. package/dist/core/todos/state.js +177 -0
  136. package/dist/core/transport/version-interceptor.js +166 -0
  137. package/dist/core/vim/keymap.js +288 -0
  138. package/dist/core/vim/state.js +92 -0
  139. package/dist/index.js +28 -0
  140. package/dist/runtime/bootstrap.js +190 -0
  141. package/dist/runtime/cli.js +2595 -278
  142. package/dist/runtime/commands/chain.js +489 -0
  143. package/dist/runtime/commands/compact.js +297 -0
  144. package/dist/runtime/commands/cost.js +199 -0
  145. package/dist/runtime/commands/delegate.js +312 -0
  146. package/dist/runtime/commands/dispatch.js +126 -0
  147. package/dist/runtime/commands/doctor.js +390 -0
  148. package/dist/runtime/commands/feedback.js +184 -0
  149. package/dist/runtime/commands/hooks.js +184 -0
  150. package/dist/runtime/commands/lsp.js +212 -28
  151. package/dist/runtime/commands/mcp.js +824 -0
  152. package/dist/runtime/commands/memory.js +508 -0
  153. package/dist/runtime/commands/memory.spec.js +174 -0
  154. package/dist/runtime/commands/model.js +237 -0
  155. package/dist/runtime/commands/onboarding.js +275 -0
  156. package/dist/runtime/commands/patch.js +17 -0
  157. package/dist/runtime/commands/permissions.js +87 -0
  158. package/dist/runtime/commands/plan.js +143 -0
  159. package/dist/runtime/commands/prd-check.js +235 -0
  160. package/dist/runtime/commands/release-notes.js +229 -0
  161. package/dist/runtime/commands/repo-map.js +95 -0
  162. package/dist/runtime/commands/report.js +299 -0
  163. package/dist/runtime/commands/resume.js +118 -0
  164. package/dist/runtime/commands/review-consensus.js +17 -2
  165. package/dist/runtime/commands/rewind.js +333 -0
  166. package/dist/runtime/commands/roster.js +117 -0
  167. package/dist/runtime/commands/sessions.js +163 -0
  168. package/dist/runtime/commands/share.js +316 -0
  169. package/dist/runtime/commands/status.js +178 -0
  170. package/dist/runtime/commands/stickers.js +82 -0
  171. package/dist/runtime/commands/style.js +194 -0
  172. package/dist/runtime/commands/theme.js +196 -0
  173. package/dist/runtime/commands/update.js +289 -0
  174. package/dist/runtime/commands/vim.js +140 -0
  175. package/dist/runtime/commands/worktree.js +50 -6
  176. package/dist/runtime/headless.js +543 -0
  177. package/dist/runtime/load-hooks-or-exit.js +71 -0
  178. package/dist/runtime/plan-decompose.js +531 -0
  179. package/dist/runtime/version.js +65 -0
  180. package/dist/tools/agent-tool.js +229 -0
  181. package/dist/tools/apply-patch.js +281 -39
  182. package/dist/tools/ask-user-question.js +213 -0
  183. package/dist/tools/ask-user.js +115 -0
  184. package/dist/tools/file-tools.js +85 -14
  185. package/dist/tools/mcp-tool.js +260 -0
  186. package/dist/tools/multi-edit.js +361 -0
  187. package/dist/tools/registry.js +30 -2
  188. package/dist/tools/skill-tool.js +96 -0
  189. package/dist/tools/tasks.js +208 -0
  190. package/dist/tools/todo-write.js +184 -0
  191. package/dist/tools/web-fetch.js +147 -2
  192. package/dist/tools/web-search.js +458 -0
  193. package/dist/tui/agent-progress-card.js +111 -0
  194. package/dist/tui/agent-tree.js +10 -0
  195. package/dist/tui/ask-modal.js +2 -2
  196. package/dist/tui/ask-user-question-prompt.js +192 -0
  197. package/dist/tui/compact-banner.js +81 -0
  198. package/dist/tui/conversation-pane.js +82 -8
  199. package/dist/tui/cost-table.js +111 -0
  200. package/dist/tui/doctor-table.js +46 -0
  201. package/dist/tui/feedback-prompt.js +156 -0
  202. package/dist/tui/input-box.js +46 -2
  203. package/dist/tui/markdown-render.js +4 -4
  204. package/dist/tui/onboarding-wizard.js +240 -0
  205. package/dist/tui/repl-render.js +293 -35
  206. package/dist/tui/repl-splash.js +2 -2
  207. package/dist/tui/repl.js +45 -13
  208. package/dist/tui/splash.js +1 -1
  209. package/dist/tui/status-bar.js +94 -16
  210. package/dist/tui/status-table.js +7 -0
  211. package/dist/tui/stickers-art.js +136 -0
  212. package/dist/tui/style-table.js +28 -0
  213. package/dist/tui/theme-table.js +29 -0
  214. package/dist/tui/tool-stream-pane.js +7 -0
  215. package/dist/tui/update-banner.js +20 -2
  216. package/dist/tui/vim-input.js +267 -0
  217. package/docs/examples/codegraph.mcp.json +10 -0
  218. package/package.json +9 -6
@@ -0,0 +1,143 @@
1
+ /**
2
+ * `pugi plan` / `/plan` — Leak L7 quick mode-switch shortcut.
3
+ *
4
+ * `/plan` is the slick UX shortcut for `/permissions plan`: one keystroke
5
+ * (well, five) puts the gate into plan mode + surfaces a banner so the
6
+ * operator knows write/dispatch tools are refused. The model goes off and
7
+ * thinks / researches without side effects until the operator types
8
+ * `/plan --back` (restore previous mode) or explicitly flips with
9
+ * `/permissions <mode>`.
10
+ *
11
+ * The slash and CLI surfaces both go through `runPlanCommand` — same
12
+ * separation as `runPermissionsCommand`. The runtime is I/O free w.r.t.
13
+ * the engine; the optional one-shot dispatch (`/plan <prompt>`) is
14
+ * handled by the CLI dispatcher AFTER this helper sets the workspace
15
+ * mode so the existing `runEngineTask('plan')` path sees plan mode as
16
+ * the workspace state without needing a parallel code path.
17
+ *
18
+ * Verdicts (the helper returns one so the caller can decide what to do
19
+ * after the mode write — print the banner, dispatch the engine, no-op):
20
+ * - `entered` — first `/plan` from a non-plan mode. Print the
21
+ * banner. Caller may then run a one-shot prompt.
22
+ * - `already-in-plan` — `/plan` while already in plan. No-op + show
23
+ * current. No banner reprint.
24
+ * - `reverted` — `/plan --back` popped the snapshot. Print a
25
+ * one-line confirmation; no banner.
26
+ * - `no-previous` — `/plan --back` without a snapshot. Print a
27
+ * clear "nothing to revert" line.
28
+ * - `persisted` — `/plan --persist` wrote the global default
29
+ * AND set workspace state to plan. Banner +
30
+ * persistence-confirmation line.
31
+ *
32
+ * `previousMode` semantics: stashed BEFORE the workspace write on
33
+ * `entered` / `persisted`. Cleared after a successful `reverted` so a
34
+ * second `--back` reports `no-previous` instead of looping back to plan.
35
+ */
36
+ import { PERMISSION_MODES, PERMISSION_MODE_GLOSS, getCurrentMode, getGlobalDefaultMode, getPreviousMode, setCurrentMode, setGlobalDefaultMode, setPreviousMode, } from '../../core/permissions/index.js';
37
+ /**
38
+ * Run the `/plan` flow. Side effects:
39
+ *
40
+ * command.back = true:
41
+ * - If a previousMode snapshot exists → restore it, clear snapshot,
42
+ * return `reverted`.
43
+ * - Otherwise → no writes, return `no-previous`.
44
+ *
45
+ * command.back = false, current mode is plan:
46
+ * - If `--persist`, write global config (no workspace re-write — it
47
+ * is already plan).
48
+ * - Print "already in plan" + the banner-summary line. Return
49
+ * `already-in-plan`.
50
+ *
51
+ * command.back = false, current mode is NOT plan:
52
+ * - Snapshot current mode → previousPermissionMode.
53
+ * - Write workspace mode = plan.
54
+ * - If `--persist`, also write global config.
55
+ * - Print the banner + (if persisted) the persistence line.
56
+ * - Return `entered` or `persisted`.
57
+ *
58
+ * --back + --persist is a no-op for persistence (revert never writes
59
+ * global config) but the revert itself fires.
60
+ */
61
+ export async function runPlanCommand(command, ctx) {
62
+ const current = effectiveMode(ctx);
63
+ if (command.back) {
64
+ const prev = getPreviousMode(ctx.workspaceRoot);
65
+ if (!prev) {
66
+ ctx.writeOutput(`No previous mode to restore. Current: ${current}. Use \`/permissions <mode>\` to switch explicitly.`);
67
+ return { verdict: 'no-previous', mode: current };
68
+ }
69
+ setCurrentMode(ctx.workspaceRoot, prev);
70
+ setPreviousMode(ctx.workspaceRoot, null);
71
+ ctx.writeOutput(`Switched back to '${prev}' mode. ${PERMISSION_MODE_GLOSS[prev]}`);
72
+ return { verdict: 'reverted', mode: prev };
73
+ }
74
+ if (current === 'plan') {
75
+ // Repeat /plan in plan mode is a no-op for the mode write, but
76
+ // --persist still honours the operator's intent to lock plan as
77
+ // the global default for future sessions.
78
+ if (command.persist) {
79
+ setGlobalDefaultMode('plan', ctx.homeDir);
80
+ ctx.writeOutput('Already in plan mode. Persisted plan as the default for future sessions (~/.pugi/config.json).');
81
+ return { verdict: 'persisted', mode: 'plan' };
82
+ }
83
+ ctx.writeOutput(`Already in plan mode. ${PERMISSION_MODE_GLOSS.plan} Switch back with \`/plan --back\` or \`/permissions <mode>\`.`);
84
+ return { verdict: 'already-in-plan', mode: 'plan' };
85
+ }
86
+ // Entering plan mode from a non-plan baseline. Stash the current mode
87
+ // BEFORE the write so /plan --back can pop it. We intentionally use the
88
+ // observed effective mode (workspace || global || default) rather than
89
+ // strictly the workspace value — if the operator's previous mode was
90
+ // sourced from the global config, `--back` should restore that observed
91
+ // state, not silently degrade to default.
92
+ setPreviousMode(ctx.workspaceRoot, current);
93
+ setCurrentMode(ctx.workspaceRoot, 'plan');
94
+ if (command.persist) {
95
+ setGlobalDefaultMode('plan', ctx.homeDir);
96
+ }
97
+ for (const line of renderPlanBanner()) {
98
+ ctx.writeOutput(line);
99
+ }
100
+ if (command.persist) {
101
+ ctx.writeOutput('Persisted plan as the default for future sessions (~/.pugi/config.json).');
102
+ return { verdict: 'persisted', mode: 'plan' };
103
+ }
104
+ return { verdict: 'entered', mode: 'plan' };
105
+ }
106
+ /**
107
+ * Render the plan-mode banner as a sequence of lines. The slash + CLI
108
+ * surfaces both print these line-by-line through their respective
109
+ * `writeOutput` sinks so the Ink REPL conversation pane and the plain
110
+ * stdout pipeline render identically.
111
+ *
112
+ * The box-drawing uses light-line glyphs (U+2500 family) which render in
113
+ * every modern terminal we target (Linux/macOS/Windows Terminal/iTerm/
114
+ * Ghostty/Alacritty). No emoji per brand-voice gate.
115
+ */
116
+ export function renderPlanBanner() {
117
+ return [
118
+ '┌─ Plan mode active ────────────────────────────────────────┐',
119
+ '│ Read-only tools allowed. Write/dispatch tools blocked. │',
120
+ '│ Pugi will think + research without making changes. │',
121
+ '│ Switch back: /plan --back or /permissions <mode> │',
122
+ '└───────────────────────────────────────────────────────────┘',
123
+ ];
124
+ }
125
+ /**
126
+ * Resolve the effective mode at the moment the helper was invoked,
127
+ * mirroring `resolveMode` but without taking a CLI flag (the `/plan`
128
+ * helper is called AFTER the top-level `--mode` flag has been applied
129
+ * to the workspace, so the file state is the source of truth here).
130
+ */
131
+ function effectiveMode(ctx) {
132
+ const workspace = getCurrentMode(ctx.workspaceRoot);
133
+ if (workspace)
134
+ return workspace;
135
+ const global = getGlobalDefaultMode(ctx.homeDir);
136
+ if (global)
137
+ return global;
138
+ // Defensive: PERMISSION_MODES[1] is 'ask' (the canonical default). We
139
+ // index off the canonical list rather than re-import DEFAULT_PERMISSION_MODE
140
+ // here to keep the symbol surface narrow.
141
+ return PERMISSION_MODES[1] ?? 'ask';
142
+ }
143
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * `pugi prd-check` — Pugi α7 Wave 6 verified-deliverable gate (2026-05-27).
3
+ *
4
+ * Runs a PRD ↔ delivered-artifact verification sweep BEFORE the
5
+ * operator (or an autonomous agent) claims a feature is done.
6
+ * Reads `docs/prd/<feature>.md` (or any explicit path), parses the
7
+ * acceptance-criteria section, and runs file / test / doc / command
8
+ * / route verifiers per criterion. Output mirrors `pugi doctor`:
9
+ * either a plain-text table or a JSON envelope (`--json`).
10
+ *
11
+ * Module contract (mirrors L17 doctor split):
12
+ *
13
+ * - parser + verifiers + reporter are pure with respect to deps;
14
+ * this file is the THIN wiring that resolves cwd, glob-expands
15
+ * `--all`, loads each PRD, and forwards the structured result
16
+ * к the supplied writeOutput sink.
17
+ *
18
+ * - `runPrdCheckCommand` is the single entry point. Both the
19
+ * top-level `pugi prd-check` shell command AND the in-REPL
20
+ * `/prd-check` slash command call it. The function returns the
21
+ * `PrdCheckEnvelope[]` so the REPL can render via Ink without
22
+ * re-running the verification.
23
+ *
24
+ * - Exit codes follow `exitCodeFor` from the reporter:
25
+ * 0 — healthy (every criterion PASS or SKIPPED)
26
+ * 1 — failing (≥1 FAIL across the scanned PRDs)
27
+ * 2 — unparsed (≥1 PRD missing the acceptance section)
28
+ * When `--all` scans multiple PRDs the worst verdict wins (1 > 2 > 0).
29
+ *
30
+ * - The `knownCommands` set is sourced from the CLI registry — we
31
+ * accept it as an injected parameter so the spec can drive
32
+ * command-verification without importing the entire cli.ts
33
+ * module (which would pull the engine graph into the test).
34
+ */
35
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
36
+ import { isAbsolute, join, relative, resolve } from 'node:path';
37
+ import { parsePrd } from '../../core/prd-check/parser.js';
38
+ import { buildEnvelope, exitCodeFor, renderTable, } from '../../core/prd-check/reporter.js';
39
+ import { createDefaultDeps, verifyAll, } from '../../core/prd-check/verifiers.js';
40
+ const DEFAULT_PRD_DIR = 'docs/prd';
41
+ /**
42
+ * Run the gate. Resolves which PRDs to inspect, runs the parser +
43
+ * verifiers + reporter chain per PRD, emits the combined output,
44
+ * and sets `process.exitCode` to the worst verdict across the set.
45
+ */
46
+ export async function runPrdCheckCommand(ctx) {
47
+ const paths = resolveTargets(ctx);
48
+ if (paths.length === 0) {
49
+ const result = {
50
+ command: 'prd-check',
51
+ overall: 'unparsed',
52
+ envelopes: [],
53
+ };
54
+ ctx.writeOutput(result, 'No PRD files found.');
55
+ process.exitCode = exitCodeFor('unparsed');
56
+ return result;
57
+ }
58
+ const deps = ctx.deps ??
59
+ createDefaultDeps({
60
+ workspaceRoot: ctx.cwd,
61
+ knownCommands: ctx.knownCommands,
62
+ });
63
+ const envelopes = [];
64
+ for (const path of paths) {
65
+ envelopes.push(checkSinglePrd(path, ctx.cwd, deps));
66
+ }
67
+ const overall = combineOverall(envelopes.map((e) => e.overall));
68
+ const result = {
69
+ command: 'prd-check',
70
+ overall,
71
+ envelopes,
72
+ };
73
+ const text = renderRun(result, ctx.cwd);
74
+ ctx.writeOutput(result, text);
75
+ process.exitCode = exitCodeFor(overall);
76
+ return result;
77
+ }
78
+ /**
79
+ * Render the combined plain-text view. Multi-PRD runs print one
80
+ * table per envelope separated by a divider; single-PRD runs print
81
+ * just the table to keep the output narrow.
82
+ */
83
+ function renderRun(result, cwd) {
84
+ if (result.envelopes.length === 0) {
85
+ return 'No PRD files found.';
86
+ }
87
+ if (result.envelopes.length === 1) {
88
+ return renderTable(result.envelopes[0]);
89
+ }
90
+ const parts = [];
91
+ for (const envelope of result.envelopes) {
92
+ const relPath = relative(cwd, envelope.prdPath) || envelope.prdPath;
93
+ parts.push(renderTable({ ...envelope, prdPath: relPath }));
94
+ parts.push('');
95
+ }
96
+ parts.push('-'.repeat(50));
97
+ const summary = `${result.envelopes.length} PRD(s) scanned. Overall: ${result.overall.toUpperCase()}`;
98
+ parts.push(summary);
99
+ return parts.join('\n');
100
+ }
101
+ function checkSinglePrd(prdPath, workspaceRoot, deps) {
102
+ let source;
103
+ try {
104
+ source = readFileSync(prdPath, 'utf8');
105
+ }
106
+ catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ return {
109
+ command: 'prd-check',
110
+ prdPath: relative(workspaceRoot, prdPath) || prdPath,
111
+ title: null,
112
+ overall: 'unparsed',
113
+ counts: { pass: 0, fail: 0, skipped: 0 },
114
+ criteria: [
115
+ {
116
+ index: 0,
117
+ text: `PRD file unreadable: ${message}`,
118
+ status: 'fail',
119
+ results: [],
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ const parsed = parsePrd(source);
125
+ const verified = verifyAll(parsed.criteria, deps);
126
+ return buildEnvelope({
127
+ prdPath: relative(workspaceRoot, prdPath) || prdPath,
128
+ title: parsed.title,
129
+ hasAcceptanceSection: parsed.hasAcceptanceSection,
130
+ verified,
131
+ });
132
+ }
133
+ function resolveTargets(ctx) {
134
+ if (ctx.flags.all) {
135
+ const prdDir = resolve(ctx.cwd, DEFAULT_PRD_DIR);
136
+ return listMarkdownFiles(prdDir);
137
+ }
138
+ if (ctx.prdPath) {
139
+ const absolute = isAbsolute(ctx.prdPath)
140
+ ? ctx.prdPath
141
+ : resolve(ctx.cwd, ctx.prdPath);
142
+ return [absolute];
143
+ }
144
+ return [];
145
+ }
146
+ function listMarkdownFiles(dir) {
147
+ let entries;
148
+ try {
149
+ entries = readdirSync(dir);
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ const out = [];
155
+ for (const entry of entries) {
156
+ const full = join(dir, entry);
157
+ let isDirectory = false;
158
+ try {
159
+ isDirectory = statSync(full).isDirectory();
160
+ }
161
+ catch {
162
+ continue;
163
+ }
164
+ if (isDirectory) {
165
+ out.push(...listMarkdownFiles(full));
166
+ continue;
167
+ }
168
+ if (entry.endsWith('.md')) {
169
+ out.push(full);
170
+ }
171
+ }
172
+ return out.sort();
173
+ }
174
+ /**
175
+ * Combine per-PRD verdicts into the run-wide one. failing > unparsed > healthy.
176
+ * Exported for the spec.
177
+ */
178
+ export function combineOverall(verdicts) {
179
+ if (verdicts.length === 0)
180
+ return 'unparsed';
181
+ if (verdicts.some((v) => v === 'failing'))
182
+ return 'failing';
183
+ if (verdicts.some((v) => v === 'unparsed'))
184
+ return 'unparsed';
185
+ return 'healthy';
186
+ }
187
+ /**
188
+ * Parse the CLI argv tail. Accepts:
189
+ *
190
+ * pugi prd-check -> error (no target)
191
+ * pugi prd-check <path> -> single PRD
192
+ * pugi prd-check --all -> scan docs/prd/**.md
193
+ * pugi prd-check <path> --json -> single PRD, JSON envelope
194
+ *
195
+ * `--json` is also forwarded from the global flag set in cli.ts;
196
+ * the local parse re-honours it so the slash command can use the
197
+ * same parser without the global flag plumbing.
198
+ */
199
+ export function parsePrdCheckArgs(args, options) {
200
+ let json = options.jsonDefault;
201
+ let all = false;
202
+ let prdPath;
203
+ for (const arg of args) {
204
+ if (arg === '--json') {
205
+ json = true;
206
+ continue;
207
+ }
208
+ if (arg === '--all') {
209
+ all = true;
210
+ continue;
211
+ }
212
+ if (arg.startsWith('--')) {
213
+ return { ok: false, error: `unknown flag: ${arg}` };
214
+ }
215
+ if (prdPath !== undefined) {
216
+ return { ok: false, error: `unexpected extra argument: ${arg}` };
217
+ }
218
+ prdPath = arg;
219
+ }
220
+ if (!all && prdPath === undefined) {
221
+ return {
222
+ ok: false,
223
+ error: 'pugi prd-check <prd-path> | --all (pass a PRD path or --all to scan docs/prd/**.md)',
224
+ };
225
+ }
226
+ if (all && prdPath !== undefined) {
227
+ return { ok: false, error: 'cannot combine <path> with --all' };
228
+ }
229
+ return {
230
+ ok: true,
231
+ flags: { json, all },
232
+ ...(prdPath !== undefined ? { prdPath } : {}),
233
+ };
234
+ }
235
+ //# sourceMappingURL=prd-check.js.map
@@ -0,0 +1,229 @@
1
+ /**
2
+ * `pugi release-notes` — changelog diff between last-seen + current
3
+ * (Leak L24, 2026-05-27).
4
+ *
5
+ * Parity command with Claude Code's `/release-notes`, which shows
6
+ * what changed between the previously-installed CLI version and the
7
+ * currently-installed one. The Pugi variant reads the bundled
8
+ * `CHANGELOG.md`, slices it к the range `(last-seen, current]`, and
9
+ * renders the Markdown sections to the operator. After а successful
10
+ * render the marker is bumped к `current` so the next invocation is а
11
+ * no-op until the operator upgrades again.
12
+ *
13
+ * # Module contract
14
+ *
15
+ * - This file owns the WIRING from CLI flags + ambient state к the
16
+ * parser + state I/O helpers. The parser + state modules в
17
+ * `core/release-notes/` have zero coupling к the CLI dispatch
18
+ * surface.
19
+ *
20
+ * - `runReleaseNotesCommand` is the single entry point. Both the
21
+ * top-level `pugi release-notes` handler в `runtime/cli.ts` AND
22
+ * the in-REPL `/release-notes` slash command call it. The
23
+ * function returns а structured `ReleaseNotesResult` so the
24
+ * slash dispatcher can route the lines к the system pane
25
+ * without re-reading the changelog.
26
+ *
27
+ * - Exit code is ALWAYS 0. The command is informational, never а
28
+ * gate. Read failures, missing CHANGELOG, and write failures all
29
+ * degrade к а structured envelope with а human-readable footer.
30
+ *
31
+ * - The changelog source is captured behind а function so the spec
32
+ * can stub it without touching disk. The default reads the file
33
+ * bundled with the CLI install (resolved relative к the package
34
+ * root); fixtures pass an in-memory string.
35
+ *
36
+ * - `--reset` flag clears the last-seen marker AND re-renders the
37
+ * full bundled changelog as if the operator had never run the
38
+ * command. Distinct from а plain `--all` toggle because the
39
+ * reset PERSISTS (the next invocation again shows everything
40
+ * newer than the cleared marker — `none`).
41
+ */
42
+ import { existsSync, readFileSync } from 'node:fs';
43
+ import { homedir } from 'node:os';
44
+ import { dirname, resolve } from 'node:path';
45
+ import { fileURLToPath } from 'node:url';
46
+ import { parseChangelog, sliceVersionsBetween, } from '../../core/release-notes/parser.js';
47
+ import { clearLastSeenVersion, readLastSeenVersion, writeLastSeenVersion, } from '../../core/release-notes/state.js';
48
+ import { PUGI_CLI_VERSION } from '../version.js';
49
+ /**
50
+ * Default loader для the bundled `apps/pugi-cli/CHANGELOG.md`. The
51
+ * compiled bundle ships under `dist/runtime/commands/release-notes.js`;
52
+ * the CHANGELOG sits next к `package.json` at the package root, two
53
+ * directories up from `dist/runtime/commands/`. We also probe а
54
+ * couple of fallback locations so the dev path (running the source
55
+ * directly из `src/`) works без а compile step.
56
+ */
57
+ export function defaultReadChangelog() {
58
+ const candidates = resolveChangelogCandidates();
59
+ for (const candidate of candidates) {
60
+ try {
61
+ if (existsSync(candidate)) {
62
+ return readFileSync(candidate, 'utf8');
63
+ }
64
+ }
65
+ catch {
66
+ // Permission errors, transient FS hiccups — keep probing the
67
+ // remaining candidates. Returning null at the end is fine; the
68
+ // renderer surfaces а "changelog-missing" envelope.
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ function resolveChangelogCandidates() {
74
+ // import.meta.url points к the compiled JS in production
75
+ // (`dist/runtime/commands/release-notes.js`) and к the source TS в
76
+ // tests / dev runs. We probe both relative ancestries so either
77
+ // path lands on `<package>/CHANGELOG.md`.
78
+ try {
79
+ const here = dirname(fileURLToPath(import.meta.url));
80
+ return [
81
+ resolve(here, '../../..', 'CHANGELOG.md'),
82
+ resolve(here, '../../../..', 'CHANGELOG.md'),
83
+ resolve(process.cwd(), 'apps/pugi-cli/CHANGELOG.md'),
84
+ resolve(process.cwd(), 'CHANGELOG.md'),
85
+ ];
86
+ }
87
+ catch {
88
+ // Some non-ESM contexts (very old node, eval'd code) reject
89
+ // `import.meta.url`. Fall back к cwd-relative probes — works for
90
+ // tests that run from the package root.
91
+ return [
92
+ resolve(process.cwd(), 'apps/pugi-cli/CHANGELOG.md'),
93
+ resolve(process.cwd(), 'CHANGELOG.md'),
94
+ ];
95
+ }
96
+ }
97
+ /**
98
+ * Default home dir resolver. Centralised so the CLI handler can call
99
+ * `runReleaseNotesCommand` without re-importing `os.homedir`.
100
+ */
101
+ export function defaultReleaseNotesHome() {
102
+ return homedir();
103
+ }
104
+ /**
105
+ * Pick + render the release notes, hand the result к the output
106
+ * sink, persist the marker. Always exits 0.
107
+ */
108
+ export function runReleaseNotesCommand(ctx) {
109
+ const readChangelog = ctx.readChangelog ?? defaultReadChangelog;
110
+ const currentVersion = ctx.currentVersion ?? PUGI_CLI_VERSION;
111
+ // `--reset` clears the marker before slicing. We capture the
112
+ // pre-clear value so the JSON envelope still shows the operator
113
+ // what their previous marker was, which makes scripting + bug
114
+ // reports easier.
115
+ const lastSeenBefore = readLastSeenVersion(ctx.home);
116
+ if (ctx.reset) {
117
+ clearLastSeenVersion(ctx.home);
118
+ }
119
+ const lastSeen = ctx.reset ? null : lastSeenBefore;
120
+ const raw = readChangelog();
121
+ if (raw === null) {
122
+ const result = {
123
+ command: 'release-notes',
124
+ currentVersion,
125
+ lastSeenVersion: lastSeenBefore ?? 'none',
126
+ sections: [],
127
+ status: 'changelog-missing',
128
+ markerPersisted: false,
129
+ persistFailure: null,
130
+ text: renderMissingChangelog(currentVersion),
131
+ };
132
+ ctx.writeOutput(result, result.text);
133
+ process.exitCode = 0;
134
+ return result;
135
+ }
136
+ const sections = parseChangelog(raw);
137
+ if (sections.length === 0) {
138
+ const result = {
139
+ command: 'release-notes',
140
+ currentVersion,
141
+ lastSeenVersion: lastSeenBefore ?? 'none',
142
+ sections: [],
143
+ status: 'changelog-empty',
144
+ markerPersisted: false,
145
+ persistFailure: null,
146
+ text: renderEmptyChangelog(currentVersion),
147
+ };
148
+ ctx.writeOutput(result, result.text);
149
+ process.exitCode = 0;
150
+ return result;
151
+ }
152
+ const slice = sliceVersionsBetween(sections, lastSeen, currentVersion);
153
+ if (slice.length === 0) {
154
+ // Nothing new — render the no-op message and DO NOT touch the
155
+ // marker (marker already equals current OR is newer; either way
156
+ // re-writing it is а no-op write we can avoid).
157
+ const result = {
158
+ command: 'release-notes',
159
+ currentVersion,
160
+ lastSeenVersion: lastSeenBefore ?? 'none',
161
+ sections: [],
162
+ status: 'up-to-date',
163
+ markerPersisted: false,
164
+ persistFailure: null,
165
+ text: renderUpToDate(currentVersion, lastSeenBefore),
166
+ };
167
+ ctx.writeOutput(result, result.text);
168
+ process.exitCode = 0;
169
+ return result;
170
+ }
171
+ const persist = writeLastSeenVersion(ctx.home, currentVersion);
172
+ const text = renderSections(slice, currentVersion, lastSeen, persist);
173
+ const result = {
174
+ command: 'release-notes',
175
+ currentVersion,
176
+ lastSeenVersion: lastSeenBefore ?? 'none',
177
+ sections: slice,
178
+ status: ctx.reset ? 'reset' : 'rendered',
179
+ markerPersisted: persist.status === 'ok',
180
+ persistFailure: persist.status === 'failed' ? persist.reason : null,
181
+ text,
182
+ };
183
+ ctx.writeOutput(result, text);
184
+ process.exitCode = 0;
185
+ return result;
186
+ }
187
+ function renderSections(sections, current, lastSeen, persist) {
188
+ const header = lastSeen
189
+ ? `Pugi release notes — ${lastSeen} → ${current}`
190
+ : `Pugi release notes — up to ${current}`;
191
+ const blocks = [header, '═'.repeat(Math.max(header.length, 30))];
192
+ for (const section of sections) {
193
+ const subhead = section.date
194
+ ? `## [${section.version}] - ${section.date}`
195
+ : `## [${section.version}]`;
196
+ blocks.push('');
197
+ blocks.push(subhead);
198
+ if (section.body.length > 0) {
199
+ blocks.push('');
200
+ blocks.push(section.body);
201
+ }
202
+ }
203
+ if (persist.status === 'failed') {
204
+ blocks.push('');
205
+ blocks.push(`Warning: could not persist last-seen marker (${persist.reason}). Next run will surface the same notes.`);
206
+ }
207
+ return blocks.join('\n');
208
+ }
209
+ function renderUpToDate(current, lastSeen) {
210
+ const lines = ['No new release notes.'];
211
+ lines.push(`Installed: ${current}`);
212
+ lines.push(`Last seen: ${lastSeen ?? 'none'}`);
213
+ return lines.join('\n');
214
+ }
215
+ function renderMissingChangelog(current) {
216
+ return [
217
+ 'Release notes are not bundled with this install.',
218
+ `Installed: ${current}`,
219
+ 'See https://pugi.io/changelog for the rendered changelog.',
220
+ ].join('\n');
221
+ }
222
+ function renderEmptyChangelog(current) {
223
+ return [
224
+ 'Bundled changelog is empty — no parsed sections.',
225
+ `Installed: ${current}`,
226
+ 'See https://pugi.io/changelog for the rendered changelog.',
227
+ ].join('\n');
228
+ }
229
+ //# sourceMappingURL=release-notes.js.map