@pugi/cli 0.1.0-beta.7 → 0.1.0-beta.9
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/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 +12 -0
- package/dist/core/repl/slash-commands.js +33 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/runtime/cli.js +257 -33
- package/dist/runtime/commands/delegate.js +219 -11
- package/dist/runtime/commands/lsp.js +206 -0
- package/dist/runtime/commands/patch.js +128 -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 +159 -32
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +18 -15
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,14 +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';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// REPL crashing at module load. When α7.7 lands, restore the real
|
|
35
|
-
// imports + delete the inline stubs.
|
|
34
|
+
import { runLspCommand } from './commands/lsp.js';
|
|
35
|
+
import { runPatchCommand } from './commands/patch.js';
|
|
36
|
+
import { runWorktreeCommand } from './commands/worktree.js';
|
|
36
37
|
import { resolveWorkspaceLabel } from '../core/repl/workspace-context.js';
|
|
37
38
|
import { runReviewConsensus } from './commands/review-consensus.js';
|
|
39
|
+
import { DECOMPOSE_PROMPT_SUFFIX, parseDecompositionFromText, writeDecomposition, } from './plan-decompose.js';
|
|
38
40
|
import { FtsSyntaxError, SqliteSessionStore, resolveProjectStoreDir } from '../core/repl/store/index.js';
|
|
39
41
|
import { slugForCwd } from '../core/repl/history.js';
|
|
40
42
|
import { dispatchEdit, } from '../core/edits/index.js';
|
|
@@ -49,7 +51,7 @@ import { dispatchEdit, } from '../core/edits/index.js';
|
|
|
49
51
|
* packages/pugi-sdk/package.json); the publish workflow validates the
|
|
50
52
|
* three are in lockstep.
|
|
51
53
|
*/
|
|
52
|
-
const PUGI_CLI_VERSION = "0.1.0-beta.
|
|
54
|
+
const PUGI_CLI_VERSION = "0.1.0-beta.9";
|
|
53
55
|
const handlers = {
|
|
54
56
|
accounts,
|
|
55
57
|
agents: dispatchAgents,
|
|
@@ -58,6 +60,7 @@ const handlers = {
|
|
|
58
60
|
budget: dispatchBudget,
|
|
59
61
|
code: runEngineTask('code'),
|
|
60
62
|
config: dispatchConfig,
|
|
63
|
+
delegate: dispatchDelegate,
|
|
61
64
|
deploy: dispatchDeploy,
|
|
62
65
|
doctor,
|
|
63
66
|
explain: runEngineTask('explain'),
|
|
@@ -76,6 +79,7 @@ const handlers = {
|
|
|
76
79
|
privacy: dispatchPrivacy,
|
|
77
80
|
review,
|
|
78
81
|
resume,
|
|
82
|
+
roster: dispatchRoster,
|
|
79
83
|
sessions,
|
|
80
84
|
skills: dispatchSkills,
|
|
81
85
|
sync,
|
|
@@ -254,6 +258,59 @@ async function dispatchPrivacy(args, flags, _session) {
|
|
|
254
258
|
writeOutput: (payload, text) => writeOutput(flags, payload, text),
|
|
255
259
|
});
|
|
256
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
|
+
}
|
|
257
314
|
async function dispatchUndo(args, flags, session) {
|
|
258
315
|
await runUndoCommand(args, {
|
|
259
316
|
workspaceRoot: process.cwd(),
|
|
@@ -328,13 +385,11 @@ async function dispatchWeb(args, flags, _session) {
|
|
|
328
385
|
* dispatch table stays narrow. The runner spawns + tears down the LSP
|
|
329
386
|
* server per invocation (no daemon yet — that ships in α7.7b).
|
|
330
387
|
*/
|
|
331
|
-
async function dispatchLsp(
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
console.log(msg);
|
|
337
|
-
process.exitCode = 6;
|
|
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;
|
|
338
393
|
}
|
|
339
394
|
/**
|
|
340
395
|
* α7.7: `pugi patch` — apply a unified-diff patch from stdin or a file.
|
|
@@ -342,28 +397,40 @@ async function dispatchLsp(_args, flags, _session) {
|
|
|
342
397
|
* (see `src/core/edits/security-gate.ts`). Exit codes mirror the
|
|
343
398
|
* security taxonomy so CI loops can alert on hostile patches without
|
|
344
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`.
|
|
345
404
|
*/
|
|
346
|
-
async function dispatchPatch(
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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;
|
|
353
414
|
}
|
|
354
415
|
/**
|
|
355
416
|
* α7.7: `pugi worktree <op>` — manual scratch worktree management.
|
|
356
417
|
* The `pugi build` and `pugi review --consensus` paths use the same
|
|
357
418
|
* primitives internally (`createWorktree` / `promoteWorktree`); this
|
|
358
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>`.
|
|
359
424
|
*/
|
|
360
|
-
async function dispatchWorktree(
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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;
|
|
367
434
|
}
|
|
368
435
|
export async function runCli(argv) {
|
|
369
436
|
const { command, args, flags, isBareInvocation } = parseArgs(argv);
|
|
@@ -450,11 +517,13 @@ function parseArgs(argv) {
|
|
|
450
517
|
// "Mira: ..." prose, never "Read(path)" prefix) OR fires falsely on
|
|
451
518
|
// accidental `Verb(noun)` shapes producing stuck `running` rows.
|
|
452
519
|
// Hide by default; opt-in via `--tool-stream` OR PUGI_TOOL_STREAM=1
|
|
453
|
-
// for development/testing. Will flip
|
|
520
|
+
// for development/testing. Will flip to default ON when backend
|
|
454
521
|
// emits real tool events (filed as α6.13.X follow-up).
|
|
455
522
|
noToolStream: process.env.PUGI_TOOL_STREAM === '1' || process.env.PUGI_HIDE_TOOL_STREAM === '1'
|
|
456
523
|
? process.env.PUGI_HIDE_TOOL_STREAM === '1'
|
|
457
524
|
: true,
|
|
525
|
+
noDefaults: process.env.PUGI_INIT_NO_DEFAULTS === '1',
|
|
526
|
+
decompose: false,
|
|
458
527
|
};
|
|
459
528
|
const args = [];
|
|
460
529
|
// Sprint 2E: `pugi --version` / `-v` are universal install-test conventions
|
|
@@ -487,7 +556,7 @@ function parseArgs(argv) {
|
|
|
487
556
|
else if (arg === '--consensus') {
|
|
488
557
|
// α6.7: customer-facing 3-model consensus review. Routes through
|
|
489
558
|
// the SSE-based runtime gate rather than the legacy artifact
|
|
490
|
-
// writer. The triple flag stays unset
|
|
559
|
+
// writer. The triple flag stays unset so the existing
|
|
491
560
|
// performRemoteTripleReview path is never accidentally entered.
|
|
492
561
|
flags.consensus = true;
|
|
493
562
|
}
|
|
@@ -510,10 +579,21 @@ function parseArgs(argv) {
|
|
|
510
579
|
flags.noToolStream = true;
|
|
511
580
|
}
|
|
512
581
|
else if (arg === '--tool-stream') {
|
|
513
|
-
// Opt-in
|
|
514
|
-
// pane shows
|
|
582
|
+
// Opt-in for α6.12 dev/testing — backend tool events not live yet,
|
|
583
|
+
// pane shows synthesized heuristic OR empty placeholder
|
|
515
584
|
flags.noToolStream = false;
|
|
516
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
|
+
}
|
|
517
597
|
else if (arg.startsWith('--privacy=')) {
|
|
518
598
|
flags.privacy = parsePrivacyMode(arg.slice('--privacy='.length));
|
|
519
599
|
}
|
|
@@ -529,8 +609,20 @@ function parseArgs(argv) {
|
|
|
529
609
|
}
|
|
530
610
|
}
|
|
531
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
|
+
}
|
|
532
624
|
return {
|
|
533
|
-
command
|
|
625
|
+
command,
|
|
534
626
|
args,
|
|
535
627
|
flags,
|
|
536
628
|
isBareInvocation,
|
|
@@ -578,6 +670,15 @@ async function help(_args, flags, _session) {
|
|
|
578
670
|
' pugi ask "<question>" Surface a yes/no question modal locally.',
|
|
579
671
|
' pugi plan-review <task> Generate + present a plan-review modal.',
|
|
580
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
|
+
'',
|
|
581
682
|
'Deploy:',
|
|
582
683
|
' pugi deploy --target vercel <vercelProject> --project <id>',
|
|
583
684
|
' Trigger a Vercel deployment from the bound Git source.',
|
|
@@ -600,6 +701,8 @@ async function help(_args, flags, _session) {
|
|
|
600
701
|
' PUGI_SKIP_SPLASH=1.',
|
|
601
702
|
' --no-tool-stream Hide the live tool stream pane (α6.12).',
|
|
602
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.',
|
|
603
706
|
'',
|
|
604
707
|
PUGI_TAGLINE,
|
|
605
708
|
'Execution defaults to local. Use --remote or --web to create a handoff bundle.',
|
|
@@ -669,6 +772,7 @@ async function init(_args, flags, _session) {
|
|
|
669
772
|
ensureDir(pugiDir, created, skipped);
|
|
670
773
|
ensureDir(resolve(pugiDir, 'artifacts'), created, skipped);
|
|
671
774
|
ensureDir(resolve(pugiDir, 'sessions'), created, skipped);
|
|
775
|
+
ensureDir(resolve(pugiDir, 'skills'), created, skipped);
|
|
672
776
|
writeJsonIfMissing(resolve(pugiDir, 'settings.json'), {
|
|
673
777
|
schema: 1,
|
|
674
778
|
workflow: {
|
|
@@ -738,17 +842,50 @@ async function init(_args, flags, _session) {
|
|
|
738
842
|
// Ensure `.pugi/` is git-ignored so users do not accidentally commit
|
|
739
843
|
// local audit logs, artifacts, or triple-review request payloads.
|
|
740
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
|
+
}
|
|
741
868
|
const payload = {
|
|
742
869
|
status: 'initialized',
|
|
743
870
|
root: cwd,
|
|
744
871
|
created,
|
|
745
872
|
skipped,
|
|
873
|
+
defaultSkills,
|
|
746
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
|
+
];
|
|
747
883
|
writeOutput(flags, payload, [
|
|
748
884
|
'Pugi initialized',
|
|
749
885
|
`Root: ${cwd}`,
|
|
750
886
|
created.length ? `Created:\n${created.map((path) => ` ${path}`).join('\n')}` : 'Created: none',
|
|
751
887
|
skipped.length ? `Already present:\n${skipped.map((path) => ` ${path}`).join('\n')}` : 'Already present: none',
|
|
888
|
+
...defaultSkillLines,
|
|
752
889
|
].join('\n'));
|
|
753
890
|
}
|
|
754
891
|
async function idea(args, flags, session) {
|
|
@@ -2093,6 +2230,26 @@ function runEngineTask(kind) {
|
|
|
2093
2230
|
const config = credential
|
|
2094
2231
|
? buildRuntimeConfig({ apiUrl: credential.apiUrl, apiKey: credential.apiKey })
|
|
2095
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
|
+
}
|
|
2096
2253
|
// Offline fallback: preserves the local-first invariant. `plan` /
|
|
2097
2254
|
// `build` / `explain` drop back to their pre-Sprint-2 stub
|
|
2098
2255
|
// behaviour so an operator without an API key (or with --offline)
|
|
@@ -2145,6 +2302,17 @@ function runEngineTask(kind) {
|
|
|
2145
2302
|
throw new Error(`pugi ${label} requires a prompt`);
|
|
2146
2303
|
}
|
|
2147
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
|
+
}
|
|
2148
2316
|
// Narrow `config` for the type checker — the offline branches above
|
|
2149
2317
|
// return whenever `config` is null, so by this point it must be set.
|
|
2150
2318
|
if (!config) {
|
|
@@ -2254,6 +2422,41 @@ function runEngineTask(kind) {
|
|
|
2254
2422
|
statusEvents,
|
|
2255
2423
|
});
|
|
2256
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
|
+
}
|
|
2257
2460
|
// Pull the headline metrics out of `eventRefs` so the summary and
|
|
2258
2461
|
// JSON envelope match without re-parsing strings in two places.
|
|
2259
2462
|
const metrics = parseEventRefs(result.eventRefs);
|
|
@@ -2313,6 +2516,20 @@ function runEngineTask(kind) {
|
|
|
2313
2516
|
reason: dr.reason,
|
|
2314
2517
|
detail: dr.detail,
|
|
2315
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,
|
|
2316
2533
|
// The full event stream is useful for cabinet UI replay. We surface
|
|
2317
2534
|
// it in JSON mode only — text mode operators want the summary, not
|
|
2318
2535
|
// 30 turn-level lines.
|
|
@@ -2322,6 +2539,13 @@ function runEngineTask(kind) {
|
|
|
2322
2539
|
if (kind === 'plan' && planArtifact) {
|
|
2323
2540
|
textLines.push(`Pugi plan written to ${planArtifact.relPath}`);
|
|
2324
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
|
+
}
|
|
2325
2549
|
textLines.push(`Pugi ${label}: ${result.status}`);
|
|
2326
2550
|
textLines.push(`Summary: ${result.summary}`);
|
|
2327
2551
|
if (result.filesChanged.length > 0) {
|