@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.51

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 (264) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/jobs-watch.js +201 -0
  6. package/dist/commands/jobs.js +15 -0
  7. package/dist/commands/smoke.js +133 -0
  8. package/dist/core/agent-progress/cleanup.js +134 -0
  9. package/dist/core/agent-progress/schema.js +144 -0
  10. package/dist/core/agent-progress/writer.js +101 -0
  11. package/dist/core/artifact-chain/dispatcher.js +148 -0
  12. package/dist/core/artifact-chain/exporter.js +164 -0
  13. package/dist/core/artifact-chain/state.js +243 -0
  14. package/dist/core/artifact-chain/steps.js +169 -0
  15. package/dist/core/auth/ensure-authenticated.js +129 -0
  16. package/dist/core/auth/env-provider.js +238 -0
  17. package/dist/core/auto-update/channels.js +122 -0
  18. package/dist/core/auto-update/checker.js +241 -0
  19. package/dist/core/auto-update/state.js +235 -0
  20. package/dist/core/bare-mode/index.js +107 -0
  21. package/dist/core/bash-classifier.js +400 -4
  22. package/dist/core/checkpoint/resumer.js +149 -0
  23. package/dist/core/checkpoint/rewinder.js +291 -0
  24. package/dist/core/codegraph/decision-store.js +248 -0
  25. package/dist/core/codegraph/detect-repo.js +459 -0
  26. package/dist/core/codegraph/install.js +134 -0
  27. package/dist/core/codegraph/offer-hook.js +220 -0
  28. package/dist/core/compact/auto-trigger.js +96 -0
  29. package/dist/core/compact/buffer-rewriter.js +115 -0
  30. package/dist/core/compact/summarizer.js +208 -0
  31. package/dist/core/compact/token-counter.js +108 -0
  32. package/dist/core/consensus/diff-capture.js +112 -3
  33. package/dist/core/context/index.js +7 -0
  34. package/dist/core/context/markdown-traverse.js +255 -0
  35. package/dist/core/cost/rate-card.js +129 -0
  36. package/dist/core/cost/tracker.js +221 -0
  37. package/dist/core/denial-tracking/index.js +8 -0
  38. package/dist/core/denial-tracking/state.js +264 -0
  39. package/dist/core/diagnostics/probe-runner.js +93 -0
  40. package/dist/core/diagnostics/probes/api.js +46 -0
  41. package/dist/core/diagnostics/probes/auth.js +86 -0
  42. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  43. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  44. package/dist/core/diagnostics/probes/config.js +72 -0
  45. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  46. package/dist/core/diagnostics/probes/disk.js +81 -0
  47. package/dist/core/diagnostics/probes/git.js +65 -0
  48. package/dist/core/diagnostics/probes/hooks.js +118 -0
  49. package/dist/core/diagnostics/probes/mcp.js +75 -0
  50. package/dist/core/diagnostics/probes/node.js +59 -0
  51. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  52. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  53. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  54. package/dist/core/diagnostics/probes/session.js +74 -0
  55. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  56. package/dist/core/diagnostics/probes/workspace.js +63 -0
  57. package/dist/core/diagnostics/types.js +70 -0
  58. package/dist/core/dispatch/cache-cleanup.js +197 -0
  59. package/dist/core/dispatch/cache-handoff.js +295 -0
  60. package/dist/core/edits/dispatch.js +218 -2
  61. package/dist/core/edits/journal.js +199 -0
  62. package/dist/core/edits/layer-d-ast.js +557 -14
  63. package/dist/core/edits/verify-hook.js +273 -0
  64. package/dist/core/edits/worktree.js +322 -0
  65. package/dist/core/engine/anvil-client.js +115 -5
  66. package/dist/core/engine/auto-compact.js +179 -0
  67. package/dist/core/engine/budgets.js +155 -0
  68. package/dist/core/engine/context-prefix.js +155 -0
  69. package/dist/core/engine/intent.js +260 -0
  70. package/dist/core/engine/native-pugi.js +897 -211
  71. package/dist/core/engine/prompts.js +88 -2
  72. package/dist/core/engine/strip-internal-fields.js +124 -0
  73. package/dist/core/engine/tool-bridge.js +1045 -36
  74. package/dist/core/feedback/queue.js +177 -0
  75. package/dist/core/feedback/submitter.js +145 -0
  76. package/dist/core/file-cache.js +113 -1
  77. package/dist/core/hooks/events.js +44 -0
  78. package/dist/core/hooks/index.js +15 -0
  79. package/dist/core/hooks/registry.js +213 -0
  80. package/dist/core/hooks/runner.js +236 -0
  81. package/dist/core/hooks/v2/event-emitter.js +115 -0
  82. package/dist/core/hooks/v2/executor.js +282 -0
  83. package/dist/core/hooks/v2/index.js +25 -0
  84. package/dist/core/hooks/v2/lifecycle.js +104 -0
  85. package/dist/core/hooks/v2/loader.js +216 -0
  86. package/dist/core/hooks/v2/matcher.js +125 -0
  87. package/dist/core/hooks/v2/trust.js +143 -0
  88. package/dist/core/hooks/v2/types.js +86 -0
  89. package/dist/core/lsp/cache.js +105 -0
  90. package/dist/core/lsp/client.js +776 -0
  91. package/dist/core/lsp/language-detect.js +66 -0
  92. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  93. package/dist/core/mcp/client.js +75 -6
  94. package/dist/core/mcp/http-server.js +553 -0
  95. package/dist/core/mcp/orchestrator-tools.js +662 -0
  96. package/dist/core/mcp/permission.js +190 -0
  97. package/dist/core/mcp/registry.js +24 -2
  98. package/dist/core/mcp/server-tools.js +219 -0
  99. package/dist/core/mcp/server.js +397 -0
  100. package/dist/core/memory/dual-write.js +416 -0
  101. package/dist/core/memory/phase1-kinds.js +20 -0
  102. package/dist/core/memory-sync/queue.js +158 -0
  103. package/dist/core/onboarding/ensure-initialized.js +133 -0
  104. package/dist/core/onboarding/marker.js +111 -0
  105. package/dist/core/onboarding/telemetry-state.js +108 -0
  106. package/dist/core/output-style/presets.js +176 -0
  107. package/dist/core/output-style/state.js +185 -0
  108. package/dist/core/path-security.js +284 -2
  109. package/dist/core/permissions/auto-classifier.js +124 -0
  110. package/dist/core/permissions/circuit-breaker.js +83 -0
  111. package/dist/core/permissions/gate.js +278 -0
  112. package/dist/core/permissions/index.js +20 -0
  113. package/dist/core/permissions/mode.js +174 -0
  114. package/dist/core/permissions/state.js +241 -0
  115. package/dist/core/permissions/tool-class.js +93 -0
  116. package/dist/core/prd-check/parser.js +215 -0
  117. package/dist/core/prd-check/reporter.js +127 -0
  118. package/dist/core/prd-check/session-review.js +557 -0
  119. package/dist/core/prd-check/verifiers.js +223 -0
  120. package/dist/core/pugi-md/context-injector.js +76 -0
  121. package/dist/core/pugi-md/walk-up.js +207 -0
  122. package/dist/core/release-notes/parser.js +241 -0
  123. package/dist/core/release-notes/state.js +116 -0
  124. package/dist/core/repl/history.js +11 -1
  125. package/dist/core/repl/model-pricing.js +135 -0
  126. package/dist/core/repl/session.js +1897 -37
  127. package/dist/core/repl/slash-commands.js +430 -15
  128. package/dist/core/repl/store/session-store.js +31 -2
  129. package/dist/core/repl/workspace-context.js +22 -0
  130. package/dist/core/repo-map/build.js +125 -0
  131. package/dist/core/repo-map/cache.js +185 -0
  132. package/dist/core/repo-map/extractor.js +254 -0
  133. package/dist/core/repo-map/formatter.js +145 -0
  134. package/dist/core/repo-map/scanner.js +211 -0
  135. package/dist/core/retry-budget/budget.js +284 -0
  136. package/dist/core/retry-budget/index.js +5 -0
  137. package/dist/core/session.js +92 -0
  138. package/dist/core/settings.js +80 -0
  139. package/dist/core/share/formatter.js +271 -0
  140. package/dist/core/share/redactor.js +221 -0
  141. package/dist/core/share/uploader.js +267 -0
  142. package/dist/core/skills/defaults.js +457 -0
  143. package/dist/core/smoke/headless-driver.js +174 -0
  144. package/dist/core/smoke/orchestrator.js +194 -0
  145. package/dist/core/smoke/runner.js +238 -0
  146. package/dist/core/smoke/scenario-parser.js +316 -0
  147. package/dist/core/subagents/dispatcher-real.js +600 -0
  148. package/dist/core/subagents/dispatcher.js +113 -24
  149. package/dist/core/subagents/index.js +18 -5
  150. package/dist/core/subagents/isolation-matrix.js +213 -0
  151. package/dist/core/subagents/spawn.js +19 -4
  152. package/dist/core/telemetry/emitter.js +229 -0
  153. package/dist/core/telemetry/queue.js +251 -0
  154. package/dist/core/theme/context.js +91 -0
  155. package/dist/core/theme/presets.js +228 -0
  156. package/dist/core/theme/state.js +181 -0
  157. package/dist/core/todos/invariant.js +10 -0
  158. package/dist/core/todos/state.js +177 -0
  159. package/dist/core/transport/version-interceptor.js +166 -0
  160. package/dist/core/vim/keymap.js +288 -0
  161. package/dist/core/vim/state.js +92 -0
  162. package/dist/core/worktree-manager/cleanup.js +123 -0
  163. package/dist/core/worktree-manager/manager.js +303 -0
  164. package/dist/index.js +28 -0
  165. package/dist/runtime/bootstrap.js +190 -0
  166. package/dist/runtime/cli.js +3241 -343
  167. package/dist/runtime/commands/cancel.js +231 -0
  168. package/dist/runtime/commands/chain.js +489 -0
  169. package/dist/runtime/commands/codegraph-status.js +227 -0
  170. package/dist/runtime/commands/compact.js +297 -0
  171. package/dist/runtime/commands/cost.js +199 -0
  172. package/dist/runtime/commands/delegate.js +242 -11
  173. package/dist/runtime/commands/dispatch.js +126 -0
  174. package/dist/runtime/commands/doctor.js +412 -0
  175. package/dist/runtime/commands/feedback.js +184 -0
  176. package/dist/runtime/commands/hooks.js +184 -0
  177. package/dist/runtime/commands/lsp.js +368 -0
  178. package/dist/runtime/commands/mcp.js +879 -0
  179. package/dist/runtime/commands/memory.js +508 -0
  180. package/dist/runtime/commands/model.js +237 -0
  181. package/dist/runtime/commands/onboarding.js +275 -0
  182. package/dist/runtime/commands/patch.js +128 -0
  183. package/dist/runtime/commands/permissions.js +112 -0
  184. package/dist/runtime/commands/plan.js +143 -0
  185. package/dist/runtime/commands/prd-check.js +285 -0
  186. package/dist/runtime/commands/redo-blob-store.js +92 -0
  187. package/dist/runtime/commands/redo.js +361 -0
  188. package/dist/runtime/commands/release-notes.js +229 -0
  189. package/dist/runtime/commands/repo-map.js +95 -0
  190. package/dist/runtime/commands/report.js +299 -0
  191. package/dist/runtime/commands/resume.js +118 -0
  192. package/dist/runtime/commands/review-consensus.js +17 -2
  193. package/dist/runtime/commands/rewind.js +333 -0
  194. package/dist/runtime/commands/sessions.js +163 -0
  195. package/dist/runtime/commands/share.js +316 -0
  196. package/dist/runtime/commands/status.js +186 -0
  197. package/dist/runtime/commands/stickers.js +82 -0
  198. package/dist/runtime/commands/style.js +194 -0
  199. package/dist/runtime/commands/theme.js +196 -0
  200. package/dist/runtime/commands/undo.js +32 -0
  201. package/dist/runtime/commands/update.js +289 -0
  202. package/dist/runtime/commands/vim.js +140 -0
  203. package/dist/runtime/commands/worktree.js +177 -0
  204. package/dist/runtime/commands/worktrees.js +155 -0
  205. package/dist/runtime/headless-repl.js +195 -0
  206. package/dist/runtime/headless.js +543 -0
  207. package/dist/runtime/load-hooks-or-exit.js +71 -0
  208. package/dist/runtime/plan-decompose.js +531 -0
  209. package/dist/runtime/version.js +65 -0
  210. package/dist/tools/agent-tool.js +229 -0
  211. package/dist/tools/apply-patch.js +556 -0
  212. package/dist/tools/ask-user-question.js +213 -0
  213. package/dist/tools/ask-user.js +115 -0
  214. package/dist/tools/bash.js +203 -4
  215. package/dist/tools/file-tools.js +85 -14
  216. package/dist/tools/lsp-tools.js +189 -0
  217. package/dist/tools/mcp-tool.js +260 -0
  218. package/dist/tools/multi-edit.js +361 -0
  219. package/dist/tools/powershell.js +268 -0
  220. package/dist/tools/registry.js +51 -0
  221. package/dist/tools/skill-tool.js +96 -0
  222. package/dist/tools/tasks.js +208 -0
  223. package/dist/tools/todo-write.js +184 -0
  224. package/dist/tools/web-fetch.js +147 -2
  225. package/dist/tools/web-search.js +458 -0
  226. package/dist/tui/agent-progress-card.js +111 -0
  227. package/dist/tui/agent-tree.js +10 -0
  228. package/dist/tui/ask-modal.js +2 -2
  229. package/dist/tui/ask-user-question-prompt.js +192 -0
  230. package/dist/tui/compact-banner.js +81 -0
  231. package/dist/tui/conversation-pane.js +82 -8
  232. package/dist/tui/cost-table.js +111 -0
  233. package/dist/tui/doctor-table.js +46 -0
  234. package/dist/tui/feedback-prompt.js +156 -0
  235. package/dist/tui/input-box.js +218 -3
  236. package/dist/tui/markdown-render.js +4 -4
  237. package/dist/tui/onboarding-wizard.js +240 -0
  238. package/dist/tui/permissions-picker.js +86 -0
  239. package/dist/tui/render.js +35 -0
  240. package/dist/tui/repl-render.js +313 -35
  241. package/dist/tui/repl-splash-art.js +1 -1
  242. package/dist/tui/repl-splash-mascot.js +32 -8
  243. package/dist/tui/repl-splash.js +2 -2
  244. package/dist/tui/repl.js +85 -5
  245. package/dist/tui/splash.js +1 -1
  246. package/dist/tui/status-bar.js +94 -16
  247. package/dist/tui/status-table.js +7 -0
  248. package/dist/tui/stickers-art.js +136 -0
  249. package/dist/tui/style-table.js +28 -0
  250. package/dist/tui/theme-table.js +29 -0
  251. package/dist/tui/thinking-spinner.js +123 -0
  252. package/dist/tui/tool-stream-pane.js +52 -3
  253. package/dist/tui/update-banner.js +27 -2
  254. package/dist/tui/vim-input.js +267 -0
  255. package/dist/tui/welcome-banner.js +107 -0
  256. package/dist/tui/welcome-data.js +293 -0
  257. package/docs/examples/codegraph.mcp.json +10 -0
  258. package/package.json +13 -7
  259. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  260. package/test/scenarios/compact-force.scenario.txt +11 -0
  261. package/test/scenarios/identity.scenario.txt +11 -0
  262. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  263. package/test/scenarios/walkback.scenario.txt +12 -0
  264. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Per-workspace permission-mode session state — Leak L6.
3
+ *
4
+ * State lives in `.pugi/session.json` under the workspace root. The
5
+ * file is read on first `getCurrentMode()` call (cached for the
6
+ * process lifetime) and written atomically via tmp+rename on
7
+ * `setCurrentMode()` so a kill mid-write does not corrupt the JSON.
8
+ *
9
+ * Resolution order for the effective mode on a fresh process:
10
+ * 1. CLI flag (`pugi --mode plan`) — passed via `resolveMode` arg;
11
+ * not read from disk here.
12
+ * 2. Workspace session state — `<root>/.pugi/session.json` field
13
+ * `permissionMode`.
14
+ * 3. Global config — `~/.pugi/config.json` field
15
+ * `defaultPermissionMode`.
16
+ * 4. Hard default `ask`.
17
+ *
18
+ * This module owns layers 2 + 3. The CLI arg parser owns layer 1; both
19
+ * funnel into `resolveMode()` which performs the merge.
20
+ */
21
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
22
+ import { dirname, resolve } from 'node:path';
23
+ import { homedir } from 'node:os';
24
+ import { z } from 'zod';
25
+ import { DEFAULT_PERMISSION_MODE, parsePermissionMode, } from './mode.js';
26
+ /**
27
+ * Wave 7: zod enum for the canonical 6-mode taxonomy. Includes α6
28
+ * aliases (`ask`, `allow`, `bypass`) as accepted input — Zod parses
29
+ * them, the helpers below remap к canonical names before returning к
30
+ * the caller. Persistence always writes the canonical name so the file
31
+ * migrates forward on next save.
32
+ */
33
+ const permissionModeEnum = z.enum([
34
+ // Canonical Wave 7 names.
35
+ 'default',
36
+ 'acceptEdits',
37
+ 'plan',
38
+ 'auto',
39
+ 'dontAsk',
40
+ 'bypassPermissions',
41
+ // α6 backwards-compat aliases — resolved via parsePermissionMode.
42
+ 'ask',
43
+ 'allow',
44
+ 'bypass',
45
+ ]);
46
+ const sessionStateSchema = z
47
+ .object({
48
+ permissionMode: permissionModeEnum.optional(),
49
+ /**
50
+ * Leak L7: snapshot of the mode that was active immediately BEFORE
51
+ * the operator typed `/plan` (or `/plan <prompt>`). `/plan --back`
52
+ * pops this snapshot and restores it. Cleared after a successful
53
+ * pop so a second `/plan --back` does not double-revert.
54
+ */
55
+ previousPermissionMode: permissionModeEnum.optional(),
56
+ })
57
+ .partial()
58
+ .passthrough();
59
+ const globalConfigSchema = z
60
+ .object({
61
+ defaultPermissionMode: permissionModeEnum.optional(),
62
+ })
63
+ .partial()
64
+ .passthrough();
65
+ const SESSION_FILE = '.pugi/session.json';
66
+ /**
67
+ * Return the path to the workspace session-state file.
68
+ */
69
+ export function sessionStatePath(workspaceRoot) {
70
+ return resolve(workspaceRoot, SESSION_FILE);
71
+ }
72
+ /**
73
+ * Return the path to the user-global config file. Uses HOME env when
74
+ * present (test fixtures, CI) so we never accidentally hit the real
75
+ * user-global file in spec runs.
76
+ */
77
+ export function globalConfigPath(homeDir = homedir()) {
78
+ return resolve(homeDir, '.pugi/config.json');
79
+ }
80
+ /**
81
+ * Read the workspace's saved permission mode. Returns null when the
82
+ * file is absent OR the field is unset; the caller layers in CLI + env
83
+ * + global config defaults to produce the effective mode.
84
+ *
85
+ * Never throws on JSON parse / schema errors — a malformed session
86
+ * file should not break the gate. The defensive `try/catch` returns
87
+ * null and lets the caller fall through to the next layer.
88
+ */
89
+ export function getCurrentMode(workspaceRoot) {
90
+ const path = sessionStatePath(workspaceRoot);
91
+ if (!existsSync(path))
92
+ return null;
93
+ try {
94
+ const raw = readFileSync(path, 'utf8');
95
+ const parsed = sessionStateSchema.parse(JSON.parse(raw));
96
+ if (typeof parsed.permissionMode !== 'string')
97
+ return null;
98
+ // Wave 7: parsePermissionMode resolves α6 aliases (`ask`, `allow`,
99
+ // `bypass`) to their canonical Wave 7 names. A session file written
100
+ // by α6.x is silently upgraded on read.
101
+ return parsePermissionMode(parsed.permissionMode);
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ /**
108
+ * Persist the workspace's permission mode. Creates the `.pugi/` dir
109
+ * when missing; preserves any unrelated keys in the file (passthrough
110
+ * schema). Atomic tmp+rename so a kill mid-write does not corrupt the
111
+ * JSON.
112
+ */
113
+ export function setCurrentMode(workspaceRoot, mode) {
114
+ const path = sessionStatePath(workspaceRoot);
115
+ mkdirSync(dirname(path), { recursive: true });
116
+ const existing = existsSync(path)
117
+ ? safeParseObject(readFileSync(path, 'utf8'))
118
+ : {};
119
+ const next = { ...existing, permissionMode: mode };
120
+ const tmpPath = `${path}.tmp`;
121
+ writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
122
+ renameSync(tmpPath, path);
123
+ }
124
+ /**
125
+ * Leak L7 — read the snapshot of the mode that was active before the
126
+ * most-recent `/plan` (or `pugi plan`) entry. Returns null when the
127
+ * file is absent OR the field is unset. Same defensive behaviour as
128
+ * `getCurrentMode`: a malformed session file never breaks the slash
129
+ * command — the worst case is `/plan --back` reports "no previous
130
+ * mode to restore" and the operator picks the target mode explicitly.
131
+ */
132
+ export function getPreviousMode(workspaceRoot) {
133
+ const path = sessionStatePath(workspaceRoot);
134
+ if (!existsSync(path))
135
+ return null;
136
+ try {
137
+ const raw = readFileSync(path, 'utf8');
138
+ const parsed = sessionStateSchema.parse(JSON.parse(raw));
139
+ if (typeof parsed.previousPermissionMode !== 'string')
140
+ return null;
141
+ return parsePermissionMode(parsed.previousPermissionMode);
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ /**
148
+ * Leak L7 — record the mode that was active immediately before the
149
+ * operator switched to plan. The runtime calls this AT `/plan` entry
150
+ * with the current mode (whatever `resolveMode` returned). Atomic
151
+ * tmp+rename keeps the snapshot consistent if the process is killed
152
+ * mid-write. Pass `null` to clear the snapshot (used after a
153
+ * successful `/plan --back` so a second `--back` does not loop).
154
+ */
155
+ export function setPreviousMode(workspaceRoot, mode) {
156
+ const path = sessionStatePath(workspaceRoot);
157
+ mkdirSync(dirname(path), { recursive: true });
158
+ const existing = existsSync(path)
159
+ ? safeParseObject(readFileSync(path, 'utf8'))
160
+ : {};
161
+ const next = { ...existing };
162
+ if (mode === null) {
163
+ delete next.previousPermissionMode;
164
+ }
165
+ else {
166
+ next.previousPermissionMode = mode;
167
+ }
168
+ const tmpPath = `${path}.tmp`;
169
+ writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
170
+ renameSync(tmpPath, path);
171
+ }
172
+ /**
173
+ * Read `~/.pugi/config.json::defaultPermissionMode`. Returns null when
174
+ * the file is absent / the field is unset; same defensive behaviour
175
+ * as `getCurrentMode` — a malformed global config never breaks the gate.
176
+ */
177
+ export function getGlobalDefaultMode(homeDir = homedir()) {
178
+ const path = globalConfigPath(homeDir);
179
+ if (!existsSync(path))
180
+ return null;
181
+ try {
182
+ const raw = readFileSync(path, 'utf8');
183
+ const parsed = globalConfigSchema.parse(JSON.parse(raw));
184
+ if (typeof parsed.defaultPermissionMode !== 'string')
185
+ return null;
186
+ return parsePermissionMode(parsed.defaultPermissionMode);
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
192
+ /**
193
+ * Persist `~/.pugi/config.json::defaultPermissionMode`. Used by the
194
+ * `/permissions <mode> --persist` flow so a future fresh session
195
+ * defaults to the same mode without an explicit `--mode` flag.
196
+ */
197
+ export function setGlobalDefaultMode(mode, homeDir = homedir()) {
198
+ const path = globalConfigPath(homeDir);
199
+ mkdirSync(dirname(path), { recursive: true });
200
+ const existing = existsSync(path)
201
+ ? safeParseObject(readFileSync(path, 'utf8'))
202
+ : {};
203
+ const next = { ...existing, defaultPermissionMode: mode };
204
+ const tmpPath = `${path}.tmp`;
205
+ writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
206
+ renameSync(tmpPath, path);
207
+ }
208
+ export function resolveMode(options) {
209
+ if (options.cliFlag) {
210
+ const flag = parsePermissionMode(options.cliFlag);
211
+ if (flag)
212
+ return flag;
213
+ }
214
+ const workspace = getCurrentMode(options.workspaceRoot);
215
+ if (workspace)
216
+ return workspace;
217
+ const global = getGlobalDefaultMode(options.homeDir);
218
+ if (global)
219
+ return global;
220
+ return DEFAULT_PERMISSION_MODE;
221
+ }
222
+ /**
223
+ * Defensive helper — parse JSON to an object; non-object payload (top-
224
+ * level array, primitive) collapses to an empty object so the merge
225
+ * doesn't surface a TypeError. The `setCurrentMode` / `setGlobalDefaultMode`
226
+ * helpers only write objects, so a non-object existing file is corrupted
227
+ * and we explicitly reset it rather than appending into a non-object.
228
+ */
229
+ function safeParseObject(raw) {
230
+ try {
231
+ const parsed = JSON.parse(raw);
232
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
233
+ return parsed;
234
+ }
235
+ return {};
236
+ }
237
+ catch {
238
+ return {};
239
+ }
240
+ }
241
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Tool side-effect classification — Leak L6.
3
+ *
4
+ * Three classes drive the canonical 4-mode permission gate:
5
+ *
6
+ * - `read` — observe-only. Plan mode allows; ask still prompts;
7
+ * allow + bypass execute silently. Examples: read,
8
+ * grep, glob, web_fetch, web_search, skills_list.
9
+ * - `write` — mutates workspace, journal, or operator screen with
10
+ * visible side effects. Plan mode refuses; ask prompts;
11
+ * allow + bypass execute. Examples: write, edit, bash,
12
+ * multi_edit, task_*. `ask_user_question` is also
13
+ * classed as `write` because it interrupts the
14
+ * dispatcher's flow control and demands operator
15
+ * attention — plan mode should not prompt operators.
16
+ * - `dispatch` — spawns a child subagent or off-tree task. Plan mode
17
+ * refuses (a write-capable child violates plan-mode's
18
+ * read-only contract); ask prompts; allow + bypass
19
+ * execute. Example: `agent`.
20
+ *
21
+ * Unknown tool names default to `write` — deny-first safety. A stale
22
+ * schema entry that the gate has not been told about should not silently
23
+ * pass in plan mode just because the gate doesn't recognise it.
24
+ */
25
+ /**
26
+ * Closed map of every built-in tool name -> side-effect class. The
27
+ * source of truth for the four standard modes; mirrored against the
28
+ * `WIRED_TOOLS` set in `core/engine/tool-bridge.ts` so an unrecognised
29
+ * tool surfaces as the safe deny-first `write` default.
30
+ *
31
+ * MCP tools follow the `mcp__<server>__<tool>` namespace and are
32
+ * uniformly classed via `getToolClass` because per-tool annotations are
33
+ * not yet a part of the MCP spec — treating them as `write` is the
34
+ * conservative default until server-side metadata is trustworthy.
35
+ */
36
+ const BUILT_IN_TOOL_CLASSES = Object.freeze({
37
+ // Read-only observations.
38
+ read: 'read',
39
+ grep: 'read',
40
+ glob: 'read',
41
+ ls: 'read',
42
+ search: 'read',
43
+ web_fetch: 'read',
44
+ web_search: 'read',
45
+ file_cache_check: 'read',
46
+ skills_list: 'read',
47
+ skill: 'read',
48
+ task_get: 'read',
49
+ task_list: 'read',
50
+ // Mutating actions.
51
+ write: 'write',
52
+ edit: 'write',
53
+ multi_edit: 'write',
54
+ bash: 'write',
55
+ task_create: 'write',
56
+ task_update: 'write',
57
+ todo_write: 'write',
58
+ // `ask_user_question` halts the loop and demands operator attention.
59
+ // Plan mode should not interrupt — class as write so the gate refuses
60
+ // it in plan mode but ask + allow + bypass execute normally.
61
+ ask_user_question: 'write',
62
+ // Dispatch — spawn a child agent. Refused in plan mode regardless of
63
+ // the child's role tier (the engine adapter applies role-based
64
+ // capability filtering, but the gate refuses dispatch up front so a
65
+ // plan-mode session cannot leak a writeable child).
66
+ agent: 'dispatch',
67
+ pugi_delegate: 'dispatch',
68
+ sub_agent_spawn: 'dispatch',
69
+ });
70
+ const MCP_TOOL_PREFIX = 'mcp__';
71
+ /**
72
+ * Resolve the class for a tool name. Unknown names default to `write`
73
+ * (deny-first). MCP tools (any name prefixed with `mcp__`) default to
74
+ * `write` for the same conservative reason — the MCP spec lacks
75
+ * per-tool annotations today.
76
+ */
77
+ export function getToolClass(toolName) {
78
+ const builtIn = BUILT_IN_TOOL_CLASSES[toolName];
79
+ if (builtIn)
80
+ return builtIn;
81
+ if (toolName.startsWith(MCP_TOOL_PREFIX))
82
+ return 'write';
83
+ return 'write';
84
+ }
85
+ /**
86
+ * Expose the built-in class map for diagnostic surfaces (`pugi doctor`,
87
+ * test fixtures). Caller MUST NOT mutate — the object is already frozen
88
+ * so any attempt throws in strict mode.
89
+ */
90
+ export function listBuiltInToolClasses() {
91
+ return BUILT_IN_TOOL_CLASSES;
92
+ }
93
+ //# sourceMappingURL=tool-class.js.map
@@ -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