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

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 (249) 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 +992 -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/registry.js +46 -0
  211. package/dist/tools/skill-tool.js +96 -0
  212. package/dist/tools/tasks.js +208 -0
  213. package/dist/tools/todo-write.js +184 -0
  214. package/dist/tools/web-fetch.js +147 -2
  215. package/dist/tools/web-search.js +458 -0
  216. package/dist/tui/agent-progress-card.js +111 -0
  217. package/dist/tui/agent-tree.js +10 -0
  218. package/dist/tui/ask-modal.js +2 -2
  219. package/dist/tui/ask-user-question-prompt.js +192 -0
  220. package/dist/tui/compact-banner.js +81 -0
  221. package/dist/tui/conversation-pane.js +82 -8
  222. package/dist/tui/cost-table.js +111 -0
  223. package/dist/tui/doctor-table.js +46 -0
  224. package/dist/tui/feedback-prompt.js +156 -0
  225. package/dist/tui/input-box.js +69 -2
  226. package/dist/tui/markdown-render.js +4 -4
  227. package/dist/tui/onboarding-wizard.js +240 -0
  228. package/dist/tui/permissions-picker.js +86 -0
  229. package/dist/tui/render.js +35 -0
  230. package/dist/tui/repl-render.js +303 -13
  231. package/dist/tui/repl-splash.js +2 -2
  232. package/dist/tui/repl.js +72 -14
  233. package/dist/tui/splash.js +1 -1
  234. package/dist/tui/status-bar.js +94 -16
  235. package/dist/tui/status-table.js +7 -0
  236. package/dist/tui/stickers-art.js +136 -0
  237. package/dist/tui/style-table.js +28 -0
  238. package/dist/tui/theme-table.js +29 -0
  239. package/dist/tui/tool-stream-pane.js +52 -3
  240. package/dist/tui/update-banner.js +20 -2
  241. package/dist/tui/vim-input.js +267 -0
  242. package/docs/examples/codegraph.mcp.json +10 -0
  243. package/package.json +12 -6
  244. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  245. package/test/scenarios/compact-force.scenario.txt +11 -0
  246. package/test/scenarios/identity.scenario.txt +11 -0
  247. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  248. package/test/scenarios/walkback.scenario.txt +12 -0
  249. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,489 @@
1
+ /**
2
+ * `pugi chain` — Pugi α7 Wave 6 artifact chain command (2026-05-27).
3
+ *
4
+ * The chain encodes Pugi's moat: an operator drops one high-level
5
+ * intent and the CLI walks a deterministic 7-step pipeline (PRD →
6
+ * ADR → mindmap → ER → sequence → tests → code), each step
7
+ * dispatched to a specialist persona. The CLI surface mirrors `pugi
8
+ * doctor` / `pugi prd-check`: thin wiring layer that resolves cwd +
9
+ * flags + writeOutput, with the heavy lifting in
10
+ * `core/artifact-chain/{state,steps,dispatcher,exporter}.ts`.
11
+ *
12
+ * Subcommand grammar:
13
+ *
14
+ * pugi chain new "<intent>" — start new chain
15
+ * pugi chain status [<chain-id>] — show current cursor + step table
16
+ * pugi chain next [<chain-id>] — advance to next step (operator approves prior)
17
+ * pugi chain show <step> [<chain-id>] — render one artifact
18
+ * pugi chain export [<chain-id>] [--json] — bundle as markdown/JSON
19
+ * pugi chain list — list every chain in the workspace
20
+ *
21
+ * When `<chain-id>` is omitted the handler resolves the most-recent
22
+ * chain on disk (operator workflow optimization — the common case is
23
+ * iterating on the chain you just created).
24
+ *
25
+ * Module contract:
26
+ *
27
+ * - `runChainCommand` is the single entry point. Both the top-level
28
+ * `pugi chain` handler in `runtime/cli.ts` AND the in-REPL
29
+ * `/chain` slash command call it. Subcommand parsing is inline so
30
+ * a typo surfaces with a usage hint instead of silently dropping.
31
+ *
32
+ * - The dispatcher dep is injected so the spec can drive every
33
+ * branch without standing up Anvil. Real wire-up binds to the
34
+ * existing `submitDelegate` SDK helper via the SSE waiter.
35
+ *
36
+ * - Exit codes:
37
+ * 0 — happy path
38
+ * 1 — runtime failure (no chain found / no credentials / dispatch failed)
39
+ * 2 — usage error (unknown subcommand / missing required arg)
40
+ */
41
+ import { submitDelegate } from '@pugi/sdk';
42
+ import { chainDir, createChain, listChainIds, markComplete, readChain, } from '../../core/artifact-chain/state.js';
43
+ import { CHAIN_STEPS, findStep, } from '../../core/artifact-chain/steps.js';
44
+ import { dispatchStep, } from '../../core/artifact-chain/dispatcher.js';
45
+ import { exportChain, } from '../../core/artifact-chain/exporter.js';
46
+ import { existsSync, readFileSync, statSync } from 'node:fs';
47
+ import { join } from 'node:path';
48
+ import { waitForDelegateTerminal, } from './delegate.js';
49
+ /**
50
+ * Parse + run the chain command. Non-throwing — every failure mode
51
+ * surfaces a structured envelope. Sets `process.exitCode` so the
52
+ * cli.ts handler does not need a try/catch wrapper.
53
+ */
54
+ export async function runChainCommand(args, ctx) {
55
+ const [rawSub, ...rest] = args;
56
+ if (!rawSub) {
57
+ emitUsage(ctx, 'missing subcommand');
58
+ return;
59
+ }
60
+ const sub = rawSub.toLowerCase();
61
+ switch (sub) {
62
+ case 'new':
63
+ return handleNew(rest, ctx);
64
+ case 'status':
65
+ return handleStatus(rest, ctx);
66
+ case 'next':
67
+ return handleNext(rest, ctx);
68
+ case 'show':
69
+ return handleShow(rest, ctx);
70
+ case 'export':
71
+ return handleExport(rest, ctx);
72
+ case 'list':
73
+ return handleList(rest, ctx);
74
+ default:
75
+ emitUsage(ctx, `unknown subcommand '${rawSub}'`);
76
+ return;
77
+ }
78
+ }
79
+ /* ------------------------------------------------------------------ */
80
+ /* Subcommand handlers */
81
+ /* ------------------------------------------------------------------ */
82
+ async function handleNew(args, ctx) {
83
+ const intent = args.join(' ').trim();
84
+ if (intent.length === 0) {
85
+ ctx.writeOutput({
86
+ ok: false,
87
+ command: 'chain',
88
+ subcommand: 'new',
89
+ error: 'intent must be non-empty',
90
+ hint: 'Usage: pugi chain new "<one-sentence intent>"',
91
+ }, 'pugi chain new: intent must be non-empty.\nUsage: pugi chain new "<one-sentence intent>"');
92
+ process.exitCode = 2;
93
+ return;
94
+ }
95
+ let state;
96
+ try {
97
+ state = createChain(ctx.cwd, intent, ctx.now ? { now: ctx.now } : {});
98
+ }
99
+ catch (err) {
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ ctx.writeOutput({ ok: false, command: 'chain', subcommand: 'new', error: message }, `pugi chain new: ${message}`);
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ const firstStep = state.nextStep ? findStep(state.nextStep) : null;
106
+ ctx.writeOutput({
107
+ ok: true,
108
+ command: 'chain',
109
+ subcommand: 'new',
110
+ chainId: state.id,
111
+ data: { intent: state.intent, nextStep: state.nextStep, dir: chainDir(ctx.cwd, state.id) },
112
+ }, [
113
+ `chain ${state.id} created.`,
114
+ `intent: ${state.intent}`,
115
+ firstStep
116
+ ? `next: ${firstStep.id} — ${firstStep.gloss} (persona: ${firstStep.personaLabel})`
117
+ : 'next: (none — chain already finalised)',
118
+ `dir: ${chainDir(ctx.cwd, state.id)}`,
119
+ 'Run `pugi chain next` to dispatch the first step.',
120
+ ].join('\n'));
121
+ }
122
+ async function handleStatus(args, ctx) {
123
+ const chainId = await resolveChainId(args[0], ctx, 'status');
124
+ if (!chainId)
125
+ return;
126
+ const state = readChain(ctx.cwd, chainId);
127
+ if (!state) {
128
+ ctx.writeOutput({
129
+ ok: false,
130
+ command: 'chain',
131
+ subcommand: 'status',
132
+ error: `chain ${chainId} not found`,
133
+ }, `pugi chain status: chain ${chainId} not found.`);
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ const next = state.nextStep ? findStep(state.nextStep) : null;
138
+ const lines = [];
139
+ lines.push(`chain ${state.id} — ${state.intent}`);
140
+ lines.push(`created ${state.createdAt}; updated ${state.updatedAt}`);
141
+ lines.push('');
142
+ lines.push(' # step status persona dispatch');
143
+ for (const step of CHAIN_STEPS) {
144
+ const record = state.steps.find((s) => s.id === step.id);
145
+ if (!record)
146
+ continue;
147
+ lines.push(` ${step.ordinal} ${step.id.padEnd(8)} ${record.status.padEnd(10)} ${step.personaLabel.padEnd(15)} ${record.dispatchId ?? '-'}`);
148
+ }
149
+ lines.push('');
150
+ lines.push(next
151
+ ? `next: ${next.id} — ${next.gloss}`
152
+ : 'next: (none — chain finalised)');
153
+ ctx.writeOutput({
154
+ ok: true,
155
+ command: 'chain',
156
+ subcommand: 'status',
157
+ chainId: state.id,
158
+ data: {
159
+ intent: state.intent,
160
+ nextStep: state.nextStep,
161
+ steps: state.steps,
162
+ },
163
+ }, lines.join('\n'));
164
+ }
165
+ async function handleNext(args, ctx) {
166
+ const chainId = await resolveChainId(args[0], ctx, 'next');
167
+ if (!chainId)
168
+ return;
169
+ const state = readChain(ctx.cwd, chainId);
170
+ if (!state) {
171
+ ctx.writeOutput({
172
+ ok: false,
173
+ command: 'chain',
174
+ subcommand: 'next',
175
+ error: `chain ${chainId} not found`,
176
+ }, `pugi chain next: chain ${chainId} not found.`);
177
+ process.exitCode = 1;
178
+ return;
179
+ }
180
+ // Operator approval gate: if the most-recent step is in `dispatched`
181
+ // and its artifact file is on disk, flip it to complete before
182
+ // advancing. This is the "operator reviewed the artifact, ship the
183
+ // next step" semantic — running `pugi chain next` twice in a row
184
+ // walks the chain forward by one step each time.
185
+ const pendingApproval = state.steps.find((s) => s.status === 'dispatched');
186
+ let workingState = state;
187
+ if (pendingApproval) {
188
+ const step = findStep(pendingApproval.id);
189
+ const artifactPath = step
190
+ ? join(chainDir(ctx.cwd, state.id), step.artifactFilename)
191
+ : null;
192
+ if (artifactPath && existsSync(artifactPath)) {
193
+ workingState = markComplete(ctx.cwd, state.id, pendingApproval.id, ctx.now ? { now: ctx.now } : {});
194
+ }
195
+ }
196
+ if (!workingState.nextStep) {
197
+ ctx.writeOutput({
198
+ ok: true,
199
+ command: 'chain',
200
+ subcommand: 'next',
201
+ chainId: workingState.id,
202
+ data: { finalised: true },
203
+ }, `chain ${workingState.id} is finalised — every step is complete.`);
204
+ return;
205
+ }
206
+ const dispatcher = ctx.dispatch ?? buildLiveDispatcher(ctx);
207
+ const result = await dispatchStep({
208
+ workspaceCwd: ctx.cwd,
209
+ chainId: workingState.id,
210
+ dispatch: dispatcher,
211
+ ...(ctx.now ? { now: ctx.now } : {}),
212
+ });
213
+ if (!result.ok) {
214
+ ctx.writeOutput({
215
+ ok: false,
216
+ command: 'chain',
217
+ subcommand: 'next',
218
+ error: result.error,
219
+ hint: `step ${result.stepId} did not complete; re-run after fixing the issue.`,
220
+ }, `pugi chain next: ${result.error}`);
221
+ process.exitCode = 1;
222
+ return;
223
+ }
224
+ const step = findStep(result.stepId);
225
+ ctx.writeOutput({
226
+ ok: true,
227
+ command: 'chain',
228
+ subcommand: 'next',
229
+ chainId: workingState.id,
230
+ data: {
231
+ stepId: result.stepId,
232
+ dispatchId: result.dispatchId,
233
+ artifactPath: result.artifactPath,
234
+ nextStep: result.state.nextStep,
235
+ },
236
+ }, [
237
+ `dispatched ${result.stepId} (${step?.personaLabel ?? 'unknown persona'}); dispatchId=${result.dispatchId}.`,
238
+ `artifact: ${result.artifactPath}`,
239
+ 'Review the artifact, then run `pugi chain next` again to advance.',
240
+ ].join('\n'));
241
+ }
242
+ async function handleShow(args, ctx) {
243
+ const [stepArg, idArg] = args;
244
+ if (!stepArg) {
245
+ emitUsage(ctx, 'usage: pugi chain show <step> [<chain-id>]');
246
+ return;
247
+ }
248
+ const step = findStep(stepArg.toLowerCase());
249
+ if (!step) {
250
+ ctx.writeOutput({
251
+ ok: false,
252
+ command: 'chain',
253
+ subcommand: 'show',
254
+ error: `unknown step '${stepArg}'`,
255
+ hint: `valid: ${CHAIN_STEPS.map((s) => s.id).join(', ')}`,
256
+ }, `pugi chain show: unknown step '${stepArg}'. Valid: ${CHAIN_STEPS.map((s) => s.id).join(', ')}.`);
257
+ process.exitCode = 2;
258
+ return;
259
+ }
260
+ const chainId = await resolveChainId(idArg, ctx, 'show');
261
+ if (!chainId)
262
+ return;
263
+ const path = join(chainDir(ctx.cwd, chainId), step.artifactFilename);
264
+ if (!existsSync(path)) {
265
+ ctx.writeOutput({
266
+ ok: false,
267
+ command: 'chain',
268
+ subcommand: 'show',
269
+ error: `artifact not found: ${path}`,
270
+ hint: `step '${step.id}' may not have been dispatched yet.`,
271
+ }, `pugi chain show: artifact not found at ${path}.`);
272
+ process.exitCode = 1;
273
+ return;
274
+ }
275
+ const contents = readFileSync(path, 'utf8');
276
+ ctx.writeOutput({
277
+ ok: true,
278
+ command: 'chain',
279
+ subcommand: 'show',
280
+ chainId,
281
+ data: { stepId: step.id, path, contents },
282
+ }, contents);
283
+ }
284
+ async function handleExport(args, ctx) {
285
+ const jsonFlag = args.includes('--json');
286
+ const positional = args.filter((a) => !a.startsWith('--'));
287
+ const chainId = await resolveChainId(positional[0], ctx, 'export');
288
+ if (!chainId)
289
+ return;
290
+ const format = jsonFlag || ctx.json ? 'json' : 'markdown';
291
+ // exportChain is non-throwing per the artifact-chain module contract;
292
+ // we branch on the structured discriminator instead of a try/catch.
293
+ // Gemini P1 fix (PR #608, 2026-05-27): the historical impl threw on
294
+ // missing chain, which broke the contract documented in
295
+ // `core/artifact-chain/exporter.ts`.
296
+ const result = exportChain({ workspaceCwd: ctx.cwd, chainId, format });
297
+ if (!result.ok) {
298
+ ctx.writeOutput({ ok: false, command: 'chain', subcommand: 'export', error: result.error }, `pugi chain export: ${result.error}`);
299
+ process.exitCode = 1;
300
+ return;
301
+ }
302
+ ctx.writeOutput({
303
+ ok: true,
304
+ command: 'chain',
305
+ subcommand: 'export',
306
+ chainId,
307
+ data: {
308
+ format,
309
+ missingSteps: result.envelope.missingSteps,
310
+ bytes: result.rendered.length,
311
+ },
312
+ }, result.rendered);
313
+ }
314
+ async function handleList(_args, ctx) {
315
+ const ids = listChainIds(ctx.cwd);
316
+ if (ids.length === 0) {
317
+ ctx.writeOutput({ ok: true, command: 'chain', subcommand: 'list', data: { chains: [] } }, 'no chains in this workspace. Run `pugi chain new "<intent>"` to start one.');
318
+ return;
319
+ }
320
+ const rows = [];
321
+ const lines = ['chain id next step intent'];
322
+ for (const id of ids) {
323
+ const state = readChain(ctx.cwd, id);
324
+ if (!state)
325
+ continue;
326
+ rows.push({ id, intent: state.intent, nextStep: state.nextStep });
327
+ lines.push(`${id.padEnd(15)} ${(state.nextStep ?? 'done').padEnd(12)} ${state.intent}`);
328
+ }
329
+ ctx.writeOutput({ ok: true, command: 'chain', subcommand: 'list', data: { chains: rows } }, lines.join('\n'));
330
+ }
331
+ /* ------------------------------------------------------------------ */
332
+ /* Helpers */
333
+ /* ------------------------------------------------------------------ */
334
+ function emitUsage(ctx, error) {
335
+ ctx.writeOutput({
336
+ ok: false,
337
+ command: 'chain',
338
+ subcommand: 'usage',
339
+ error,
340
+ hint: 'Usage:\n' +
341
+ ' pugi chain new "<intent>"\n' +
342
+ ' pugi chain status [<chain-id>]\n' +
343
+ ' pugi chain next [<chain-id>]\n' +
344
+ ' pugi chain show <step> [<chain-id>]\n' +
345
+ ' pugi chain export [<chain-id>] [--json]\n' +
346
+ ' pugi chain list',
347
+ }, `pugi chain: ${error}\n` +
348
+ 'Usage:\n' +
349
+ ' pugi chain new "<intent>"\n' +
350
+ ' pugi chain status [<chain-id>]\n' +
351
+ ' pugi chain next [<chain-id>]\n' +
352
+ ' pugi chain show <step> [<chain-id>]\n' +
353
+ ' pugi chain export [<chain-id>] [--json]\n' +
354
+ ' pugi chain list');
355
+ process.exitCode = 2;
356
+ }
357
+ async function resolveChainId(explicit, ctx, subcommand) {
358
+ if (explicit && explicit.length > 0 && !explicit.startsWith('--')) {
359
+ return explicit;
360
+ }
361
+ const ids = listChainIds(ctx.cwd);
362
+ if (ids.length === 0) {
363
+ ctx.writeOutput({
364
+ ok: false,
365
+ command: 'chain',
366
+ subcommand,
367
+ error: 'no chains in this workspace',
368
+ hint: 'Run `pugi chain new "<intent>"` first.',
369
+ }, `pugi chain ${subcommand}: no chains in this workspace. Run \`pugi chain new "<intent>"\` first.`);
370
+ process.exitCode = 1;
371
+ return null;
372
+ }
373
+ // Pick the most-recently-modified chain (last write wins). The
374
+ // `listChainIds` sort is alphabetic, so we re-stat each candidate
375
+ // and pick the freshest. Cheap — operator workspaces hold tens of
376
+ // chains at the absolute outside.
377
+ let best = ids[0];
378
+ let bestMtime = 0;
379
+ for (const id of ids) {
380
+ const statePath = join(chainDir(ctx.cwd, id), 'chain.json');
381
+ try {
382
+ const mtime = statSync(statePath).mtimeMs;
383
+ if (mtime > bestMtime) {
384
+ best = id;
385
+ bestMtime = mtime;
386
+ }
387
+ }
388
+ catch {
389
+ // skip directories without a chain.json
390
+ }
391
+ }
392
+ return best;
393
+ }
394
+ /**
395
+ * Build the live dispatcher that binds to the existing delegate SDK
396
+ * surface. The dispatcher opens a fresh session per step (matching
397
+ * `pugi delegate` semantics), POSTs the brief, and waits for the SSE
398
+ * terminal event. The waiter aggregates every matching
399
+ * `agent.message` frame for the persona's taskId and returns the
400
+ * concatenated text as the `transcript` field on the `completed`
401
+ * outcome; the dispatcher writes that transcript verbatim as the
402
+ * artifact, fulfilling the artifact-chain module contract that the
403
+ * persona's response lands on disk.
404
+ *
405
+ * When credentials are missing the dispatcher returns a `failed`
406
+ * outcome on every call so the chain surfaces a clean error instead
407
+ * of crashing. The CLI handler caller surfaces this to the operator.
408
+ *
409
+ * Gemini P1 fix (PR #608, 2026-05-27): the previous implementation
410
+ * wrote a comment-only stub pointing the operator at the SSE log
411
+ * because the waiter did not surface persona text. Pairing the new
412
+ * `transcript` field on `DelegateTerminalOutcome` with this caller
413
+ * closes the loop end-to-end.
414
+ *
415
+ * Test surface: `ctx.waitForTerminal` is injected so the spec can
416
+ * exercise the success / failure / empty-transcript / blocked paths
417
+ * without standing up a real fetch + EventSource pipeline. The default
418
+ * is the live `waitForDelegateTerminal` impl.
419
+ */
420
+ export function buildLiveDispatcher(ctx) {
421
+ return async ({ chainId, step, brief }) => {
422
+ const config = ctx.resolveConfig ? ctx.resolveConfig() : null;
423
+ if (!config) {
424
+ return {
425
+ kind: 'failed',
426
+ dispatchId: null,
427
+ error: 'no Pugi credential configured; run `pugi login` first. Set ctx.resolveConfig OR `pugi login`.',
428
+ };
429
+ }
430
+ if (!ctx.openSession) {
431
+ return {
432
+ kind: 'failed',
433
+ dispatchId: null,
434
+ error: 'session opener not wired; this code path requires a runtime context',
435
+ };
436
+ }
437
+ const opened = await ctx.openSession(config, ctx.cwd);
438
+ if ('error' in opened) {
439
+ return {
440
+ kind: 'failed',
441
+ dispatchId: null,
442
+ error: `session open failed: ${opened.error}`,
443
+ };
444
+ }
445
+ const submit = ctx.submitDelegate ?? submitDelegate;
446
+ const result = await submit(config, opened.sessionId, {
447
+ persona: step.persona,
448
+ brief,
449
+ });
450
+ if (result.status !== 'ok') {
451
+ return {
452
+ kind: 'failed',
453
+ dispatchId: null,
454
+ error: `delegate failed: ${result.status}${'message' in result ? ` — ${result.message}` : ''}`,
455
+ };
456
+ }
457
+ const waiter = ctx.waitForTerminal ?? waitForDelegateTerminal;
458
+ const outcome = await waiter(config, opened.sessionId, result.response.personaSlug);
459
+ if (outcome.kind === 'completed') {
460
+ // Persona text captured verbatim from `agent.message` frames on
461
+ // the SSE stream. The `transcript` field is empty when the
462
+ // persona reached `agent.completed` without emitting any
463
+ // assistant text (e.g. a tool-only turn) — we still surface a
464
+ // structured artifact so `pugi chain show` does not 404 and the
465
+ // operator has dispatch provenance even on a silent completion.
466
+ const artifact = outcome.transcript.length > 0
467
+ ? outcome.transcript
468
+ : `<!-- chain ${chainId} step ${step.id} completed with no assistant text -->\n` +
469
+ `<!-- dispatchId: ${result.response.dispatchId}, session: ${opened.sessionId} -->\n` +
470
+ `<!-- persona '${step.personaLabel}' reached terminal without emitting agent.message frames -->\n`;
471
+ return {
472
+ kind: 'completed',
473
+ dispatchId: result.response.dispatchId,
474
+ artifact,
475
+ };
476
+ }
477
+ const error = outcome.kind === 'blocked'
478
+ ? outcome.detail
479
+ : outcome.kind === 'failed'
480
+ ? outcome.error
481
+ : outcome.error;
482
+ return {
483
+ kind: 'failed',
484
+ dispatchId: result.response.dispatchId,
485
+ error,
486
+ };
487
+ };
488
+ }
489
+ //# sourceMappingURL=chain.js.map