@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,215 @@
1
+ /**
2
+ * PRD parser — Pugi α7 Wave 6 (`/prd-check`, 2026-05-27).
3
+ *
4
+ * Reads a markdown PRD file and extracts the acceptance-criteria
5
+ * section into a list of verifiable criteria. The parser is
6
+ * intentionally narrow: it owns ONE function, accepts raw markdown
7
+ * source as a string (no fs I/O), and returns a structured list the
8
+ * verifier module can fan out over.
9
+ *
10
+ * Why two phases (parse → verify) instead of a single pass:
11
+ *
12
+ * - keeps the parser deterministic + fast (pure string in, JSON
13
+ * out — trivial to unit-test without touching the filesystem)
14
+ *
15
+ * - lets the reporter render the criterion list even when every
16
+ * verifier fails, so operators see WHAT failed before WHY
17
+ *
18
+ * - mirrors the L17 doctor split: `probe-runner` runs a set of
19
+ * probe descriptors → identical contract here, just for PRD
20
+ * acceptance items instead of environment probes
21
+ *
22
+ * Heading recognition is tolerant by design: PRD authors use both
23
+ * `## Acceptance Criteria` and `## Success Criteria`, sometimes with
24
+ * a trailing colon, sometimes inside an h3. We accept any h2/h3
25
+ * matching either label (case-insensitive). The first matching
26
+ * section wins; subsequent matches are ignored so a PRD with both
27
+ * sections does not double-count items.
28
+ *
29
+ * Item recognition supports two shapes documented in the wave-6
30
+ * spec:
31
+ *
32
+ * 1. numbered lists `1. <text>` / `1) <text>`
33
+ * 2. markdown checklists `- [ ] <text>` / `- [x] <text>`
34
+ *
35
+ * Either shape may include inline mentions the verifier extracts:
36
+ * file paths (`apps/foo/bar.ts`), test specs (`*.spec.ts`),
37
+ * route declarations (`GET /api/x`), CLI commands (`pugi prd-check`),
38
+ * and doc references (`docs/foo.md`). The parser captures these
39
+ * verbatim into `mentions` so the verifier module can fan checks
40
+ * without re-tokenising the prose.
41
+ */
42
+ const ACCEPTANCE_HEADING_RE = /^(#{2,3})\s+(acceptance criteria|success criteria|deliverables)\b\s*:?\s*$/i;
43
+ const ANY_HEADING_RE = /^(#{1,6})\s+\S/;
44
+ const TITLE_HEADING_RE = /^#\s+(.+?)\s*$/;
45
+ const NUMBERED_ITEM_RE = /^(\s*)(\d+)[\.)]\s+(.+?)\s*$/;
46
+ const CHECKLIST_ITEM_RE = /^(\s*)-\s+\[([ xX])\]\s+(.+?)\s*$/;
47
+ /**
48
+ * Public entry: parse a markdown PRD source into `ParsedPrd`. Pure
49
+ * function — no filesystem, no logging. The CLI handler is
50
+ * responsible for reading the file and forwarding the contents.
51
+ */
52
+ export function parsePrd(source) {
53
+ const lines = source.split(/\r?\n/);
54
+ const title = extractTitle(lines);
55
+ const range = findAcceptanceRange(lines);
56
+ if (!range) {
57
+ return { title, hasAcceptanceSection: false, criteria: [] };
58
+ }
59
+ const sectionLines = lines.slice(range.start, range.end);
60
+ const criteria = extractCriteria(sectionLines);
61
+ return { title, hasAcceptanceSection: true, criteria };
62
+ }
63
+ /**
64
+ * Extract verifiable mentions from a criterion text. Exported for
65
+ * the verifier spec so tests can drive mention classification
66
+ * without running the full parser.
67
+ */
68
+ export function extractMentions(text) {
69
+ const mentions = [];
70
+ const seen = new Set();
71
+ const push = (key, mention) => {
72
+ if (seen.has(key))
73
+ return;
74
+ seen.add(key);
75
+ mentions.push(mention);
76
+ };
77
+ // 1) Routes — `GET /api/path`, `POST /foo`, etc. Recognised
78
+ // BEFORE file paths because the trailing `/` could otherwise
79
+ // be mis-classified.
80
+ const routeRe = /\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\/[A-Za-z0-9_./:\-]+)/g;
81
+ for (const match of text.matchAll(routeRe)) {
82
+ const method = match[1].toUpperCase();
83
+ const path = match[2];
84
+ push(`route:${method} ${path}`, { kind: 'route', method, path });
85
+ }
86
+ // 2) Backtick-wrapped tokens — most reliable signal. The parser
87
+ // inspects each token and decides whether it is a path, a test
88
+ // spec, a CLI command, or a route.
89
+ const backtickRe = /`([^`\n]+)`/g;
90
+ for (const match of text.matchAll(backtickRe)) {
91
+ const raw = match[1].trim();
92
+ classifyToken(raw, push);
93
+ }
94
+ // 3) Bare paths with at least one slash + an extension. Authors
95
+ // sometimes forget the backticks; we still surface the file
96
+ // so the verifier can attempt the check.
97
+ const barePathRe = /(?<![A-Za-z0-9])((?:[a-zA-Z0-9_.\-]+\/)+[a-zA-Z0-9_.\-]+\.[a-zA-Z0-9]{1,6})/g;
98
+ for (const match of text.matchAll(barePathRe)) {
99
+ const path = match[1];
100
+ classifyPath(path, push);
101
+ }
102
+ return mentions;
103
+ }
104
+ function classifyToken(raw, push) {
105
+ const trimmed = raw.replace(/[,;.]+$/u, '').trim();
106
+ if (trimmed.length === 0)
107
+ return;
108
+ // Route shape inside backticks (`GET /api/x`).
109
+ const routeMatch = trimmed.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\/[A-Za-z0-9_./:\-]+)$/u);
110
+ if (routeMatch) {
111
+ const method = routeMatch[1].toUpperCase();
112
+ const path = routeMatch[2];
113
+ push(`route:${method} ${path}`, { kind: 'route', method, path });
114
+ return;
115
+ }
116
+ // Pugi command — `pugi <name>` or `/<name>`. Slash form covers
117
+ // REPL slash commands; pugi form covers shell commands.
118
+ const pugiCmdMatch = trimmed.match(/^pugi\s+([a-z][a-z0-9-]*)(?:\s+.*)?$/u);
119
+ if (pugiCmdMatch) {
120
+ const name = pugiCmdMatch[1];
121
+ push(`command:${name}`, { kind: 'command', name });
122
+ return;
123
+ }
124
+ const slashCmdMatch = trimmed.match(/^\/([a-z][a-z0-9-]*)$/u);
125
+ if (slashCmdMatch) {
126
+ const name = slashCmdMatch[1];
127
+ push(`command:${name}`, { kind: 'command', name });
128
+ return;
129
+ }
130
+ // Path shape — must contain at least one `/` AND an extension.
131
+ if (trimmed.includes('/') && /\.[a-zA-Z0-9]{1,6}$/.test(trimmed)) {
132
+ classifyPath(trimmed, push);
133
+ }
134
+ }
135
+ function classifyPath(path, push) {
136
+ if (/\.spec\.[a-z]+$|\.test\.[a-z]+$/u.test(path)) {
137
+ push(`test:${path}`, { kind: 'test', path });
138
+ return;
139
+ }
140
+ if (/^docs?\//u.test(path) || /\.md$/u.test(path)) {
141
+ push(`doc:${path}`, { kind: 'doc', path });
142
+ return;
143
+ }
144
+ push(`file:${path}`, { kind: 'file', path });
145
+ }
146
+ function extractTitle(lines) {
147
+ for (const line of lines) {
148
+ const match = line.match(TITLE_HEADING_RE);
149
+ if (match) {
150
+ return match[1].trim();
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ function findAcceptanceRange(lines) {
156
+ let startIdx = -1;
157
+ let startHeadingLevel = 0;
158
+ for (let i = 0; i < lines.length; i += 1) {
159
+ const line = lines[i];
160
+ const match = line.match(ACCEPTANCE_HEADING_RE);
161
+ if (match) {
162
+ startIdx = i + 1;
163
+ startHeadingLevel = match[1].length;
164
+ break;
165
+ }
166
+ }
167
+ if (startIdx === -1)
168
+ return null;
169
+ let endIdx = lines.length;
170
+ for (let i = startIdx; i < lines.length; i += 1) {
171
+ const line = lines[i];
172
+ const headingMatch = line.match(ANY_HEADING_RE);
173
+ if (!headingMatch)
174
+ continue;
175
+ const level = headingMatch[1].length;
176
+ if (level <= startHeadingLevel) {
177
+ endIdx = i;
178
+ break;
179
+ }
180
+ }
181
+ return { start: startIdx, end: endIdx };
182
+ }
183
+ function extractCriteria(sectionLines) {
184
+ const out = [];
185
+ let index = 0;
186
+ for (const line of sectionLines) {
187
+ const checklistMatch = line.match(CHECKLIST_ITEM_RE);
188
+ if (checklistMatch) {
189
+ index += 1;
190
+ const marker = checklistMatch[2].toLowerCase();
191
+ const text = checklistMatch[3];
192
+ out.push({
193
+ index,
194
+ text,
195
+ preChecked: marker === 'x',
196
+ mentions: extractMentions(text),
197
+ });
198
+ continue;
199
+ }
200
+ const numberedMatch = line.match(NUMBERED_ITEM_RE);
201
+ if (numberedMatch) {
202
+ index += 1;
203
+ const text = numberedMatch[3];
204
+ out.push({
205
+ index,
206
+ text,
207
+ preChecked: false,
208
+ mentions: extractMentions(text),
209
+ });
210
+ continue;
211
+ }
212
+ }
213
+ return out;
214
+ }
215
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1,127 @@
1
+ /**
2
+ * PRD-check reporter — Pugi α7 Wave 6 (`/prd-check`, 2026-05-27).
3
+ *
4
+ * Turns a list of `VerifiedCriterion` into either a plain-text
5
+ * table (matches the L17 doctor renderer layout for visual
6
+ * consistency) OR a structured JSON envelope for scripted callers.
7
+ *
8
+ * The reporter is intentionally render-only — it does not perform
9
+ * any verification work. The verifier module decides PASS / FAIL /
10
+ * SKIPPED; the reporter only formats those verdicts. This keeps
11
+ * the JSON envelope deterministic + diff-friendly between runs.
12
+ *
13
+ * Exit-code policy:
14
+ *
15
+ * - `healthy` -> every criterion PASS or SKIPPED. exit 0.
16
+ * - `failing` -> at least one FAIL. exit 1.
17
+ * - `unparsed` -> the PRD had no acceptance section. exit 2
18
+ * (operator authored a stub but never filled it
19
+ * in — distinct signal from "criteria don't
20
+ * verify yet" so CI can route differently).
21
+ */
22
+ /**
23
+ * Build the JSON envelope. Pure transform — no fs, no clock. The
24
+ * CLI handler wraps this in the writeOutput sink.
25
+ */
26
+ export function buildEnvelope(input) {
27
+ const counts = {
28
+ pass: 0,
29
+ fail: 0,
30
+ skipped: 0,
31
+ };
32
+ for (const v of input.verified) {
33
+ counts[v.status] += 1;
34
+ }
35
+ const overall = computeOverall(input.hasAcceptanceSection, counts);
36
+ return {
37
+ command: 'prd-check',
38
+ prdPath: input.prdPath,
39
+ title: input.title,
40
+ overall,
41
+ counts,
42
+ criteria: input.verified.map((v) => ({
43
+ index: v.criterion.index,
44
+ text: v.criterion.text,
45
+ status: v.status,
46
+ results: v.results.map((r) => ({
47
+ kind: r.mention.kind,
48
+ target: mentionTarget(r.mention),
49
+ status: r.status,
50
+ evidence: r.evidence,
51
+ })),
52
+ })),
53
+ };
54
+ }
55
+ /** Exit code for the resolved overall verdict. */
56
+ export function exitCodeFor(overall) {
57
+ switch (overall) {
58
+ case 'healthy':
59
+ return 0;
60
+ case 'failing':
61
+ return 1;
62
+ case 'unparsed':
63
+ return 2;
64
+ }
65
+ }
66
+ /**
67
+ * Plain-text renderer. Mirrors the L17 doctor table for visual
68
+ * consistency — 4 columns: # / STATUS / CRITERION / EVIDENCE. The
69
+ * criterion column is truncated to 60 chars so narrow terminals
70
+ * stay readable; the full text lives in the JSON envelope for
71
+ * scripted callers that want every byte.
72
+ */
73
+ export function renderTable(envelope) {
74
+ const lines = [];
75
+ const titlePart = envelope.title ? ` — ${envelope.title}` : '';
76
+ lines.push(`Pugi PRD-check${titlePart}`);
77
+ lines.push('='.repeat(50));
78
+ lines.push(`Source: ${envelope.prdPath}`);
79
+ lines.push('');
80
+ if (envelope.overall === 'unparsed') {
81
+ lines.push('No acceptance-criteria section found in PRD.');
82
+ lines.push('');
83
+ lines.push('Expected one of:');
84
+ lines.push(' ## Acceptance Criteria');
85
+ lines.push(' ## Success Criteria');
86
+ lines.push(' ## Deliverables');
87
+ return lines.join('\n');
88
+ }
89
+ if (envelope.criteria.length === 0) {
90
+ lines.push('Acceptance section present but contains 0 items.');
91
+ return lines.join('\n');
92
+ }
93
+ for (const c of envelope.criteria) {
94
+ const status = c.status.toUpperCase().padEnd(7, ' ');
95
+ const truncated = c.text.length > 60 ? `${c.text.slice(0, 57)}...` : c.text;
96
+ lines.push(`#${String(c.index).padStart(2, ' ')} ${status} ${truncated}`);
97
+ for (const r of c.results) {
98
+ const subStatus = r.status.toUpperCase().padEnd(7, ' ');
99
+ lines.push(` ${subStatus} ${r.evidence}`);
100
+ }
101
+ }
102
+ lines.push('');
103
+ const { pass, fail, skipped } = envelope.counts;
104
+ const summary = envelope.overall === 'healthy' ? 'HEALTHY' : envelope.overall === 'failing' ? 'FAILING' : 'UNPARSED';
105
+ lines.push(`${fail} fail · ${pass} pass · ${skipped} skipped. Overall: ${summary}`);
106
+ return lines.join('\n');
107
+ }
108
+ function computeOverall(hasAcceptanceSection, counts) {
109
+ if (!hasAcceptanceSection)
110
+ return 'unparsed';
111
+ if (counts.fail > 0)
112
+ return 'failing';
113
+ return 'healthy';
114
+ }
115
+ function mentionTarget(mention) {
116
+ switch (mention.kind) {
117
+ case 'file':
118
+ case 'test':
119
+ case 'doc':
120
+ return mention.path;
121
+ case 'command':
122
+ return mention.name;
123
+ case 'route':
124
+ return `${mention.method} ${mention.path}`;
125
+ }
126
+ }
127
+ //# sourceMappingURL=reporter.js.map
@@ -0,0 +1,223 @@
1
+ /**
2
+ * PRD criterion verifiers — Pugi α7 Wave 6 (`/prd-check`, 2026-05-27).
3
+ *
4
+ * Given a `ParsedCriterion` from `parser.ts`, run a set of built-in
5
+ * checks against the repository workspace and return a
6
+ * `VerifiedCriterion` with a per-criterion verdict + per-mention
7
+ * evidence trail. The module owns the file-system + grep surface
8
+ * the parser deliberately avoided.
9
+ *
10
+ * Verdict semantics (mirror the doctor probe contract so the
11
+ * reporter can render both with the same column layout):
12
+ *
13
+ * - PASS : at least one mention verified AND every attempted
14
+ * verifier passed
15
+ * - FAIL : at least one verifier failed (missing file, empty
16
+ * doc, command not in registry, etc.)
17
+ * - SKIPPED : no verifiable mentions in the criterion. The
18
+ * criterion is preserved in the report so the
19
+ * operator sees it, but it does not gate the verdict.
20
+ *
21
+ * Each `MentionResult` carries an evidence string the reporter
22
+ * shows next to the criterion. For PASS we render the resolved
23
+ * absolute path or matched line; for FAIL we render the missing
24
+ * artifact identifier so the operator can fix it directly.
25
+ *
26
+ * The verifier deps are injected (existsSync, readFileSync, …) so
27
+ * the spec can drive every branch without touching the real disk.
28
+ * The default-bound variant `runDefaultVerifiers` plugs the real
29
+ * Node fs helpers in for the CLI handler.
30
+ */
31
+ import { existsSync, readFileSync } from 'node:fs';
32
+ import { isAbsolute, resolve } from 'node:path';
33
+ /**
34
+ * Top-level verify-one-criterion entry. Walks the mention list,
35
+ * dispatches each to the right verifier, then computes the
36
+ * roll-up. Pure with respect to `deps` — no globals touched.
37
+ */
38
+ export function verifyCriterion(criterion, deps) {
39
+ if (criterion.mentions.length === 0) {
40
+ return {
41
+ criterion,
42
+ status: 'skipped',
43
+ results: [],
44
+ };
45
+ }
46
+ const results = [];
47
+ for (const mention of criterion.mentions) {
48
+ results.push(verifyMention(mention, deps));
49
+ }
50
+ const status = rollUp(results);
51
+ return { criterion, status, results };
52
+ }
53
+ /** Verify a whole PRD's worth of criteria. */
54
+ export function verifyAll(criteria, deps) {
55
+ return criteria.map((c) => verifyCriterion(c, deps));
56
+ }
57
+ function verifyMention(mention, deps) {
58
+ switch (mention.kind) {
59
+ case 'file':
60
+ return verifyFile(mention, deps);
61
+ case 'test':
62
+ return verifyTest(mention, deps);
63
+ case 'doc':
64
+ return verifyDoc(mention, deps);
65
+ case 'command':
66
+ return verifyCommand(mention, deps);
67
+ case 'route':
68
+ return verifyRoute(mention, deps);
69
+ }
70
+ }
71
+ function verifyFile(mention, deps) {
72
+ const absolute = deps.resolveWorkspacePath(mention.path);
73
+ if (deps.existsSync(absolute)) {
74
+ return {
75
+ mention,
76
+ status: 'pass',
77
+ evidence: `file present (${mention.path})`,
78
+ };
79
+ }
80
+ return {
81
+ mention,
82
+ status: 'fail',
83
+ evidence: `file missing (${mention.path})`,
84
+ };
85
+ }
86
+ function verifyTest(mention, deps) {
87
+ const absolute = deps.resolveWorkspacePath(mention.path);
88
+ if (!deps.existsSync(absolute)) {
89
+ return {
90
+ mention,
91
+ status: 'fail',
92
+ evidence: `spec missing (${mention.path})`,
93
+ };
94
+ }
95
+ let body;
96
+ try {
97
+ body = deps.readFileSync(absolute);
98
+ }
99
+ catch (error) {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ return {
102
+ mention,
103
+ status: 'fail',
104
+ evidence: `spec unreadable: ${message}`,
105
+ };
106
+ }
107
+ // Count `test(`, `it(`, and `describe(...).it(` blocks. The
108
+ // matcher is permissive — any of the three counts because the
109
+ // PRD only cares whether the spec asserts anything at all.
110
+ const matches = body.match(/\b(it|test)\s*\(/g);
111
+ if (!matches || matches.length === 0) {
112
+ return {
113
+ mention,
114
+ status: 'fail',
115
+ evidence: `spec present but has 0 test()/it() blocks`,
116
+ };
117
+ }
118
+ return {
119
+ mention,
120
+ status: 'pass',
121
+ evidence: `spec present with ${matches.length} block(s)`,
122
+ };
123
+ }
124
+ function verifyDoc(mention, deps) {
125
+ const absolute = deps.resolveWorkspacePath(mention.path);
126
+ if (!deps.existsSync(absolute)) {
127
+ return {
128
+ mention,
129
+ status: 'fail',
130
+ evidence: `doc missing (${mention.path})`,
131
+ };
132
+ }
133
+ let body;
134
+ try {
135
+ body = deps.readFileSync(absolute);
136
+ }
137
+ catch (error) {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ return {
140
+ mention,
141
+ status: 'fail',
142
+ evidence: `doc unreadable: ${message}`,
143
+ };
144
+ }
145
+ const trimmed = body.trim();
146
+ if (trimmed.length < 100) {
147
+ return {
148
+ mention,
149
+ status: 'fail',
150
+ evidence: `doc present but too short (${trimmed.length} chars, < 100)`,
151
+ };
152
+ }
153
+ return {
154
+ mention,
155
+ status: 'pass',
156
+ evidence: `doc present (${trimmed.length} chars)`,
157
+ };
158
+ }
159
+ function verifyCommand(mention, deps) {
160
+ if (deps.isKnownCommand(mention.name)) {
161
+ return {
162
+ mention,
163
+ status: 'pass',
164
+ evidence: `command \`${mention.name}\` registered`,
165
+ };
166
+ }
167
+ return {
168
+ mention,
169
+ status: 'fail',
170
+ evidence: `command \`${mention.name}\` not found in CLI registry`,
171
+ };
172
+ }
173
+ function verifyRoute(mention, deps) {
174
+ if (deps.hasRoute(mention.method, mention.path)) {
175
+ return {
176
+ mention,
177
+ status: 'pass',
178
+ evidence: `route ${mention.method} ${mention.path} registered`,
179
+ };
180
+ }
181
+ return {
182
+ mention,
183
+ status: 'fail',
184
+ evidence: `route ${mention.method} ${mention.path} not found`,
185
+ };
186
+ }
187
+ function rollUp(results) {
188
+ if (results.length === 0)
189
+ return 'skipped';
190
+ const anyFail = results.some((r) => r.status === 'fail');
191
+ if (anyFail)
192
+ return 'fail';
193
+ const anyPass = results.some((r) => r.status === 'pass');
194
+ return anyPass ? 'pass' : 'skipped';
195
+ }
196
+ /**
197
+ * Default verifier deps bound to real fs + a CLI-command predicate
198
+ * + a grep-based route locator. The CLI handler passes the
199
+ * workspace root + the known-command list at call time.
200
+ */
201
+ export function createDefaultDeps(options) {
202
+ return {
203
+ resolveWorkspacePath: (relative) => {
204
+ if (isAbsolute(relative))
205
+ return relative;
206
+ return resolve(options.workspaceRoot, relative);
207
+ },
208
+ existsSync: (path) => existsSync(path),
209
+ readFileSync: (path) => readFileSync(path, 'utf8'),
210
+ isKnownCommand: (name) => options.knownCommands.has(name),
211
+ hasRoute: () => {
212
+ // Best-effort default: the wave-6 PRD says the route verifier
213
+ // is "best-effort grep of controllers". Since the workspace
214
+ // layout varies per repo, the CLI handler injects a project
215
+ // -aware implementation when one is available; the default
216
+ // stays conservative and reports `fail` so the reporter
217
+ // surfaces the missing-verifier signal instead of silently
218
+ // passing.
219
+ return false;
220
+ },
221
+ };
222
+ }
223
+ //# sourceMappingURL=verifiers.js.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Aggregate byte cap on the full rendered block. 96 KB = 3 files at
3
+ * the per-file cap, which is enough for cwd + parent + homedir while
4
+ * leaving plenty of prompt budget for the rest of the system prompt.
5
+ * Anything beyond is replaced with a truncation marker.
6
+ */
7
+ export const MAX_INJECT_BYTES = 96 * 1024;
8
+ /**
9
+ * Marker line emitted when the aggregate cap is hit. Visible to the
10
+ * model so it knows ambient context was clipped; visible to the
11
+ * operator via the doctor probe so they can decide whether to trim
12
+ * their `PUGI.md` hierarchy.
13
+ */
14
+ export const TRUNCATION_MARKER = '<ambient-context-truncated reason="aggregate-cap" />';
15
+ /**
16
+ * Render a HierarchyFile array into the system-prompt block. Returns
17
+ * `''` when `files` is empty. Each file becomes one
18
+ * `<ambient-context source="..." level="...">...</ambient-context>`
19
+ * stanza separated by a single newline.
20
+ *
21
+ * Determinism: same input always produces byte-identical output.
22
+ */
23
+ export function renderAmbientContext(files) {
24
+ if (files.length === 0)
25
+ return '';
26
+ const stanzas = [];
27
+ let bytes = 0;
28
+ let truncated = false;
29
+ for (const file of files) {
30
+ const stanza = renderStanza(file);
31
+ const stanzaBytes = Buffer.byteLength(stanza, 'utf8') + 1; // newline join cost
32
+ if (bytes + stanzaBytes > MAX_INJECT_BYTES) {
33
+ truncated = true;
34
+ break;
35
+ }
36
+ stanzas.push(stanza);
37
+ bytes += stanzaBytes;
38
+ }
39
+ if (truncated)
40
+ stanzas.push(TRUNCATION_MARKER);
41
+ return stanzas.join('\n');
42
+ }
43
+ /**
44
+ * Build a single `<ambient-context>` stanza for one HierarchyFile.
45
+ * The `source` attribute carries the absolute path (after realpath)
46
+ * so the model can cite which file a piece of guidance came from
47
+ * when it explains its decisions to the operator.
48
+ */
49
+ function renderStanza(file) {
50
+ const sourceAttr = escapeAttr(file.path);
51
+ const levelAttr = String(file.level);
52
+ // No trailing newline inside `content` — the join adds one between
53
+ // stanzas. Trimming the file's trailing whitespace keeps the tag
54
+ // close to the content for readability when an engineer dumps the
55
+ // assembled prompt for debugging.
56
+ const trimmed = file.content.replace(/\s+$/g, '');
57
+ return [
58
+ `<ambient-context source="${sourceAttr}" level="${levelAttr}">`,
59
+ trimmed,
60
+ `</ambient-context>`,
61
+ ].join('\n');
62
+ }
63
+ /**
64
+ * Escape an XML attribute value. We expect operator-controlled paths
65
+ * (not adversarial input) but `&`, `"` and `<` are still possible in
66
+ * symlinked / unicode paths so we escape them defensively. The model
67
+ * has been trained to read this attribute as opaque metadata.
68
+ */
69
+ function escapeAttr(value) {
70
+ return value
71
+ .replace(/&/g, '&amp;')
72
+ .replace(/"/g, '&quot;')
73
+ .replace(/</g, '&lt;')
74
+ .replace(/>/g, '&gt;');
75
+ }
76
+ //# sourceMappingURL=context-injector.js.map