@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.41

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 (250) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  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/commands/smoke.js +133 -0
  7. package/dist/core/agent-progress/cleanup.js +134 -0
  8. package/dist/core/agent-progress/schema.js +144 -0
  9. package/dist/core/agent-progress/writer.js +101 -0
  10. package/dist/core/artifact-chain/dispatcher.js +148 -0
  11. package/dist/core/artifact-chain/exporter.js +164 -0
  12. package/dist/core/artifact-chain/state.js +243 -0
  13. package/dist/core/artifact-chain/steps.js +169 -0
  14. package/dist/core/auth/ensure-authenticated.js +129 -0
  15. package/dist/core/auth/env-provider.js +238 -0
  16. package/dist/core/auto-update/channels.js +122 -0
  17. package/dist/core/auto-update/checker.js +241 -0
  18. package/dist/core/auto-update/state.js +235 -0
  19. package/dist/core/bare-mode/index.js +107 -0
  20. package/dist/core/bash-classifier.js +108 -1
  21. package/dist/core/checkpoint/resumer.js +149 -0
  22. package/dist/core/checkpoint/rewinder.js +291 -0
  23. package/dist/core/codegraph/decision-store.js +248 -0
  24. package/dist/core/codegraph/detect-repo.js +459 -0
  25. package/dist/core/codegraph/install.js +134 -0
  26. package/dist/core/codegraph/offer-hook.js +220 -0
  27. package/dist/core/compact/auto-trigger.js +96 -0
  28. package/dist/core/compact/buffer-rewriter.js +115 -0
  29. package/dist/core/compact/summarizer.js +208 -0
  30. package/dist/core/compact/token-counter.js +108 -0
  31. package/dist/core/consensus/diff-capture.js +73 -0
  32. package/dist/core/context/index.js +7 -0
  33. package/dist/core/context/markdown-traverse.js +255 -0
  34. package/dist/core/cost/rate-card.js +129 -0
  35. package/dist/core/cost/tracker.js +221 -0
  36. package/dist/core/denial-tracking/index.js +8 -0
  37. package/dist/core/denial-tracking/state.js +264 -0
  38. package/dist/core/diagnostics/probe-runner.js +93 -0
  39. package/dist/core/diagnostics/probes/api.js +46 -0
  40. package/dist/core/diagnostics/probes/auth.js +86 -0
  41. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  42. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  43. package/dist/core/diagnostics/probes/config.js +72 -0
  44. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  45. package/dist/core/diagnostics/probes/disk.js +81 -0
  46. package/dist/core/diagnostics/probes/git.js +65 -0
  47. package/dist/core/diagnostics/probes/mcp.js +75 -0
  48. package/dist/core/diagnostics/probes/node.js +59 -0
  49. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  50. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  51. package/dist/core/diagnostics/probes/session.js +74 -0
  52. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  53. package/dist/core/diagnostics/probes/workspace.js +63 -0
  54. package/dist/core/diagnostics/types.js +70 -0
  55. package/dist/core/dispatch/cache-cleanup.js +197 -0
  56. package/dist/core/dispatch/cache-handoff.js +295 -0
  57. package/dist/core/edits/dispatch.js +218 -2
  58. package/dist/core/edits/journal.js +199 -0
  59. package/dist/core/edits/layer-d-ast.js +557 -14
  60. package/dist/core/edits/verify-hook.js +273 -0
  61. package/dist/core/edits/worktree.js +322 -0
  62. package/dist/core/engine/anvil-client.js +115 -5
  63. package/dist/core/engine/budgets.js +98 -0
  64. package/dist/core/engine/context-prefix.js +155 -0
  65. package/dist/core/engine/intent.js +260 -0
  66. package/dist/core/engine/native-pugi.js +860 -211
  67. package/dist/core/engine/prompts.js +88 -2
  68. package/dist/core/engine/strip-internal-fields.js +124 -0
  69. package/dist/core/engine/tool-bridge.js +1045 -36
  70. package/dist/core/feedback/queue.js +177 -0
  71. package/dist/core/feedback/submitter.js +145 -0
  72. package/dist/core/file-cache.js +113 -1
  73. package/dist/core/hooks/events.js +44 -0
  74. package/dist/core/hooks/index.js +15 -0
  75. package/dist/core/hooks/registry.js +213 -0
  76. package/dist/core/hooks/runner.js +236 -0
  77. package/dist/core/hooks/v2/event-emitter.js +115 -0
  78. package/dist/core/hooks/v2/executor.js +282 -0
  79. package/dist/core/hooks/v2/index.js +25 -0
  80. package/dist/core/hooks/v2/lifecycle.js +104 -0
  81. package/dist/core/hooks/v2/loader.js +216 -0
  82. package/dist/core/hooks/v2/matcher.js +125 -0
  83. package/dist/core/hooks/v2/trust.js +143 -0
  84. package/dist/core/hooks/v2/types.js +86 -0
  85. package/dist/core/lsp/cache.js +105 -0
  86. package/dist/core/lsp/client.js +776 -0
  87. package/dist/core/lsp/language-detect.js +66 -0
  88. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  89. package/dist/core/mcp/client.js +75 -6
  90. package/dist/core/mcp/http-server.js +553 -0
  91. package/dist/core/mcp/orchestrator-tools.js +662 -0
  92. package/dist/core/mcp/permission.js +190 -0
  93. package/dist/core/mcp/registry.js +24 -2
  94. package/dist/core/mcp/server-tools.js +219 -0
  95. package/dist/core/mcp/server.js +397 -0
  96. package/dist/core/memory/dual-write.js +416 -0
  97. package/dist/core/memory/phase1-kinds.js +20 -0
  98. package/dist/core/memory-sync/queue.js +158 -0
  99. package/dist/core/onboarding/ensure-initialized.js +133 -0
  100. package/dist/core/onboarding/marker.js +111 -0
  101. package/dist/core/onboarding/telemetry-state.js +108 -0
  102. package/dist/core/output-style/presets.js +176 -0
  103. package/dist/core/output-style/state.js +185 -0
  104. package/dist/core/permissions/auto-classifier.js +124 -0
  105. package/dist/core/permissions/circuit-breaker.js +83 -0
  106. package/dist/core/permissions/gate.js +278 -0
  107. package/dist/core/permissions/index.js +20 -0
  108. package/dist/core/permissions/mode.js +174 -0
  109. package/dist/core/permissions/state.js +241 -0
  110. package/dist/core/permissions/tool-class.js +93 -0
  111. package/dist/core/prd-check/parser.js +215 -0
  112. package/dist/core/prd-check/reporter.js +127 -0
  113. package/dist/core/prd-check/session-review.js +557 -0
  114. package/dist/core/prd-check/verifiers.js +223 -0
  115. package/dist/core/pugi-md/context-injector.js +76 -0
  116. package/dist/core/pugi-md/walk-up.js +207 -0
  117. package/dist/core/release-notes/parser.js +241 -0
  118. package/dist/core/release-notes/state.js +116 -0
  119. package/dist/core/repl/history.js +11 -1
  120. package/dist/core/repl/model-pricing.js +135 -0
  121. package/dist/core/repl/session.js +1899 -38
  122. package/dist/core/repl/slash-commands.js +406 -21
  123. package/dist/core/repl/store/session-store.js +31 -2
  124. package/dist/core/repl/workspace-context.js +22 -0
  125. package/dist/core/repo-map/build.js +125 -0
  126. package/dist/core/repo-map/cache.js +185 -0
  127. package/dist/core/repo-map/extractor.js +254 -0
  128. package/dist/core/repo-map/formatter.js +145 -0
  129. package/dist/core/repo-map/scanner.js +211 -0
  130. package/dist/core/retry-budget/budget.js +284 -0
  131. package/dist/core/retry-budget/index.js +5 -0
  132. package/dist/core/session.js +92 -0
  133. package/dist/core/settings.js +80 -0
  134. package/dist/core/share/formatter.js +271 -0
  135. package/dist/core/share/redactor.js +221 -0
  136. package/dist/core/share/uploader.js +267 -0
  137. package/dist/core/skills/defaults.js +457 -0
  138. package/dist/core/smoke/headless-driver.js +174 -0
  139. package/dist/core/smoke/orchestrator.js +194 -0
  140. package/dist/core/smoke/runner.js +238 -0
  141. package/dist/core/smoke/scenario-parser.js +316 -0
  142. package/dist/core/subagents/dispatcher-real.js +600 -0
  143. package/dist/core/subagents/dispatcher.js +113 -24
  144. package/dist/core/subagents/index.js +18 -5
  145. package/dist/core/subagents/isolation-matrix.js +213 -0
  146. package/dist/core/subagents/spawn.js +19 -4
  147. package/dist/core/telemetry/emitter.js +229 -0
  148. package/dist/core/telemetry/queue.js +251 -0
  149. package/dist/core/theme/context.js +91 -0
  150. package/dist/core/theme/presets.js +228 -0
  151. package/dist/core/theme/state.js +181 -0
  152. package/dist/core/todos/invariant.js +10 -0
  153. package/dist/core/todos/state.js +177 -0
  154. package/dist/core/transport/version-interceptor.js +166 -0
  155. package/dist/core/vim/keymap.js +288 -0
  156. package/dist/core/vim/state.js +92 -0
  157. package/dist/index.js +28 -0
  158. package/dist/runtime/bootstrap.js +190 -0
  159. package/dist/runtime/cli.js +3073 -321
  160. package/dist/runtime/commands/cancel.js +231 -0
  161. package/dist/runtime/commands/chain.js +489 -0
  162. package/dist/runtime/commands/codegraph-status.js +227 -0
  163. package/dist/runtime/commands/compact.js +297 -0
  164. package/dist/runtime/commands/cost.js +199 -0
  165. package/dist/runtime/commands/delegate.js +242 -11
  166. package/dist/runtime/commands/dispatch.js +126 -0
  167. package/dist/runtime/commands/doctor.js +390 -0
  168. package/dist/runtime/commands/feedback.js +184 -0
  169. package/dist/runtime/commands/hooks.js +184 -0
  170. package/dist/runtime/commands/lsp.js +368 -0
  171. package/dist/runtime/commands/mcp.js +879 -0
  172. package/dist/runtime/commands/memory.js +508 -0
  173. package/dist/runtime/commands/model.js +237 -0
  174. package/dist/runtime/commands/onboarding.js +275 -0
  175. package/dist/runtime/commands/patch.js +128 -0
  176. package/dist/runtime/commands/permissions.js +112 -0
  177. package/dist/runtime/commands/plan.js +143 -0
  178. package/dist/runtime/commands/prd-check.js +285 -0
  179. package/dist/runtime/commands/redo-blob-store.js +92 -0
  180. package/dist/runtime/commands/redo.js +361 -0
  181. package/dist/runtime/commands/release-notes.js +229 -0
  182. package/dist/runtime/commands/repo-map.js +95 -0
  183. package/dist/runtime/commands/report.js +299 -0
  184. package/dist/runtime/commands/resume.js +118 -0
  185. package/dist/runtime/commands/review-consensus.js +17 -2
  186. package/dist/runtime/commands/rewind.js +333 -0
  187. package/dist/runtime/commands/sessions.js +163 -0
  188. package/dist/runtime/commands/share.js +316 -0
  189. package/dist/runtime/commands/status.js +186 -0
  190. package/dist/runtime/commands/stickers.js +82 -0
  191. package/dist/runtime/commands/style.js +194 -0
  192. package/dist/runtime/commands/theme.js +196 -0
  193. package/dist/runtime/commands/undo.js +32 -0
  194. package/dist/runtime/commands/update.js +289 -0
  195. package/dist/runtime/commands/vim.js +140 -0
  196. package/dist/runtime/commands/worktree.js +177 -0
  197. package/dist/runtime/headless-repl.js +195 -0
  198. package/dist/runtime/headless.js +543 -0
  199. package/dist/runtime/load-hooks-or-exit.js +71 -0
  200. package/dist/runtime/plan-decompose.js +531 -0
  201. package/dist/runtime/version.js +65 -0
  202. package/dist/tools/agent-tool.js +229 -0
  203. package/dist/tools/apply-patch.js +556 -0
  204. package/dist/tools/ask-user-question.js +213 -0
  205. package/dist/tools/ask-user.js +115 -0
  206. package/dist/tools/file-tools.js +85 -14
  207. package/dist/tools/lsp-tools.js +189 -0
  208. package/dist/tools/mcp-tool.js +260 -0
  209. package/dist/tools/multi-edit.js +361 -0
  210. package/dist/tools/powershell.js +156 -0
  211. package/dist/tools/registry.js +51 -0
  212. package/dist/tools/skill-tool.js +96 -0
  213. package/dist/tools/tasks.js +208 -0
  214. package/dist/tools/todo-write.js +184 -0
  215. package/dist/tools/web-fetch.js +147 -2
  216. package/dist/tools/web-search.js +458 -0
  217. package/dist/tui/agent-progress-card.js +111 -0
  218. package/dist/tui/agent-tree.js +10 -0
  219. package/dist/tui/ask-modal.js +2 -2
  220. package/dist/tui/ask-user-question-prompt.js +192 -0
  221. package/dist/tui/compact-banner.js +81 -0
  222. package/dist/tui/conversation-pane.js +82 -8
  223. package/dist/tui/cost-table.js +111 -0
  224. package/dist/tui/doctor-table.js +46 -0
  225. package/dist/tui/feedback-prompt.js +156 -0
  226. package/dist/tui/input-box.js +69 -2
  227. package/dist/tui/markdown-render.js +4 -4
  228. package/dist/tui/onboarding-wizard.js +240 -0
  229. package/dist/tui/permissions-picker.js +86 -0
  230. package/dist/tui/render.js +35 -0
  231. package/dist/tui/repl-render.js +303 -13
  232. package/dist/tui/repl-splash.js +2 -2
  233. package/dist/tui/repl.js +72 -14
  234. package/dist/tui/splash.js +1 -1
  235. package/dist/tui/status-bar.js +94 -16
  236. package/dist/tui/status-table.js +7 -0
  237. package/dist/tui/stickers-art.js +136 -0
  238. package/dist/tui/style-table.js +28 -0
  239. package/dist/tui/theme-table.js +29 -0
  240. package/dist/tui/tool-stream-pane.js +52 -3
  241. package/dist/tui/update-banner.js +20 -2
  242. package/dist/tui/vim-input.js +267 -0
  243. package/docs/examples/codegraph.mcp.json +10 -0
  244. package/package.json +12 -6
  245. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  246. package/test/scenarios/compact-force.scenario.txt +11 -0
  247. package/test/scenarios/identity.scenario.txt +11 -0
  248. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  249. package/test/scenarios/walkback.scenario.txt +12 -0
  250. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,531 @@
1
+ import { mkdirSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { relative, resolve, sep } from 'node:path';
3
+ import { z } from 'zod';
4
+ /**
5
+ * α6.8 `pugi plan --decompose <idea>` — pattern ported (not copied) from
6
+ * the piercelamb/deep-project plugin (MIT-licensed, 134 stars). The Deep
7
+ * Trilogy splits a high-level idea into focused components, each backed
8
+ * by its own `spec.md`, plus a `project-manifest.md` with the dependency
9
+ * DAG. Pugi's port reuses our existing `pugi plan` engine task and
10
+ * persists artifacts under `.pugi/plan/<session-id>/` so the decomposed
11
+ * specs are reviewable + resumable in the same shape as the rest of the
12
+ * artifact store.
13
+ *
14
+ * Why a separate module: the engine task in `runtime/cli.ts` is already
15
+ * 200+ LOC. Lifting the prompt suffix, JSON parser, and atomic writer
16
+ * out keeps the integration in `runEngineTask` to a single import + a
17
+ * single post-result branch. The module exports a small surface
18
+ * (parser + writer + helpers) so the spec can drive each piece in
19
+ * isolation against golden fixtures.
20
+ *
21
+ * Reference: https://github.com/piercelamb/deep-project (MIT). Pugi
22
+ * extracted the high-level pattern (split-into-components + manifest
23
+ * DAG + per-component spec) and ships a Pugi-native implementation.
24
+ */
25
+ /**
26
+ * Suffix appended to the operator's `pugi plan --decompose` prompt so
27
+ * the model emits a single JSON block at the end of its final answer.
28
+ * The JSON is the machine-readable contract; the prose preceding it
29
+ * (rationale, alternatives considered) lands inside `manifest.md` for
30
+ * the operator's review.
31
+ *
32
+ * Design choices anchored by the Deep-Project pattern + our own
33
+ * planning ergonomics:
34
+ * - 3-7 component sweet spot: more than 7 produces unreviewable
35
+ * spaghetti, fewer than 3 wastes the decomposition step. The
36
+ * schema enforces this hard so the prompt promise matches the
37
+ * contract.
38
+ * - `dependsOn` references component names verbatim — the writer
39
+ * enforces uniqueness and resolves the DAG, so a typo there
40
+ * fails loud at parse time rather than producing a silently
41
+ * wrong manifest.
42
+ * - JSON fence is mandatory so the parser can `slice` it out
43
+ * deterministically; loose JSON-in-prose extractions are
44
+ * brittle and trigger false positives.
45
+ */
46
+ export const DECOMPOSE_PROMPT_SUFFIX = [
47
+ '',
48
+ '## Decomposition request',
49
+ '',
50
+ 'Split the idea above into 3-7 focused components. Each component must be:',
51
+ '- Small enough to fit a single supervised build session (~half a day).',
52
+ '- Explicit about its dependencies on other components by name.',
53
+ '- Specified well enough that a fresh `pugi plan <component-name>` session could pick it up.',
54
+ '',
55
+ 'After your prose rationale, emit a SINGLE fenced JSON block at the END of your answer.',
56
+ 'The JSON block MUST match this shape exactly:',
57
+ '',
58
+ '```json',
59
+ '{',
60
+ ' "components": [',
61
+ ' {',
62
+ ' "name": "kebab-case-name",',
63
+ ' "summary": "one-sentence headline",',
64
+ ' "spec": "multi-line spec body — what to build, acceptance criteria, files touched",',
65
+ ' "dependsOn": ["other-component-name", "..."]',
66
+ ' }',
67
+ ' ]',
68
+ '}',
69
+ '```',
70
+ '',
71
+ 'Constraints:',
72
+ '- `name` is kebab-case, 1-48 chars, unique across components.',
73
+ '- `dependsOn` references other component names from the same list. Empty array if no dependencies.',
74
+ '- Order components in topological order (dependencies first).',
75
+ '- Do NOT emit additional fields. Do NOT emit multiple JSON blocks. Do NOT inline implementation code.',
76
+ ].join('\n');
77
+ /**
78
+ * Windows reserved filename basenames (case-insensitive). The atomic
79
+ * writer rejects any component name that lowercases to one of these so
80
+ * a future Windows checkout never trips on `splits/01-con/spec.md` etc.
81
+ * The kebab-case regex already blocks the colon-separated `com1:` style,
82
+ * but the bare `con` / `nul` / `prn` / `aux` / `comN` / `lptN` forms
83
+ * still pass the regex; the denylist closes that gap.
84
+ */
85
+ const WINDOWS_RESERVED_BASENAMES = new Set([
86
+ 'con',
87
+ 'prn',
88
+ 'aux',
89
+ 'nul',
90
+ 'com1',
91
+ 'com2',
92
+ 'com3',
93
+ 'com4',
94
+ 'com5',
95
+ 'com6',
96
+ 'com7',
97
+ 'com8',
98
+ 'com9',
99
+ 'lpt1',
100
+ 'lpt2',
101
+ 'lpt3',
102
+ 'lpt4',
103
+ 'lpt5',
104
+ 'lpt6',
105
+ 'lpt7',
106
+ 'lpt8',
107
+ 'lpt9',
108
+ ]);
109
+ /**
110
+ * Reject any free-text field whose body would close a triple-backtick
111
+ * fence and re-open the surrounding Markdown to model-controlled
112
+ * interpretation. Mermaid blocks and the SPLIT_MANIFEST JSON block are
113
+ * both rendered inside ``` fences in the manifest, so a stray ``` in
114
+ * `summary` / `prompt` / `name` / `spec` would tip the renderer mid
115
+ * fence. We reject at the schema layer (fail loud) rather than escape
116
+ * at render time — escaping would still let model-authored text leak
117
+ * into the Mermaid AST and we have no need to round-trip backticks.
118
+ */
119
+ const TRIPLE_BACKTICK_RE = /```/;
120
+ /**
121
+ * Single decomposed component. The shape mirrors the JSON contract in
122
+ * the prompt suffix exactly so the parser is a one-shot `z.parse`.
123
+ */
124
+ const componentSchema = z.object({
125
+ name: z
126
+ .string()
127
+ .min(1)
128
+ .max(48)
129
+ .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'name must be kebab-case (a-z, 0-9, hyphen)')
130
+ .refine((value) => !TRIPLE_BACKTICK_RE.test(value), {
131
+ message: 'name must not contain triple backticks',
132
+ })
133
+ .refine((value) => !WINDOWS_RESERVED_BASENAMES.has(value.toLowerCase()), {
134
+ message: 'name collides with a Windows reserved filename',
135
+ }),
136
+ summary: z
137
+ .string()
138
+ .min(1)
139
+ .max(200)
140
+ .refine((value) => !TRIPLE_BACKTICK_RE.test(value), {
141
+ message: 'summary must not contain triple backticks',
142
+ }),
143
+ spec: z
144
+ .string()
145
+ .min(1)
146
+ .refine((value) => !TRIPLE_BACKTICK_RE.test(value), {
147
+ message: 'spec must not contain triple backticks',
148
+ }),
149
+ // Reject empty strings at the array element level so a stray `""` in
150
+ // `dependsOn` cannot pass the structural validation and turn into a
151
+ // mystery node id later.
152
+ dependsOn: z.array(z.string().min(1)).default([]),
153
+ });
154
+ export const decompositionSchema = z
155
+ .object({
156
+ // 3-7 components matches the prompt contract verbatim. Tighter
157
+ // bounds catch malformed outputs (e.g. a model that returned a
158
+ // single mega-component or a list of fifteen) at parse time.
159
+ components: z.array(componentSchema).min(3).max(7),
160
+ })
161
+ .superRefine((value, ctx) => {
162
+ const names = new Set();
163
+ for (const [index, component] of value.components.entries()) {
164
+ if (names.has(component.name)) {
165
+ ctx.addIssue({
166
+ code: z.ZodIssueCode.custom,
167
+ path: ['components', index, 'name'],
168
+ message: `duplicate component name "${component.name}"`,
169
+ });
170
+ }
171
+ names.add(component.name);
172
+ }
173
+ for (const [index, component] of value.components.entries()) {
174
+ for (const [depIndex, dep] of component.dependsOn.entries()) {
175
+ if (!names.has(dep)) {
176
+ // Surface the unresolved reference at parse time so the writer
177
+ // is guaranteed a referentially-closed DAG.
178
+ ctx.addIssue({
179
+ code: z.ZodIssueCode.custom,
180
+ path: ['components', index, 'dependsOn', depIndex],
181
+ message: `dependsOn references unknown component "${dep}"`,
182
+ });
183
+ }
184
+ if (dep === component.name) {
185
+ ctx.addIssue({
186
+ code: z.ZodIssueCode.custom,
187
+ path: ['components', index, 'dependsOn', depIndex],
188
+ message: `component "${component.name}" cannot depend on itself`,
189
+ });
190
+ }
191
+ }
192
+ }
193
+ });
194
+ /**
195
+ * Extract the LAST fenced JSON block from the model's final text and
196
+ * validate it against the decomposition schema. The LAST block is the
197
+ * right choice because models sometimes emit example JSON inside the
198
+ * rationale to illustrate a point, then emit the canonical block at
199
+ * the bottom. Honouring "last" makes the contract unambiguous.
200
+ *
201
+ * Fence patterns accepted: ```json ... ``` (preferred) and bare
202
+ * ``` ... ``` when the body is valid JSON. We do NOT support naked
203
+ * JSON outside a fence: that is brittle (any model that wraps prose
204
+ * in quotes would tip the parser) and the prompt mandates the fence.
205
+ *
206
+ * Line endings: the regex tolerates both LF and CRLF newlines so a
207
+ * Windows-checkout transcript pastes through without ceremony.
208
+ */
209
+ export function parseDecompositionFromText(text) {
210
+ const blocks = extractFencedJsonBlocks(text);
211
+ if (blocks.length === 0) {
212
+ return {
213
+ ok: false,
214
+ reason: 'no_json_block',
215
+ detail: 'model did not emit a fenced JSON block',
216
+ };
217
+ }
218
+ // Pick the LAST block — see docstring. The rationale is everything
219
+ // before that block's opening fence.
220
+ const last = blocks[blocks.length - 1];
221
+ const rationale = text.slice(0, last.fenceStart).trim();
222
+ let parsed;
223
+ try {
224
+ parsed = JSON.parse(last.body);
225
+ }
226
+ catch (error) {
227
+ return {
228
+ ok: false,
229
+ reason: 'invalid_json',
230
+ detail: error instanceof Error ? error.message : String(error),
231
+ };
232
+ }
233
+ const result = decompositionSchema.safeParse(parsed);
234
+ if (!result.success) {
235
+ return {
236
+ ok: false,
237
+ reason: 'invalid_schema',
238
+ detail: result.error.issues
239
+ .map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`)
240
+ .join('; '),
241
+ };
242
+ }
243
+ return { ok: true, decomposition: result.data, rationale };
244
+ }
245
+ function extractFencedJsonBlocks(text) {
246
+ const blocks = [];
247
+ // Matches ```json\n...\n``` and ```\n...\n``` (capture group 1 is body).
248
+ // We use `[\s\S]*?` (non-greedy) so adjacent fences do not merge into a
249
+ // single capture. The `\r?\n` tolerance keeps CRLF transcripts working
250
+ // without a separate normalization pass.
251
+ const pattern = /```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```/g;
252
+ let match = pattern.exec(text);
253
+ while (match !== null) {
254
+ blocks.push({
255
+ fenceStart: match.index,
256
+ fenceEnd: match.index + match[0].length,
257
+ body: match[1].trim(),
258
+ });
259
+ match = pattern.exec(text);
260
+ }
261
+ return blocks;
262
+ }
263
+ export function writeDecomposition(input) {
264
+ const { root, sessionId, prompt, decomposition, rationale } = input;
265
+ const generatedAt = input.generatedAt ?? new Date().toISOString();
266
+ const planDir = resolve(root, '.pugi', 'plan', sessionId);
267
+ const splitsDir = resolve(planDir, 'splits');
268
+ const stagingSuffix = `splits.tmp-${sessionId}-${process.pid}-${Date.now()}`;
269
+ const stagingDir = resolve(planDir, stagingSuffix);
270
+ const manifestFinalPath = resolve(planDir, 'manifest.md');
271
+ const manifestTmpPath = `${manifestFinalPath}.tmp-${process.pid}-${Date.now()}`;
272
+ mkdirSync(planDir, { recursive: true });
273
+ mkdirSync(stagingDir, { recursive: true });
274
+ const splitPaths = [];
275
+ try {
276
+ for (const [index, component] of decomposition.components.entries()) {
277
+ const numbered = `${String(index + 1).padStart(2, '0')}-${component.name}`;
278
+ const componentDir = resolve(stagingDir, numbered);
279
+ // Defense-in-depth: even though the schema rejects `..` / `/` /
280
+ // NUL / Windows reserved basenames in `name`, re-assert that the
281
+ // resolved path stays inside the staging directory before any
282
+ // byte is written. One regex change away from escape becomes one
283
+ // assertion away from escape.
284
+ assertContained(stagingDir, componentDir);
285
+ mkdirSync(componentDir, { recursive: true });
286
+ const specPath = resolve(componentDir, 'spec.md');
287
+ assertContained(stagingDir, specPath);
288
+ writeFileSync(specPath, formatSpec(component, decomposition, generatedAt), {
289
+ encoding: 'utf8',
290
+ mode: 0o600,
291
+ });
292
+ splitPaths.push({
293
+ name: numbered,
294
+ // The relative path is reported against the FINAL splits dir so
295
+ // operator-facing output never mentions the staging prefix.
296
+ path: relative(root, resolve(splitsDir, numbered, 'spec.md')),
297
+ });
298
+ }
299
+ // Manifest staged next to its final path so a single rename swaps
300
+ // it into place. Same atomicity argument as the splits tree.
301
+ writeFileSync(manifestTmpPath, formatManifest({
302
+ prompt,
303
+ decomposition,
304
+ rationale,
305
+ generatedAt,
306
+ sessionId,
307
+ }), { encoding: 'utf8', mode: 0o600 });
308
+ // Final flip — both renames happen after every byte is on disk.
309
+ // `splits/` is published first because the manifest body references
310
+ // it; that ordering means a reader who sees the manifest can trust
311
+ // every path inside it resolves.
312
+ renameSync(stagingDir, splitsDir);
313
+ renameSync(manifestTmpPath, manifestFinalPath);
314
+ }
315
+ catch (error) {
316
+ // Best-effort cleanup of staged artifacts so a failed run does not
317
+ // leave a `splits.tmp-...` carcass behind. We swallow cleanup
318
+ // errors to keep the original failure as the surfaced cause.
319
+ try {
320
+ rmSync(stagingDir, { recursive: true, force: true });
321
+ }
322
+ catch {
323
+ // ignore
324
+ }
325
+ try {
326
+ rmSync(manifestTmpPath, { force: true });
327
+ }
328
+ catch {
329
+ // ignore
330
+ }
331
+ throw error;
332
+ }
333
+ return {
334
+ planDir,
335
+ manifestPath: manifestFinalPath,
336
+ splitPaths,
337
+ };
338
+ }
339
+ /**
340
+ * Assert that `candidate` (already absolute via `resolve`) lives inside
341
+ * `parent`. Catches the unlikely-but-possible case where the kebab
342
+ * regex is relaxed in a future PR and a `..` slips through.
343
+ */
344
+ function assertContained(parent, candidate) {
345
+ const parentWithSep = parent.endsWith(sep) ? parent : `${parent}${sep}`;
346
+ if (candidate !== parent && !candidate.startsWith(parentWithSep)) {
347
+ throw new Error(`decomposition path escape detected: ${candidate} is not inside ${parent}`);
348
+ }
349
+ }
350
+ /**
351
+ * Render the per-component `spec.md`. Sections kept narrow so the file
352
+ * is grep-friendly and the next `pugi plan` session can pick it up
353
+ * with minimal context loading.
354
+ */
355
+ export function formatSpec(component, decomposition, generatedAt) {
356
+ const dependants = decomposition.components
357
+ .filter((other) => other.dependsOn.includes(component.name))
358
+ .map((other) => other.name);
359
+ const lines = [];
360
+ lines.push(`# ${component.name}`);
361
+ lines.push('');
362
+ lines.push(`> ${component.summary}`);
363
+ lines.push('');
364
+ lines.push(`**Generated:** ${generatedAt}`);
365
+ lines.push('');
366
+ lines.push('## Depends on');
367
+ lines.push('');
368
+ if (component.dependsOn.length === 0) {
369
+ lines.push('_(none — this component can start in parallel with other roots)_');
370
+ }
371
+ else {
372
+ for (const dep of component.dependsOn)
373
+ lines.push(`- ${dep}`);
374
+ }
375
+ lines.push('');
376
+ lines.push('## Blocked by this component');
377
+ lines.push('');
378
+ if (dependants.length === 0) {
379
+ lines.push('_(none — this is a leaf component)_');
380
+ }
381
+ else {
382
+ for (const dep of dependants)
383
+ lines.push(`- ${dep}`);
384
+ }
385
+ lines.push('');
386
+ lines.push('## Spec');
387
+ lines.push('');
388
+ lines.push(component.spec.trim());
389
+ lines.push('');
390
+ return lines.join('\n');
391
+ }
392
+ export function formatManifest(input) {
393
+ const { prompt, decomposition, rationale, generatedAt, sessionId } = input;
394
+ const lines = [];
395
+ lines.push('# Pugi Plan — Decomposition Manifest');
396
+ lines.push('');
397
+ lines.push(`**Prompt:** ${prompt}`);
398
+ lines.push(`**Session:** ${sessionId}`);
399
+ lines.push(`**Generated:** ${generatedAt}`);
400
+ lines.push(`**Component count:** ${decomposition.components.length}`);
401
+ lines.push('');
402
+ if (rationale) {
403
+ lines.push('## Rationale');
404
+ lines.push('');
405
+ lines.push(rationale);
406
+ lines.push('');
407
+ }
408
+ lines.push('## Components');
409
+ lines.push('');
410
+ for (const [index, component] of decomposition.components.entries()) {
411
+ const numbered = `${String(index + 1).padStart(2, '0')}-${component.name}`;
412
+ const deps = component.dependsOn.length === 0 ? '_(no deps)_' : component.dependsOn.join(', ');
413
+ lines.push(`- **${numbered}** — ${component.summary} (depends on: ${deps})`);
414
+ }
415
+ lines.push('');
416
+ lines.push('## Dependency DAG');
417
+ lines.push('');
418
+ lines.push('```mermaid');
419
+ lines.push('graph TD');
420
+ for (const component of decomposition.components) {
421
+ const safeNode = sanitizeMermaidId(component.name);
422
+ lines.push(` ${safeNode}["${component.name}"]`);
423
+ }
424
+ for (const component of decomposition.components) {
425
+ const toNode = sanitizeMermaidId(component.name);
426
+ for (const dep of component.dependsOn) {
427
+ const fromNode = sanitizeMermaidId(dep);
428
+ lines.push(` ${fromNode} --> ${toNode}`);
429
+ }
430
+ }
431
+ lines.push('```');
432
+ lines.push('');
433
+ lines.push('## Execution order');
434
+ lines.push('');
435
+ const topo = topologicalOrder(decomposition);
436
+ const order = topo.order;
437
+ for (const [index, name] of order.entries()) {
438
+ lines.push(`${index + 1}. ${name}`);
439
+ }
440
+ lines.push('');
441
+ if (topo.cycleNodes.length > 0) {
442
+ // Cycle detected — surface explicitly so the operator does not have
443
+ // to count "fewer lines than components" by hand. We do not refuse
444
+ // to render the manifest: it still has every spec link plus the
445
+ // mermaid graph, which actually visualises the cycle.
446
+ lines.push('## Cycles detected');
447
+ lines.push('');
448
+ lines.push(`The dependency graph contains a cycle involving ${topo.cycleNodes.length} component(s). ` +
449
+ 'Re-run `pugi plan --decompose` with a clearer prompt or edit the manifest manually before executing splits in order.');
450
+ lines.push('');
451
+ for (const node of topo.cycleNodes) {
452
+ lines.push(`- ${node}`);
453
+ }
454
+ lines.push('');
455
+ }
456
+ lines.push('## SPLIT_MANIFEST');
457
+ lines.push('');
458
+ lines.push('```json');
459
+ lines.push(JSON.stringify({
460
+ schema: 1,
461
+ sessionId,
462
+ generatedAt,
463
+ components: decomposition.components.map((component, index) => ({
464
+ name: component.name,
465
+ path: `splits/${String(index + 1).padStart(2, '0')}-${component.name}/spec.md`,
466
+ summary: component.summary,
467
+ dependsOn: component.dependsOn,
468
+ })),
469
+ executionOrder: order,
470
+ cycleNodes: topo.cycleNodes,
471
+ }, null, 2));
472
+ lines.push('```');
473
+ lines.push('');
474
+ return lines.join('\n');
475
+ }
476
+ /**
477
+ * Kahn-style topological sort. The schema already rejected
478
+ * self-references and unknown deps; cycles are still possible (A->B,
479
+ * B->A). On cycle detection we append the remaining nodes verbatim to
480
+ * `order` (so the manifest still renders something useful) and surface
481
+ * the same nodes in `cycleNodes` so the caller can flag the situation
482
+ * loudly.
483
+ */
484
+ export function topologicalOrder(decomposition) {
485
+ const remaining = new Map();
486
+ for (const component of decomposition.components) {
487
+ remaining.set(component.name, new Set(component.dependsOn));
488
+ }
489
+ const ordered = [];
490
+ const cycleNodes = [];
491
+ while (remaining.size > 0) {
492
+ const ready = [];
493
+ for (const [name, deps] of remaining.entries()) {
494
+ if (deps.size === 0)
495
+ ready.push(name);
496
+ }
497
+ if (ready.length === 0) {
498
+ // Cycle — append the remaining names in declaration order so the
499
+ // manifest is still useful, AND surface the same nodes in the
500
+ // cycle report.
501
+ for (const component of decomposition.components) {
502
+ if (remaining.has(component.name)) {
503
+ ordered.push(component.name);
504
+ cycleNodes.push(component.name);
505
+ }
506
+ }
507
+ break;
508
+ }
509
+ // Sort ready bucket alphabetically for determinism: multiple
510
+ // independent roots otherwise come out in `Map.keys()` insertion
511
+ // order, which depends on the upstream JSON which we don't control.
512
+ ready.sort();
513
+ for (const name of ready) {
514
+ ordered.push(name);
515
+ remaining.delete(name);
516
+ for (const deps of remaining.values()) {
517
+ deps.delete(name);
518
+ }
519
+ }
520
+ }
521
+ return { order: ordered, cycleNodes };
522
+ }
523
+ function sanitizeMermaidId(name) {
524
+ // Mermaid graph node ids must be valid identifiers (no hyphens at
525
+ // arbitrary positions, no leading digits). We map kebab-case names
526
+ // to safe ids by replacing hyphens with underscores and prefixing
527
+ // a leading non-letter with `n_`.
528
+ const replaced = name.replace(/-/g, '_');
529
+ return /^[A-Za-z]/.test(replaced) ? replaced : `n_${replaced}`;
530
+ }
531
+ //# sourceMappingURL=plan-decompose.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pugi CLI ↔ admin-api version handshake — shared constants.
3
+ *
4
+ * The CLI declares its installed version on every outbound request to
5
+ * the admin-api so the server can enforce a minClientVersion floor and
6
+ * surface a soft-warn for stale-but-allowed installs. See ADR-225 and
7
+ * `apps/admin-api/src/runtime/version.contract.ts` for the server side.
8
+ *
9
+ * # Why the constants live here, not in `@pugi/sdk`
10
+ *
11
+ * The SDK ships to customers as part of the public npm package; bumping
12
+ * a header name there would force a coupled CLI ↔ SDK release. Keeping
13
+ * the constants in the CLI gives us room to evolve the handshake
14
+ * without coupling the SDK's release cadence to it.
15
+ */
16
+ /**
17
+ * Defensive semver sanitizer — single source of truth for the
18
+ * `0.0.0-unknown` fallback when a partially-published pnpm release
19
+ * leaks `workspace:*` into the version literal. `runtime/cli.ts`
20
+ * re-exports this under its `__test__` surface so the existing
21
+ * cli.ts test corpus keeps working with zero churn.
22
+ *
23
+ * Lives here (not in cli.ts) because the version handshake interceptor
24
+ * needs the same guarantee without pulling in the heavyweight cli.ts
25
+ * module graph during transport bootstrap.
26
+ */
27
+ export function sanitizeSemver(raw) {
28
+ if (typeof raw !== 'string')
29
+ return '0.0.0-unknown';
30
+ const trimmed = raw.trim();
31
+ if (!trimmed)
32
+ return '0.0.0-unknown';
33
+ const stripped = trimmed.replace(/^(workspace:|npm:|file:)/, '');
34
+ if (/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(stripped)) {
35
+ return stripped;
36
+ }
37
+ return '0.0.0-unknown';
38
+ }
39
+ /**
40
+ * Installed CLI version — single source of truth for the handshake
41
+ * header value. Mirrors the literal in `runtime/cli.ts` (intentional
42
+ * duplication because the cli.ts module pulls in Ink + half the CLI
43
+ * surface and the transport interceptor must remain side-effect-free
44
+ * during import). When bumping the CLI version BOTH literals must be
45
+ * updated; the release smoke-test (`pack:smoke`) verifies they agree.
46
+ */
47
+ export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.41');
48
+ /**
49
+ * Outbound: the CLI's installed semver. Read at request time by
50
+ * `version-interceptor.ts` and injected on every `fetch` call.
51
+ */
52
+ export const PUGI_CLI_VERSION_HEADER = 'X-Pugi-Cli-Version';
53
+ /**
54
+ * Inbound: the server's recommendation for which CLI version operators
55
+ * should be on. Surfaces through the UpdateBanner alongside the npm
56
+ * registry poll (the higher of the two wins).
57
+ */
58
+ export const PUGI_CLI_UPGRADE_RECOMMENDED_HEADER = 'X-Pugi-Cli-Upgrade-Recommended';
59
+ /**
60
+ * Inbound: server's own admin-api version. Useful for diagnostics
61
+ * (`pugi doctor --json`) but no enforcement logic keys off it — server
62
+ * drift between admin-api releases is normal during rolling deploys.
63
+ */
64
+ export const PUGI_SERVER_VERSION_HEADER = 'X-Pugi-Server-Version';
65
+ //# sourceMappingURL=version.js.map