@pugi/cli 0.1.0-beta.1 → 0.1.0-beta.10
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 -40
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/prompts.js +8 -0
- package/dist/core/lsp/client.js +719 -0
- package/dist/core/repl/session.js +26 -1
- package/dist/core/repl/slash-commands.js +33 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/runtime/cli.js +282 -7
- package/dist/runtime/commands/delegate.js +289 -0
- package/dist/runtime/commands/lsp.js +206 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/roster.js +117 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/tools/apply-patch.js +495 -0
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tui/repl-render.js +169 -10
- package/dist/tui/repl.js +15 -2
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +6 -4
package/dist/runtime/cli.js
CHANGED
|
@@ -17,8 +17,10 @@ import { toolRegistry, toolSchemaBundleHashInput } from '../tools/registry.js';
|
|
|
17
17
|
import { webFetchTool } from '../tools/web-fetch.js';
|
|
18
18
|
import { emptyIndex, rebuildIndex, readIndex, upsertArtifact, writeIndex, } from '../core/index-store.js';
|
|
19
19
|
import { signatureForPlanReview } from '../core/repl/ask.js';
|
|
20
|
-
import { buildRuntimeConfig, loadRuntimeConfig, pollDeviceFlow, pugiHandoffBundleSchema, pugiSyncDryRunPlanSchema, pugiSyncPrivacyModeSchema, pugiSyncRequestSchema, pugiSyncUploadPlanSchema, pugiTripleReviewRequestSchema, startDeviceFlow, submitSync, submitTripleReview, } from '@pugi/sdk';
|
|
20
|
+
import { buildRuntimeConfig, fetchPersonaRoster, loadRuntimeConfig, openPugiSession, pollDeviceFlow, pugiHandoffBundleSchema, pugiSyncDryRunPlanSchema, pugiSyncPrivacyModeSchema, pugiSyncRequestSchema, pugiSyncUploadPlanSchema, pugiTripleReviewRequestSchema, startDeviceFlow, submitDelegate, submitSync, submitTripleReview, } from '@pugi/sdk';
|
|
21
21
|
import { PUGI_TAGLINE } from '@pugi/personas';
|
|
22
|
+
import { resolveRoster, renderRosterTable } from './commands/roster.js';
|
|
23
|
+
import { runDelegateCommand } from './commands/delegate.js';
|
|
22
24
|
import { clearApiKey, DEFAULT_API_URL, listStoredCredentials, maskApiKey, normalizeApiUrl, purgeAllCredentials, readCredentialsFile, resolveActiveCredential, storeApiKey, switchActiveAccount, } from '../core/credentials.js';
|
|
23
25
|
import { runDeployCommand } from '../commands/deploy.js';
|
|
24
26
|
import { runJobsCommand } from '../commands/jobs.js';
|
|
@@ -27,9 +29,14 @@ import { runPrivacyCommand } from './commands/privacy.js';
|
|
|
27
29
|
import { runUndoCommand } from './commands/undo.js';
|
|
28
30
|
import { runBudgetCommand } from './commands/budget.js';
|
|
29
31
|
import { runSkillsCommand } from './commands/skills.js';
|
|
32
|
+
import { installDefaultSkills } from '../core/skills/defaults.js';
|
|
30
33
|
import { runAgentsCommand } from './commands/agents.js';
|
|
34
|
+
import { runLspCommand } from './commands/lsp.js';
|
|
35
|
+
import { runPatchCommand } from './commands/patch.js';
|
|
36
|
+
import { runWorktreeCommand } from './commands/worktree.js';
|
|
31
37
|
import { resolveWorkspaceLabel } from '../core/repl/workspace-context.js';
|
|
32
38
|
import { runReviewConsensus } from './commands/review-consensus.js';
|
|
39
|
+
import { DECOMPOSE_PROMPT_SUFFIX, parseDecompositionFromText, writeDecomposition, } from './plan-decompose.js';
|
|
33
40
|
import { FtsSyntaxError, SqliteSessionStore, resolveProjectStoreDir } from '../core/repl/store/index.js';
|
|
34
41
|
import { slugForCwd } from '../core/repl/history.js';
|
|
35
42
|
import { dispatchEdit, } from '../core/edits/index.js';
|
|
@@ -44,7 +51,7 @@ import { dispatchEdit, } from '../core/edits/index.js';
|
|
|
44
51
|
* packages/pugi-sdk/package.json); the publish workflow validates the
|
|
45
52
|
* three are in lockstep.
|
|
46
53
|
*/
|
|
47
|
-
const PUGI_CLI_VERSION = "0.1.0-beta.
|
|
54
|
+
const PUGI_CLI_VERSION = "0.1.0-beta.10";
|
|
48
55
|
const handlers = {
|
|
49
56
|
accounts,
|
|
50
57
|
agents: dispatchAgents,
|
|
@@ -53,6 +60,7 @@ const handlers = {
|
|
|
53
60
|
budget: dispatchBudget,
|
|
54
61
|
code: runEngineTask('code'),
|
|
55
62
|
config: dispatchConfig,
|
|
63
|
+
delegate: dispatchDelegate,
|
|
56
64
|
deploy: dispatchDeploy,
|
|
57
65
|
doctor,
|
|
58
66
|
explain: runEngineTask('explain'),
|
|
@@ -64,11 +72,14 @@ const handlers = {
|
|
|
64
72
|
jobs,
|
|
65
73
|
login,
|
|
66
74
|
logout,
|
|
75
|
+
lsp: dispatchLsp,
|
|
76
|
+
patch: dispatchPatch,
|
|
67
77
|
plan: runEngineTask('plan'),
|
|
68
78
|
'plan-review': dispatchPlanReview,
|
|
69
79
|
privacy: dispatchPrivacy,
|
|
70
80
|
review,
|
|
71
81
|
resume,
|
|
82
|
+
roster: dispatchRoster,
|
|
72
83
|
sessions,
|
|
73
84
|
skills: dispatchSkills,
|
|
74
85
|
sync,
|
|
@@ -76,6 +87,7 @@ const handlers = {
|
|
|
76
87
|
version,
|
|
77
88
|
web: dispatchWeb,
|
|
78
89
|
whoami,
|
|
90
|
+
worktree: dispatchWorktree,
|
|
79
91
|
};
|
|
80
92
|
/**
|
|
81
93
|
* α6.3 `pugi ask "<question>"` — surface the office-hours forcing-question
|
|
@@ -246,6 +258,59 @@ async function dispatchPrivacy(args, flags, _session) {
|
|
|
246
258
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
247
259
|
});
|
|
248
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* `pugi roster` - α7.5 Phase 1.
|
|
263
|
+
*
|
|
264
|
+
* List the live Tier 1 personas with display name, role, and routing
|
|
265
|
+
* tag. Walks the remote /api/pugi/sessions/roster endpoint when a
|
|
266
|
+
* credential is available; falls back to the local @pugi/personas
|
|
267
|
+
* roster when offline so the operator can still see who is on the team.
|
|
268
|
+
*/
|
|
269
|
+
async function dispatchRoster(_args, flags, _session) {
|
|
270
|
+
const credential = resolveActiveCredential();
|
|
271
|
+
const config = credential
|
|
272
|
+
? buildRuntimeConfig({ apiUrl: credential.apiUrl, apiKey: credential.apiKey })
|
|
273
|
+
: null;
|
|
274
|
+
const { rows, warning } = await resolveRoster(config);
|
|
275
|
+
const payload = {
|
|
276
|
+
ok: true,
|
|
277
|
+
personas: rows,
|
|
278
|
+
warning,
|
|
279
|
+
};
|
|
280
|
+
const text = (warning ? `# warning: ${warning}\n\n` : '') +
|
|
281
|
+
renderRosterTable(rows);
|
|
282
|
+
writeOutput(flags, payload, text);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* `pugi delegate <slug> "<brief>"` - α7.5 Phase 1.
|
|
286
|
+
*
|
|
287
|
+
* Open a fresh REPL session and POST the brief to one Tier 1 persona,
|
|
288
|
+
* bypassing Mira's coordinator pass. Non-interactive: the CLI prints
|
|
289
|
+
* the dispatch id on success and exits; the operator (or a script) can
|
|
290
|
+
* subscribe to the session stream separately if they want the live
|
|
291
|
+
* lifecycle. Interactive operators use `/delegate` from inside the REPL
|
|
292
|
+
* instead so the dispatch lifecycle surfaces inline.
|
|
293
|
+
*/
|
|
294
|
+
async function dispatchDelegate(args, flags, _session) {
|
|
295
|
+
await runDelegateCommand(args, {
|
|
296
|
+
workspaceCwd: process.cwd(),
|
|
297
|
+
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
298
|
+
resolveConfig: () => {
|
|
299
|
+
const credential = resolveActiveCredential();
|
|
300
|
+
if (!credential)
|
|
301
|
+
return null;
|
|
302
|
+
return buildRuntimeConfig({ apiUrl: credential.apiUrl, apiKey: credential.apiKey });
|
|
303
|
+
},
|
|
304
|
+
fetchRoster: fetchPersonaRoster,
|
|
305
|
+
submitDelegate,
|
|
306
|
+
openSession: async (config, workspaceCwd) => {
|
|
307
|
+
const result = await openPugiSession(config, { workspaceCwd });
|
|
308
|
+
if (result.status === 'ok')
|
|
309
|
+
return { sessionId: result.response.sessionId };
|
|
310
|
+
return { error: `${result.status}: ${result.message}` };
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
249
314
|
async function dispatchUndo(args, flags, session) {
|
|
250
315
|
await runUndoCommand(args, {
|
|
251
316
|
workspaceRoot: process.cwd(),
|
|
@@ -314,6 +379,59 @@ async function dispatchWeb(args, flags, _session) {
|
|
|
314
379
|
}
|
|
315
380
|
writeOutput(flags, result, `# ${result.title}\n# ${result.url}\n# fetched ${result.fetched_at}\n\n${result.content_md}`);
|
|
316
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* α7.7: `pugi lsp <op> <file> [args]` — direct LSP queries. Delegated
|
|
384
|
+
* to the standalone runner in `./commands/lsp.ts` so the giant cli.ts
|
|
385
|
+
* dispatch table stays narrow. The runner spawns + tears down the LSP
|
|
386
|
+
* server per invocation (no daemon yet — that ships in α7.7b).
|
|
387
|
+
*/
|
|
388
|
+
async function dispatchLsp(args, flags, _session) {
|
|
389
|
+
const result = await runLspCommand(args, { cwd: process.cwd(), json: flags.json });
|
|
390
|
+
console.log(result.text);
|
|
391
|
+
if (result.exitCode !== 0)
|
|
392
|
+
process.exitCode = result.exitCode;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* α7.7: `pugi patch` — apply a unified-diff patch from stdin or a file.
|
|
396
|
+
* Routes through the same security gate as the Layer A/B/C applicators
|
|
397
|
+
* (see `src/core/edits/security-gate.ts`). Exit codes mirror the
|
|
398
|
+
* security taxonomy so CI loops can alert on hostile patches without
|
|
399
|
+
* confusing them with operator typos.
|
|
400
|
+
*
|
|
401
|
+
* R1 fix (2026-05-26, PR #413 r1): pass `flags.dryRun` through so the
|
|
402
|
+
* top-level parser's consumption of `--dry-run` does not silently
|
|
403
|
+
* disable dry-run mode on `pugi patch --dry-run < diff.patch`.
|
|
404
|
+
*/
|
|
405
|
+
async function dispatchPatch(args, flags, _session) {
|
|
406
|
+
const result = await runPatchCommand(args, {
|
|
407
|
+
cwd: process.cwd(),
|
|
408
|
+
json: flags.json,
|
|
409
|
+
dryRun: flags.dryRun,
|
|
410
|
+
});
|
|
411
|
+
console.log(result.text);
|
|
412
|
+
if (result.exitCode !== 0)
|
|
413
|
+
process.exitCode = result.exitCode;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* α7.7: `pugi worktree <op>` — manual scratch worktree management.
|
|
417
|
+
* The `pugi build` and `pugi review --consensus` paths use the same
|
|
418
|
+
* primitives internally (`createWorktree` / `promoteWorktree`); this
|
|
419
|
+
* surface is the operator escape hatch for debug + experiment flows.
|
|
420
|
+
*
|
|
421
|
+
* R1 fix (2026-05-26, PR #413 r1): forward `flags.dryRun` so the
|
|
422
|
+
* top-level parser's consumption of `--dry-run` does not silently
|
|
423
|
+
* disable dry-run mode on `pugi worktree promote --dry-run <path>`.
|
|
424
|
+
*/
|
|
425
|
+
async function dispatchWorktree(args, flags, _session) {
|
|
426
|
+
const result = await runWorktreeCommand(args, {
|
|
427
|
+
cwd: process.cwd(),
|
|
428
|
+
json: flags.json,
|
|
429
|
+
dryRun: flags.dryRun,
|
|
430
|
+
});
|
|
431
|
+
console.log(result.text);
|
|
432
|
+
if (result.exitCode !== 0)
|
|
433
|
+
process.exitCode = result.exitCode;
|
|
434
|
+
}
|
|
317
435
|
export async function runCli(argv) {
|
|
318
436
|
const { command, args, flags, isBareInvocation } = parseArgs(argv);
|
|
319
437
|
// Bare `pugi` on a TTY enters the REPL-by-default agentic session
|
|
@@ -399,11 +517,13 @@ function parseArgs(argv) {
|
|
|
399
517
|
// "Mira: ..." prose, never "Read(path)" prefix) OR fires falsely on
|
|
400
518
|
// accidental `Verb(noun)` shapes producing stuck `running` rows.
|
|
401
519
|
// Hide by default; opt-in via `--tool-stream` OR PUGI_TOOL_STREAM=1
|
|
402
|
-
// for development/testing. Will flip
|
|
520
|
+
// for development/testing. Will flip to default ON when backend
|
|
403
521
|
// emits real tool events (filed as α6.13.X follow-up).
|
|
404
522
|
noToolStream: process.env.PUGI_TOOL_STREAM === '1' || process.env.PUGI_HIDE_TOOL_STREAM === '1'
|
|
405
523
|
? process.env.PUGI_HIDE_TOOL_STREAM === '1'
|
|
406
524
|
: true,
|
|
525
|
+
noDefaults: process.env.PUGI_INIT_NO_DEFAULTS === '1',
|
|
526
|
+
decompose: false,
|
|
407
527
|
};
|
|
408
528
|
const args = [];
|
|
409
529
|
// Sprint 2E: `pugi --version` / `-v` are universal install-test conventions
|
|
@@ -436,7 +556,7 @@ function parseArgs(argv) {
|
|
|
436
556
|
else if (arg === '--consensus') {
|
|
437
557
|
// α6.7: customer-facing 3-model consensus review. Routes through
|
|
438
558
|
// the SSE-based runtime gate rather than the legacy artifact
|
|
439
|
-
// writer. The triple flag stays unset
|
|
559
|
+
// writer. The triple flag stays unset so the existing
|
|
440
560
|
// performRemoteTripleReview path is never accidentally entered.
|
|
441
561
|
flags.consensus = true;
|
|
442
562
|
}
|
|
@@ -459,10 +579,21 @@ function parseArgs(argv) {
|
|
|
459
579
|
flags.noToolStream = true;
|
|
460
580
|
}
|
|
461
581
|
else if (arg === '--tool-stream') {
|
|
462
|
-
// Opt-in
|
|
463
|
-
// pane shows
|
|
582
|
+
// Opt-in for α6.12 dev/testing — backend tool events not live yet,
|
|
583
|
+
// pane shows synthesized heuristic OR empty placeholder
|
|
464
584
|
flags.noToolStream = false;
|
|
465
585
|
}
|
|
586
|
+
else if (arg === '--no-defaults') {
|
|
587
|
+
// Init-only flag: skip the bundled default-skills install. Parsed
|
|
588
|
+
// at the global level for consistency with --no-splash / --no-tool-stream.
|
|
589
|
+
flags.noDefaults = true;
|
|
590
|
+
}
|
|
591
|
+
else if (arg === '--decompose') {
|
|
592
|
+
// α6.8 EXTEND PR1: plan-only flag. Other engine commands ignore
|
|
593
|
+
// it. Parsed globally for symmetry with the rest of the flag
|
|
594
|
+
// grammar; `runEngineTask('plan')` is the single consumer.
|
|
595
|
+
flags.decompose = true;
|
|
596
|
+
}
|
|
466
597
|
else if (arg.startsWith('--privacy=')) {
|
|
467
598
|
flags.privacy = parsePrivacyMode(arg.slice('--privacy='.length));
|
|
468
599
|
}
|
|
@@ -478,8 +609,20 @@ function parseArgs(argv) {
|
|
|
478
609
|
}
|
|
479
610
|
}
|
|
480
611
|
const isBareInvocation = args.length === 0;
|
|
612
|
+
const command = args.shift() ?? 'help';
|
|
613
|
+
// Sprint α6.X CEO dogfood 2026-05-26 (P0 hot-fix): trailing `--help`
|
|
614
|
+
// / `-h` on ANY sub-command must route to the help printer rather
|
|
615
|
+
// than dispatching the real engine. Before this guard `pugi build
|
|
616
|
+
// --help` burned 86k tokens running the actual build loop because
|
|
617
|
+
// the dispatcher saw `--help` as an opaque arg and forwarded it
|
|
618
|
+
// through to the engine. Re-routing here means `pugi <cmd> --help`
|
|
619
|
+
// becomes `pugi help <cmd>` deterministically across the entire
|
|
620
|
+
// command tree.
|
|
621
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
622
|
+
return { command: 'help', args: [command], flags, isBareInvocation: false };
|
|
623
|
+
}
|
|
481
624
|
return {
|
|
482
|
-
command
|
|
625
|
+
command,
|
|
483
626
|
args,
|
|
484
627
|
flags,
|
|
485
628
|
isBareInvocation,
|
|
@@ -527,6 +670,15 @@ async function help(_args, flags, _session) {
|
|
|
527
670
|
' pugi ask "<question>" Surface a yes/no question modal locally.',
|
|
528
671
|
' pugi plan-review <task> Generate + present a plan-review modal.',
|
|
529
672
|
'',
|
|
673
|
+
'Persona dispatch (α7.5):',
|
|
674
|
+
' pugi roster List the live Tier 1 personas + roles.',
|
|
675
|
+
' pugi delegate <slug> "<brief>" Dispatch a brief to one specialist.',
|
|
676
|
+
'',
|
|
677
|
+
'Plan decomposition (α6.8):',
|
|
678
|
+
' pugi plan --decompose <idea> Split a high-level idea into 3-7 components.',
|
|
679
|
+
' Writes .pugi/plan/<session-id>/splits/NN-<name>/spec.md',
|
|
680
|
+
' plus manifest.md with the dependency DAG.',
|
|
681
|
+
'',
|
|
530
682
|
'Deploy:',
|
|
531
683
|
' pugi deploy --target vercel <vercelProject> --project <id>',
|
|
532
684
|
' Trigger a Vercel deployment from the bound Git source.',
|
|
@@ -549,6 +701,8 @@ async function help(_args, flags, _session) {
|
|
|
549
701
|
' PUGI_SKIP_SPLASH=1.',
|
|
550
702
|
' --no-tool-stream Hide the live tool stream pane (α6.12).',
|
|
551
703
|
' Pairs with PUGI_HIDE_TOOL_STREAM=1.',
|
|
704
|
+
' --no-defaults Skip bundled default-skills install on',
|
|
705
|
+
' `pugi init`. Pairs with PUGI_INIT_NO_DEFAULTS=1.',
|
|
552
706
|
'',
|
|
553
707
|
PUGI_TAGLINE,
|
|
554
708
|
'Execution defaults to local. Use --remote or --web to create a handoff bundle.',
|
|
@@ -618,6 +772,7 @@ async function init(_args, flags, _session) {
|
|
|
618
772
|
ensureDir(pugiDir, created, skipped);
|
|
619
773
|
ensureDir(resolve(pugiDir, 'artifacts'), created, skipped);
|
|
620
774
|
ensureDir(resolve(pugiDir, 'sessions'), created, skipped);
|
|
775
|
+
ensureDir(resolve(pugiDir, 'skills'), created, skipped);
|
|
621
776
|
writeJsonIfMissing(resolve(pugiDir, 'settings.json'), {
|
|
622
777
|
schema: 1,
|
|
623
778
|
workflow: {
|
|
@@ -687,17 +842,50 @@ async function init(_args, flags, _session) {
|
|
|
687
842
|
// Ensure `.pugi/` is git-ignored so users do not accidentally commit
|
|
688
843
|
// local audit logs, artifacts, or triple-review request payloads.
|
|
689
844
|
ensurePugiGitIgnore(cwd, created, skipped);
|
|
845
|
+
// Bundled default skills (brand-voice, endpoint-probe, readme-sync).
|
|
846
|
+
// Skipped when --no-defaults is passed OR when PUGI_INIT_NO_DEFAULTS=1.
|
|
847
|
+
// Idempotent: a skill whose target directory already exists is left
|
|
848
|
+
// alone so re-running `pugi init` after the operator customised one of
|
|
849
|
+
// the defaults does not clobber their edits.
|
|
850
|
+
let defaultSkills = [];
|
|
851
|
+
if (!flags.noDefaults) {
|
|
852
|
+
try {
|
|
853
|
+
defaultSkills = await installDefaultSkills({
|
|
854
|
+
workspaceRoot: cwd,
|
|
855
|
+
log: (line) => process.stderr.write(line),
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
// Default-skills install is a convenience layer. A failure here
|
|
860
|
+
// (bad sha256 hashing, permission error on .pugi/skills/) must not
|
|
861
|
+
// leave `pugi init` in a half-state where settings.json exists but
|
|
862
|
+
// the operator sees an unexplained crash. Log the error to stderr
|
|
863
|
+
// and continue — the operator can still install skills manually.
|
|
864
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
865
|
+
process.stderr.write(`[pugi init] default-skills install failed: ${message}\n`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
690
868
|
const payload = {
|
|
691
869
|
status: 'initialized',
|
|
692
870
|
root: cwd,
|
|
693
871
|
created,
|
|
694
872
|
skipped,
|
|
873
|
+
defaultSkills,
|
|
695
874
|
};
|
|
875
|
+
const defaultSkillLines = flags.noDefaults
|
|
876
|
+
? ['Default skills: skipped (--no-defaults)']
|
|
877
|
+
: defaultSkills.length === 0
|
|
878
|
+
? ['Default skills: none installed']
|
|
879
|
+
: [
|
|
880
|
+
'Default skills:',
|
|
881
|
+
...defaultSkills.map((entry) => ` ${entry.name.padEnd(18)} ${entry.status}`),
|
|
882
|
+
];
|
|
696
883
|
writeOutput(flags, payload, [
|
|
697
884
|
'Pugi initialized',
|
|
698
885
|
`Root: ${cwd}`,
|
|
699
886
|
created.length ? `Created:\n${created.map((path) => ` ${path}`).join('\n')}` : 'Created: none',
|
|
700
887
|
skipped.length ? `Already present:\n${skipped.map((path) => ` ${path}`).join('\n')}` : 'Already present: none',
|
|
888
|
+
...defaultSkillLines,
|
|
701
889
|
].join('\n'));
|
|
702
890
|
}
|
|
703
891
|
async function idea(args, flags, session) {
|
|
@@ -2042,6 +2230,26 @@ function runEngineTask(kind) {
|
|
|
2042
2230
|
const config = credential
|
|
2043
2231
|
? buildRuntimeConfig({ apiUrl: credential.apiUrl, apiKey: credential.apiKey })
|
|
2044
2232
|
: envConfig;
|
|
2233
|
+
// α6.8 EXTEND PR1 v2: `--decompose` gating runs BEFORE the offline
|
|
2234
|
+
// fallback. Two reasons:
|
|
2235
|
+
// 1. The flag is plan-only — surfacing the rejection for
|
|
2236
|
+
// `pugi build --decompose` before we drop into `offlineBuild`
|
|
2237
|
+
// means the operator gets a deterministic error instead of a
|
|
2238
|
+
// silent no-op stub.
|
|
2239
|
+
// 2. The decompose post-processor depends on the engine's final
|
|
2240
|
+
// text. The offline plan stub does not invoke the engine, so
|
|
2241
|
+
// `pugi plan --decompose --offline` would silently skip the
|
|
2242
|
+
// decomposition step. Refusing the combination up front is the
|
|
2243
|
+
// cheapest way to keep the contract honest.
|
|
2244
|
+
if (flags.decompose && kind !== 'plan') {
|
|
2245
|
+
throw new Error(`--decompose is only valid for \`pugi plan\` (got \`pugi ${label}\`)`);
|
|
2246
|
+
}
|
|
2247
|
+
if (flags.decompose && flags.offline) {
|
|
2248
|
+
throw new Error('--decompose requires the engine — drop --offline (decomposition needs the model to emit a fenced JSON block)');
|
|
2249
|
+
}
|
|
2250
|
+
if (flags.decompose && !config) {
|
|
2251
|
+
throw new Error('--decompose requires the engine — run `pugi login` or set PUGI_API_KEY (decomposition needs the model to emit a fenced JSON block)');
|
|
2252
|
+
}
|
|
2045
2253
|
// Offline fallback: preserves the local-first invariant. `plan` /
|
|
2046
2254
|
// `build` / `explain` drop back to their pre-Sprint-2 stub
|
|
2047
2255
|
// behaviour so an operator without an API key (or with --offline)
|
|
@@ -2094,6 +2302,17 @@ function runEngineTask(kind) {
|
|
|
2094
2302
|
throw new Error(`pugi ${label} requires a prompt`);
|
|
2095
2303
|
}
|
|
2096
2304
|
}
|
|
2305
|
+
// α6.8 EXTEND PR1: when `--decompose` is set, augment the user
|
|
2306
|
+
// prompt with the decomposition-request suffix BEFORE the adapter
|
|
2307
|
+
// run. The system prompt for `plan` already constrains the model
|
|
2308
|
+
// to read-only tools + a plan deliverable; the suffix layers the
|
|
2309
|
+
// JSON-emission contract on top so the post-run parser can lift
|
|
2310
|
+
// the structured payload out of the final answer. The plan-only /
|
|
2311
|
+
// engine-required gates fired before the offline fallback above,
|
|
2312
|
+
// so by here we know we are on the engine path with a plan task.
|
|
2313
|
+
if (flags.decompose && kind === 'plan') {
|
|
2314
|
+
prompt = `${prompt}\n${DECOMPOSE_PROMPT_SUFFIX}`;
|
|
2315
|
+
}
|
|
2097
2316
|
// Narrow `config` for the type checker — the offline branches above
|
|
2098
2317
|
// return whenever `config` is null, so by this point it must be set.
|
|
2099
2318
|
if (!config) {
|
|
@@ -2203,6 +2422,41 @@ function runEngineTask(kind) {
|
|
|
2203
2422
|
statusEvents,
|
|
2204
2423
|
});
|
|
2205
2424
|
}
|
|
2425
|
+
// α6.8 EXTEND PR1: `--decompose` post-processing. We only attempt
|
|
2426
|
+
// the parse on a `done` plan (a blocked/failed plan is already
|
|
2427
|
+
// captured in plan.md with its reason; no JSON to extract). The
|
|
2428
|
+
// model's final answer arrives via `result.summary` — on success
|
|
2429
|
+
// the adapter prefix is empty so it is the raw final text. We
|
|
2430
|
+
// strip any leading/trailing whitespace then run the parser
|
|
2431
|
+
// against the contents. On parse failure we surface a non-fatal
|
|
2432
|
+
// structured error in the payload — the operator still gets the
|
|
2433
|
+
// plan.md artifact and can re-run.
|
|
2434
|
+
//
|
|
2435
|
+
// TODO(α7.x): `result.summary` is currently a string contract that
|
|
2436
|
+
// doubles as both "human-readable headline" and "raw final model
|
|
2437
|
+
// text". Split into `{ summary, finalText }` on the adapter so the
|
|
2438
|
+
// parser does not have to assume the prefix is empty. Tracked in
|
|
2439
|
+
// PR #423 v2 retro (P2.6, Claude review).
|
|
2440
|
+
let decomposeArtifact = null;
|
|
2441
|
+
let decomposeError = null;
|
|
2442
|
+
if (flags.decompose && kind === 'plan' && result.status === 'done') {
|
|
2443
|
+
const parsed = parseDecompositionFromText(result.summary);
|
|
2444
|
+
if (parsed.ok) {
|
|
2445
|
+
decomposeArtifact = writeDecomposition({
|
|
2446
|
+
root,
|
|
2447
|
+
sessionId: session.id,
|
|
2448
|
+
// Persist the OPERATOR's original prompt, not the prompt+suffix
|
|
2449
|
+
// we sent to the engine. The suffix is plumbing; the manifest
|
|
2450
|
+
// header reads naturally only with the operator text.
|
|
2451
|
+
prompt: args.join(' ').trim() || prompt,
|
|
2452
|
+
decomposition: parsed.decomposition,
|
|
2453
|
+
rationale: parsed.rationale,
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
else {
|
|
2457
|
+
decomposeError = { reason: parsed.reason, detail: parsed.detail };
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2206
2460
|
// Pull the headline metrics out of `eventRefs` so the summary and
|
|
2207
2461
|
// JSON envelope match without re-parsing strings in two places.
|
|
2208
2462
|
const metrics = parseEventRefs(result.eventRefs);
|
|
@@ -2262,6 +2516,20 @@ function runEngineTask(kind) {
|
|
|
2262
2516
|
reason: dr.reason,
|
|
2263
2517
|
detail: dr.detail,
|
|
2264
2518
|
})),
|
|
2519
|
+
// α6.8 EXTEND PR1: decompose artifacts (only present when
|
|
2520
|
+
// `--decompose` was passed AND the model emitted a parseable
|
|
2521
|
+
// JSON block). The `error` shape lands when the model returned
|
|
2522
|
+
// unparseable output; the operator can re-run with a tighter
|
|
2523
|
+
// prompt without losing the plain plan.md artifact.
|
|
2524
|
+
decompose: decomposeArtifact !== null
|
|
2525
|
+
? {
|
|
2526
|
+
manifest: relative(root, decomposeArtifact.manifestPath),
|
|
2527
|
+
planDir: relative(root, decomposeArtifact.planDir),
|
|
2528
|
+
splits: decomposeArtifact.splitPaths,
|
|
2529
|
+
}
|
|
2530
|
+
: decomposeError !== null
|
|
2531
|
+
? { error: decomposeError }
|
|
2532
|
+
: undefined,
|
|
2265
2533
|
// The full event stream is useful for cabinet UI replay. We surface
|
|
2266
2534
|
// it in JSON mode only — text mode operators want the summary, not
|
|
2267
2535
|
// 30 turn-level lines.
|
|
@@ -2271,6 +2539,13 @@ function runEngineTask(kind) {
|
|
|
2271
2539
|
if (kind === 'plan' && planArtifact) {
|
|
2272
2540
|
textLines.push(`Pugi plan written to ${planArtifact.relPath}`);
|
|
2273
2541
|
}
|
|
2542
|
+
if (decomposeArtifact !== null) {
|
|
2543
|
+
textLines.push(`Decomposition: ${decomposeArtifact.splitPaths.length} component spec${decomposeArtifact.splitPaths.length === 1 ? '' : 's'} under ${relative(root, decomposeArtifact.planDir)}`);
|
|
2544
|
+
textLines.push(`Manifest: ${relative(root, decomposeArtifact.manifestPath)}`);
|
|
2545
|
+
}
|
|
2546
|
+
else if (decomposeError !== null) {
|
|
2547
|
+
textLines.push(`Decomposition: skipped (${decomposeError.reason}) — plan.md still written`);
|
|
2548
|
+
}
|
|
2274
2549
|
textLines.push(`Pugi ${label}: ${result.status}`);
|
|
2275
2550
|
textLines.push(`Summary: ${result.summary}`);
|
|
2276
2551
|
if (result.filesChanged.length > 0) {
|