@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash-classifier.js +108 -1
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +73 -0
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1899 -38
- package/dist/core/repl/slash-commands.js +406 -21
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3073 -321
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +390 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +156 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +69 -2
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +303 -13
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +72 -14
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +20 -2
- package/dist/tui/vim-input.js +267 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- 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
|