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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) 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/session-review.js +557 -0
  98. package/dist/core/prd-check/verifiers.js +223 -0
  99. package/dist/core/pugi-md/context-injector.js +76 -0
  100. package/dist/core/pugi-md/walk-up.js +207 -0
  101. package/dist/core/release-notes/parser.js +241 -0
  102. package/dist/core/release-notes/state.js +116 -0
  103. package/dist/core/repl/codebase-survey.js +308 -0
  104. package/dist/core/repl/history.js +11 -1
  105. package/dist/core/repl/init-interview.js +457 -0
  106. package/dist/core/repl/model-pricing.js +135 -0
  107. package/dist/core/repl/onboarding-state.js +297 -0
  108. package/dist/core/repl/session.js +1529 -30
  109. package/dist/core/repl/slash-commands.js +361 -13
  110. package/dist/core/repl/store/session-store.js +31 -2
  111. package/dist/core/repl/workspace-context.js +22 -0
  112. package/dist/core/repo-map/build.js +125 -0
  113. package/dist/core/repo-map/cache.js +185 -0
  114. package/dist/core/repo-map/extractor.js +254 -0
  115. package/dist/core/repo-map/formatter.js +145 -0
  116. package/dist/core/repo-map/scanner.js +211 -0
  117. package/dist/core/retry-budget/budget.js +284 -0
  118. package/dist/core/retry-budget/index.js +5 -0
  119. package/dist/core/session.js +44 -0
  120. package/dist/core/settings.js +80 -0
  121. package/dist/core/share/formatter.js +271 -0
  122. package/dist/core/share/redactor.js +221 -0
  123. package/dist/core/share/uploader.js +267 -0
  124. package/dist/core/skills/defaults.js +457 -0
  125. package/dist/core/subagents/dispatcher-real.js +600 -0
  126. package/dist/core/subagents/dispatcher.js +113 -24
  127. package/dist/core/subagents/index.js +18 -5
  128. package/dist/core/subagents/isolation-matrix.js +213 -0
  129. package/dist/core/subagents/spawn.js +19 -4
  130. package/dist/core/telemetry/emitter.js +229 -0
  131. package/dist/core/telemetry/queue.js +251 -0
  132. package/dist/core/theme/context.js +91 -0
  133. package/dist/core/theme/presets.js +228 -0
  134. package/dist/core/theme/state.js +181 -0
  135. package/dist/core/todos/invariant.js +10 -0
  136. package/dist/core/todos/state.js +177 -0
  137. package/dist/core/transport/version-interceptor.js +166 -0
  138. package/dist/core/vim/keymap.js +288 -0
  139. package/dist/core/vim/state.js +92 -0
  140. package/dist/index.js +28 -0
  141. package/dist/runtime/bootstrap.js +190 -0
  142. package/dist/runtime/cli.js +2603 -278
  143. package/dist/runtime/commands/chain.js +489 -0
  144. package/dist/runtime/commands/compact.js +297 -0
  145. package/dist/runtime/commands/cost.js +199 -0
  146. package/dist/runtime/commands/delegate.js +312 -0
  147. package/dist/runtime/commands/dispatch.js +126 -0
  148. package/dist/runtime/commands/doctor.js +390 -0
  149. package/dist/runtime/commands/feedback.js +184 -0
  150. package/dist/runtime/commands/hooks.js +184 -0
  151. package/dist/runtime/commands/lsp.js +212 -28
  152. package/dist/runtime/commands/mcp.js +824 -0
  153. package/dist/runtime/commands/memory.js +508 -0
  154. package/dist/runtime/commands/memory.spec.js +174 -0
  155. package/dist/runtime/commands/model.js +237 -0
  156. package/dist/runtime/commands/onboarding.js +275 -0
  157. package/dist/runtime/commands/patch.js +17 -0
  158. package/dist/runtime/commands/permissions.js +87 -0
  159. package/dist/runtime/commands/plan.js +143 -0
  160. package/dist/runtime/commands/prd-check.js +285 -0
  161. package/dist/runtime/commands/release-notes.js +229 -0
  162. package/dist/runtime/commands/repo-map.js +95 -0
  163. package/dist/runtime/commands/report.js +299 -0
  164. package/dist/runtime/commands/resume.js +118 -0
  165. package/dist/runtime/commands/review-consensus.js +17 -2
  166. package/dist/runtime/commands/rewind.js +333 -0
  167. package/dist/runtime/commands/roster.js +117 -0
  168. package/dist/runtime/commands/sessions.js +163 -0
  169. package/dist/runtime/commands/share.js +316 -0
  170. package/dist/runtime/commands/status.js +178 -0
  171. package/dist/runtime/commands/stickers.js +82 -0
  172. package/dist/runtime/commands/style.js +194 -0
  173. package/dist/runtime/commands/theme.js +196 -0
  174. package/dist/runtime/commands/update.js +289 -0
  175. package/dist/runtime/commands/vim.js +140 -0
  176. package/dist/runtime/commands/worktree.js +50 -6
  177. package/dist/runtime/headless.js +543 -0
  178. package/dist/runtime/load-hooks-or-exit.js +71 -0
  179. package/dist/runtime/plan-decompose.js +531 -0
  180. package/dist/runtime/version.js +65 -0
  181. package/dist/tools/agent-tool.js +229 -0
  182. package/dist/tools/apply-patch.js +281 -39
  183. package/dist/tools/ask-user-question.js +213 -0
  184. package/dist/tools/ask-user.js +115 -0
  185. package/dist/tools/file-tools.js +85 -14
  186. package/dist/tools/mcp-tool.js +260 -0
  187. package/dist/tools/multi-edit.js +361 -0
  188. package/dist/tools/registry.js +30 -2
  189. package/dist/tools/skill-tool.js +96 -0
  190. package/dist/tools/tasks.js +208 -0
  191. package/dist/tools/todo-write.js +184 -0
  192. package/dist/tools/web-fetch.js +147 -2
  193. package/dist/tools/web-search.js +458 -0
  194. package/dist/tui/agent-progress-card.js +111 -0
  195. package/dist/tui/agent-tree.js +10 -0
  196. package/dist/tui/ask-modal.js +2 -2
  197. package/dist/tui/ask-user-question-prompt.js +192 -0
  198. package/dist/tui/compact-banner.js +81 -0
  199. package/dist/tui/conversation-pane.js +82 -8
  200. package/dist/tui/cost-table.js +111 -0
  201. package/dist/tui/doctor-table.js +46 -0
  202. package/dist/tui/feedback-prompt.js +156 -0
  203. package/dist/tui/input-box.js +46 -2
  204. package/dist/tui/markdown-render.js +4 -4
  205. package/dist/tui/onboarding-wizard.js +240 -0
  206. package/dist/tui/repl-render.js +293 -35
  207. package/dist/tui/repl-splash.js +2 -2
  208. package/dist/tui/repl.js +45 -13
  209. package/dist/tui/splash.js +1 -1
  210. package/dist/tui/status-bar.js +94 -16
  211. package/dist/tui/status-table.js +7 -0
  212. package/dist/tui/stickers-art.js +136 -0
  213. package/dist/tui/style-table.js +28 -0
  214. package/dist/tui/theme-table.js +29 -0
  215. package/dist/tui/tool-stream-pane.js +7 -0
  216. package/dist/tui/update-banner.js +20 -2
  217. package/dist/tui/vim-input.js +267 -0
  218. package/docs/examples/codegraph.mcp.json +10 -0
  219. package/package.json +9 -6
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Permission gate — Leak L6 canonical 4-mode enforcement.
3
+ *
4
+ * Single dispatch entry point. Every tool call goes through `gate()`
5
+ * before the executor runs the tool body; the executor surfaces the
6
+ * `PermissionDenied` error as a model-readable sentinel so the model
7
+ * can either reformulate the request or wait for the operator to
8
+ * change the mode.
9
+ *
10
+ * Routing matrix (mode × class):
11
+ *
12
+ * | read | write | dispatch
13
+ * plan | allow | deny | deny
14
+ * ask | ask | ask | ask
15
+ * allow | allow | allow | allow
16
+ * bypass | allow | allow | allow (plus: hooks bypassed)
17
+ *
18
+ * In ask mode the gate consults a session-scoped `always-allow` cache
19
+ * keyed by tool name (set when the operator picks "always-allow-tool"
20
+ * in the prompt). The cache is in-memory only — restarting the session
21
+ * resets it, by design (every-session-fresh ask consent).
22
+ *
23
+ * Bypass mode does NOT take a different code path in this module — the
24
+ * `hooksBypassed` flag in the decision payload signals the executor /
25
+ * hook layer to skip policy hooks. The classification logic is the
26
+ * same as `allow` because the gate doesn't own hook execution; the
27
+ * caller decides what to do with the bypass signal.
28
+ */
29
+ import { getToolClass } from './tool-class.js';
30
+ export const ASK_OPTIONS = Object.freeze([
31
+ 'allow-once',
32
+ 'always-this-tool',
33
+ 'deny-once',
34
+ 'always-deny-this-tool',
35
+ ]);
36
+ export function createAskAlwaysCache() {
37
+ return {
38
+ alwaysAllowed: new Set(),
39
+ alwaysDenied: new Set(),
40
+ };
41
+ }
42
+ /**
43
+ * Apply the operator's answer to an `ask` decision. Caller invokes this
44
+ * after the operator picks an option so the cache stays in sync.
45
+ * Returns the effective decision: `allow-once` / `always-this-tool`
46
+ * become `allow`; `deny-once` / `always-deny-this-tool` become `deny`.
47
+ *
48
+ * `always-*` answers persist to the cache and short-circuit the next
49
+ * gate call for the same tool name within the same session.
50
+ */
51
+ export function applyAskAnswer(cache, toolName, answer) {
52
+ switch (answer) {
53
+ case 'allow-once':
54
+ return { decision: 'allow', reason: `Allowed once for ${toolName}` };
55
+ case 'always-this-tool':
56
+ cache.alwaysAllowed.add(toolName);
57
+ cache.alwaysDenied.delete(toolName);
58
+ return { decision: 'allow', reason: `Allowed for ${toolName} this session` };
59
+ case 'deny-once':
60
+ return { decision: 'deny', reason: `Denied once for ${toolName}` };
61
+ case 'always-deny-this-tool':
62
+ cache.alwaysDenied.add(toolName);
63
+ cache.alwaysAllowed.delete(toolName);
64
+ return { decision: 'deny', reason: `Denied for ${toolName} this session` };
65
+ }
66
+ }
67
+ /**
68
+ * Permission-denied sentinel. Distinguishable from other tool errors
69
+ * (parse errors, IO failures) so the caller can route the message back
70
+ * to the model with the canonical recovery hint.
71
+ */
72
+ export class PermissionDenied extends Error {
73
+ name = 'PermissionDenied';
74
+ mode;
75
+ toolName;
76
+ toolClass;
77
+ /**
78
+ * Human-friendly reason surfaced in logs / hook payloads. Distinct
79
+ * from `message` so the spec layer can pattern-match the canonical
80
+ * `PERMISSION_DENIED:` sentinel verbatim while operators see the
81
+ * full explanation in console output.
82
+ */
83
+ reason;
84
+ constructor(toolName, toolClass, mode, reason) {
85
+ // The base Error.message is the canonical sentinel so default
86
+ // toString() / re-throw paths preserve the format the model and
87
+ // the spec layer pattern-match against.
88
+ super(`PERMISSION_DENIED: ${toolName} blocked in ${mode} mode. Operator can switch with /permissions <mode>.`);
89
+ this.mode = mode;
90
+ this.toolName = toolName;
91
+ this.toolClass = toolClass;
92
+ this.reason = reason;
93
+ }
94
+ /**
95
+ * Render the sentinel message the executor surfaces to the model.
96
+ * The string format is stable so a parent agent / E2E spec can
97
+ * pattern-match `PERMISSION_DENIED: <tool> blocked in <mode> mode.`
98
+ * verbatim. Equivalent to `this.message`; kept as a method so
99
+ * downstream callers can use whichever spelling reads better at the
100
+ * site.
101
+ */
102
+ toModelMessage() {
103
+ return this.message;
104
+ }
105
+ }
106
+ /**
107
+ * Core dispatch gate. Pure function — no IO, no side effects beyond
108
+ * mutating the caller-supplied `alwaysCache`. Safe to call from any
109
+ * layer (engine adapter, agent-as-tool bridge, doctor command).
110
+ *
111
+ * Argument bag mirrors the executor entry shape:
112
+ * - `toolName` is the registered tool key (e.g. `read`, `write`,
113
+ * `mcp__github__list_issues`).
114
+ * - `args` is the raw arg payload. Currently unused in the routing
115
+ * decision — the matrix only cares about class. Plumbed in
116
+ * because future "always-allow-this-pattern" rules (e.g.
117
+ * `git status` auto-allow) will consume it without changing the
118
+ * callsite contract.
119
+ * - `ctx` carries mode + session-scoped state.
120
+ */
121
+ export function gate(toolName,
122
+ // Reserved for future pattern-based rules (always-allow `git status`).
123
+ // Suppress unused-argument lint — the contract is stable on purpose.
124
+ _args, ctx) {
125
+ const toolClass = getToolClass(toolName);
126
+ const cache = ctx.alwaysCache;
127
+ // Ask-mode session memory: an explicit "always-deny" beats any other
128
+ // routing because the operator has actively refused this tool.
129
+ if (cache?.alwaysDenied.has(toolName)) {
130
+ return {
131
+ decision: 'deny',
132
+ reason: `Tool ${toolName} denied for the session via /permissions ask`,
133
+ };
134
+ }
135
+ // "Always-allow" in ask mode skips the prompt for subsequent calls.
136
+ // Plan mode IGNORES the always-allow cache because plan mode's
137
+ // contract is structural (read-only), not consent-based.
138
+ if (cache?.alwaysAllowed.has(toolName) && ctx.permissionMode === 'ask') {
139
+ return {
140
+ decision: 'allow',
141
+ reason: `Tool ${toolName} always-allowed for this session`,
142
+ };
143
+ }
144
+ switch (ctx.permissionMode) {
145
+ case 'plan': {
146
+ if (toolClass === 'read') {
147
+ return { decision: 'allow', reason: `Plan mode: read tools allowed (${toolName})` };
148
+ }
149
+ return {
150
+ decision: 'deny',
151
+ reason: `Plan mode: ${toolClass} tools blocked. Switch with /permissions allow.`,
152
+ };
153
+ }
154
+ case 'ask': {
155
+ return {
156
+ decision: 'ask',
157
+ reason: `Ask mode: prompt before ${toolName}`,
158
+ question: buildAskQuestion(toolName, toolClass, ctx.target),
159
+ options: ASK_OPTIONS,
160
+ toolClass,
161
+ };
162
+ }
163
+ case 'allow': {
164
+ return {
165
+ decision: 'allow',
166
+ reason: `Allow mode: ${toolName} executed`,
167
+ };
168
+ }
169
+ case 'bypass': {
170
+ return {
171
+ decision: 'allow',
172
+ reason: `Bypass mode: ${toolName} executed (policy hooks skipped)`,
173
+ hooksBypassed: true,
174
+ };
175
+ }
176
+ }
177
+ }
178
+ /**
179
+ * Build the operator-facing question string for an ask-mode prompt.
180
+ * Kept in one place so the wording stays consistent across the REPL
181
+ * Ink modal and the simpler stdin fallback.
182
+ */
183
+ function buildAskQuestion(toolName, toolClass, target) {
184
+ const suffix = target ? ` on ${target}` : '';
185
+ return `Allow ${toolName} (${toolClass})${suffix}?`;
186
+ }
187
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Permission gate (Leak L6) public surface.
3
+ *
4
+ * Re-exports the canonical 4-mode types, the tool-class classifier,
5
+ * the dispatch gate, and the workspace + global session-state helpers
6
+ * so callers import from one place:
7
+ *
8
+ * import { gate, resolveMode, PermissionDenied } from '<...>/permissions/index.js';
9
+ *
10
+ * Keeps the internal file split (mode / tool-class / gate / state)
11
+ * invisible to consumers — those files are an implementation detail
12
+ * the engine adapter does not need to know about.
13
+ */
14
+ export { DEFAULT_PERMISSION_MODE, PERMISSION_MODE_GLOSS, PERMISSION_MODES, isPermissionMode, parsePermissionMode, toLegacyMode, } from './mode.js';
15
+ export { getToolClass, listBuiltInToolClasses, } from './tool-class.js';
16
+ export { ASK_OPTIONS, PermissionDenied, applyAskAnswer, createAskAlwaysCache, gate, } from './gate.js';
17
+ export { getCurrentMode, getGlobalDefaultMode, getPreviousMode, globalConfigPath, resolveMode, sessionStatePath, setCurrentMode, setGlobalDefaultMode, setPreviousMode, } from './state.js';
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Permission modes — canonical 4-mode taxonomy (Leak L6).
3
+ *
4
+ * Pugi historically shipped a 6-mode taxonomy in `@pugi/sdk`
5
+ * (`plan | ask | acceptEdits | auto | dontAsk | bypassPermissions`)
6
+ * which the legacy `core/permission.ts` engine maps tools onto. Claude
7
+ * Code, Codex, and the openclaude / openwork leaks all converge on a
8
+ * smaller, sharper 4-mode set:
9
+ *
10
+ * - `plan` — read-only proposal mode. Write/dispatch tools refused
11
+ * with a deterministic sentinel; the model is expected
12
+ * to surface a plan, not execute it.
13
+ * - `ask` — every tool execution prompts the operator. Default
14
+ * mode for new operators; the safe ground state.
15
+ * - `allow` — every tool executes without per-call prompts, BUT
16
+ * the policy hook layer (skill-steering, denial audit,
17
+ * destructive deny-list) still fires.
18
+ * - `bypass` — same as allow but ALSO skips policy hooks. Power-user
19
+ * mode for trusted scripted runs; surface a banner on
20
+ * entry so an operator who flips here by accident sees
21
+ * they have disengaged the audit layer.
22
+ *
23
+ * This module owns the union type, the canonical default, and the
24
+ * mode-resolution helper. The runtime gate (`gate.ts`) consumes it; the
25
+ * legacy 6-mode SDK enum remains the system-of-record for bash-class
26
+ * decisions inside `core/permission.ts` — the canonical 4-mode layer
27
+ * sits in front and short-circuits the dispatch decision before bash
28
+ * classification ever runs.
29
+ */
30
+ /**
31
+ * Closed list — useful for input validation and slash-command help.
32
+ */
33
+ export const PERMISSION_MODES = Object.freeze([
34
+ 'plan',
35
+ 'ask',
36
+ 'allow',
37
+ 'bypass',
38
+ ]);
39
+ /**
40
+ * Default mode applied when no `--mode` flag, no per-workspace session
41
+ * state, and no `defaultPermissionMode` in `~/.pugi/config.json`. We
42
+ * default cautious (`ask`) — an operator who has not configured anything
43
+ * is treated as a new operator who deserves visibility into every tool
44
+ * call.
45
+ */
46
+ export const DEFAULT_PERMISSION_MODE = 'ask';
47
+ /**
48
+ * Type guard for arbitrary string input (CLI flag, session.json
49
+ * deserialization). Returns false for casing variants — caller is
50
+ * expected to lowercase before testing.
51
+ */
52
+ export function isPermissionMode(value) {
53
+ return typeof value === 'string' && PERMISSION_MODES.includes(value);
54
+ }
55
+ /**
56
+ * Parse + validate a mode string. Returns null for invalid input so the
57
+ * caller can surface a typed error (`unknown mode: <value>`) instead of
58
+ * throwing from a parse helper.
59
+ */
60
+ export function parsePermissionMode(value) {
61
+ const lower = value.trim().toLowerCase();
62
+ return isPermissionMode(lower) ? lower : null;
63
+ }
64
+ /**
65
+ * Map the canonical 4-mode taxonomy to the legacy 6-mode SDK enum used
66
+ * by `core/permission.ts::evaluateBashPermission` and friends. The map
67
+ * is intentionally surjective on a narrower target — the canonical
68
+ * layer is the new public contract, the legacy layer is plumbing.
69
+ *
70
+ * plan -> 'plan' (read-only)
71
+ * ask -> 'ask' (prompt every action)
72
+ * allow -> 'auto' (allow non-destructive; deny destructive)
73
+ * bypass -> 'bypassPermissions' (allow everything except destructive override)
74
+ *
75
+ * Callers that need the legacy enum (existing bash classifier, settings
76
+ * persistence) should funnel through this helper so the mapping is in
77
+ * one place.
78
+ */
79
+ export function toLegacyMode(mode) {
80
+ switch (mode) {
81
+ case 'plan':
82
+ return 'plan';
83
+ case 'ask':
84
+ return 'ask';
85
+ case 'allow':
86
+ return 'auto';
87
+ case 'bypass':
88
+ return 'bypassPermissions';
89
+ }
90
+ }
91
+ /**
92
+ * One-line human-readable summary surfaced by the `/permissions` table
93
+ * and `pugi --help` text. Kept inline so the strings stay localizable
94
+ * via a single edit point.
95
+ */
96
+ export const PERMISSION_MODE_GLOSS = Object.freeze({
97
+ plan: 'Read-only — propose, never execute. Write + dispatch tools refused.',
98
+ ask: 'Prompt before every tool call. Default for new operators.',
99
+ allow: 'Execute tools without prompts. Policy hooks still fire.',
100
+ bypass: 'Execute tools without prompts AND skip policy hooks. Power-user only.',
101
+ });
102
+ //# sourceMappingURL=mode.js.map
@@ -0,0 +1,215 @@
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, isPermissionMode, parsePermissionMode, } from './mode.js';
26
+ const permissionModeEnum = z.enum(['plan', 'ask', 'allow', 'bypass']);
27
+ const sessionStateSchema = z
28
+ .object({
29
+ permissionMode: permissionModeEnum.optional(),
30
+ /**
31
+ * Leak L7: snapshot of the mode that was active immediately BEFORE
32
+ * the operator typed `/plan` (or `/plan <prompt>`). `/plan --back`
33
+ * pops this snapshot and restores it. Cleared after a successful
34
+ * pop so a second `/plan --back` does not double-revert.
35
+ */
36
+ previousPermissionMode: permissionModeEnum.optional(),
37
+ })
38
+ .partial()
39
+ .passthrough();
40
+ const globalConfigSchema = z
41
+ .object({
42
+ defaultPermissionMode: permissionModeEnum.optional(),
43
+ })
44
+ .partial()
45
+ .passthrough();
46
+ const SESSION_FILE = '.pugi/session.json';
47
+ /**
48
+ * Return the path to the workspace session-state file.
49
+ */
50
+ export function sessionStatePath(workspaceRoot) {
51
+ return resolve(workspaceRoot, SESSION_FILE);
52
+ }
53
+ /**
54
+ * Return the path to the user-global config file. Uses HOME env when
55
+ * present (test fixtures, CI) so we never accidentally hit the real
56
+ * user-global file in spec runs.
57
+ */
58
+ export function globalConfigPath(homeDir = homedir()) {
59
+ return resolve(homeDir, '.pugi/config.json');
60
+ }
61
+ /**
62
+ * Read the workspace's saved permission mode. Returns null when the
63
+ * file is absent OR the field is unset; the caller layers in CLI + env
64
+ * + global config defaults to produce the effective mode.
65
+ *
66
+ * Never throws on JSON parse / schema errors — a malformed session
67
+ * file should not break the gate. The defensive `try/catch` returns
68
+ * null and lets the caller fall through to the next layer.
69
+ */
70
+ export function getCurrentMode(workspaceRoot) {
71
+ const path = sessionStatePath(workspaceRoot);
72
+ if (!existsSync(path))
73
+ return null;
74
+ try {
75
+ const raw = readFileSync(path, 'utf8');
76
+ const parsed = sessionStateSchema.parse(JSON.parse(raw));
77
+ return isPermissionMode(parsed.permissionMode) ? parsed.permissionMode : null;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ /**
84
+ * Persist the workspace's permission mode. Creates the `.pugi/` dir
85
+ * when missing; preserves any unrelated keys in the file (passthrough
86
+ * schema). Atomic tmp+rename so a kill mid-write does not corrupt the
87
+ * JSON.
88
+ */
89
+ export function setCurrentMode(workspaceRoot, mode) {
90
+ const path = sessionStatePath(workspaceRoot);
91
+ mkdirSync(dirname(path), { recursive: true });
92
+ const existing = existsSync(path)
93
+ ? safeParseObject(readFileSync(path, 'utf8'))
94
+ : {};
95
+ const next = { ...existing, permissionMode: mode };
96
+ const tmpPath = `${path}.tmp`;
97
+ writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
98
+ renameSync(tmpPath, path);
99
+ }
100
+ /**
101
+ * Leak L7 — read the snapshot of the mode that was active before the
102
+ * most-recent `/plan` (or `pugi plan`) entry. Returns null when the
103
+ * file is absent OR the field is unset. Same defensive behaviour as
104
+ * `getCurrentMode`: a malformed session file never breaks the slash
105
+ * command — the worst case is `/plan --back` reports "no previous
106
+ * mode to restore" and the operator picks the target mode explicitly.
107
+ */
108
+ export function getPreviousMode(workspaceRoot) {
109
+ const path = sessionStatePath(workspaceRoot);
110
+ if (!existsSync(path))
111
+ return null;
112
+ try {
113
+ const raw = readFileSync(path, 'utf8');
114
+ const parsed = sessionStateSchema.parse(JSON.parse(raw));
115
+ return isPermissionMode(parsed.previousPermissionMode)
116
+ ? parsed.previousPermissionMode
117
+ : null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ /**
124
+ * Leak L7 — record the mode that was active immediately before the
125
+ * operator switched to plan. The runtime calls this AT `/plan` entry
126
+ * with the current mode (whatever `resolveMode` returned). Atomic
127
+ * tmp+rename keeps the snapshot consistent if the process is killed
128
+ * mid-write. Pass `null` to clear the snapshot (used after a
129
+ * successful `/plan --back` so a second `--back` does not loop).
130
+ */
131
+ export function setPreviousMode(workspaceRoot, mode) {
132
+ const path = sessionStatePath(workspaceRoot);
133
+ mkdirSync(dirname(path), { recursive: true });
134
+ const existing = existsSync(path)
135
+ ? safeParseObject(readFileSync(path, 'utf8'))
136
+ : {};
137
+ const next = { ...existing };
138
+ if (mode === null) {
139
+ delete next.previousPermissionMode;
140
+ }
141
+ else {
142
+ next.previousPermissionMode = mode;
143
+ }
144
+ const tmpPath = `${path}.tmp`;
145
+ writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
146
+ renameSync(tmpPath, path);
147
+ }
148
+ /**
149
+ * Read `~/.pugi/config.json::defaultPermissionMode`. Returns null when
150
+ * the file is absent / the field is unset; same defensive behaviour
151
+ * as `getCurrentMode` — a malformed global config never breaks the gate.
152
+ */
153
+ export function getGlobalDefaultMode(homeDir = homedir()) {
154
+ const path = globalConfigPath(homeDir);
155
+ if (!existsSync(path))
156
+ return null;
157
+ try {
158
+ const raw = readFileSync(path, 'utf8');
159
+ const parsed = globalConfigSchema.parse(JSON.parse(raw));
160
+ return isPermissionMode(parsed.defaultPermissionMode) ? parsed.defaultPermissionMode : null;
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ /**
167
+ * Persist `~/.pugi/config.json::defaultPermissionMode`. Used by the
168
+ * `/permissions <mode> --persist` flow so a future fresh session
169
+ * defaults to the same mode without an explicit `--mode` flag.
170
+ */
171
+ export function setGlobalDefaultMode(mode, homeDir = homedir()) {
172
+ const path = globalConfigPath(homeDir);
173
+ mkdirSync(dirname(path), { recursive: true });
174
+ const existing = existsSync(path)
175
+ ? safeParseObject(readFileSync(path, 'utf8'))
176
+ : {};
177
+ const next = { ...existing, defaultPermissionMode: mode };
178
+ const tmpPath = `${path}.tmp`;
179
+ writeFileSync(tmpPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
180
+ renameSync(tmpPath, path);
181
+ }
182
+ export function resolveMode(options) {
183
+ if (options.cliFlag) {
184
+ const flag = parsePermissionMode(options.cliFlag);
185
+ if (flag)
186
+ return flag;
187
+ }
188
+ const workspace = getCurrentMode(options.workspaceRoot);
189
+ if (workspace)
190
+ return workspace;
191
+ const global = getGlobalDefaultMode(options.homeDir);
192
+ if (global)
193
+ return global;
194
+ return DEFAULT_PERMISSION_MODE;
195
+ }
196
+ /**
197
+ * Defensive helper — parse JSON to an object; non-object payload (top-
198
+ * level array, primitive) collapses to an empty object so the merge
199
+ * doesn't surface a TypeError. The `setCurrentMode` / `setGlobalDefaultMode`
200
+ * helpers only write objects, so a non-object existing file is corrupted
201
+ * and we explicitly reset it rather than appending into a non-object.
202
+ */
203
+ function safeParseObject(raw) {
204
+ try {
205
+ const parsed = JSON.parse(raw);
206
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
207
+ return parsed;
208
+ }
209
+ return {};
210
+ }
211
+ catch {
212
+ return {};
213
+ }
214
+ }
215
+ //# 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