@pugi/cli 0.1.0-beta.8 → 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 +244 -32
- 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
|
}
|
|
@@ -590,6 +670,15 @@ async function help(_args, flags, _session) {
|
|
|
590
670
|
' pugi ask "<question>" Surface a yes/no question modal locally.',
|
|
591
671
|
' pugi plan-review <task> Generate + present a plan-review modal.',
|
|
592
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
|
+
'',
|
|
593
682
|
'Deploy:',
|
|
594
683
|
' pugi deploy --target vercel <vercelProject> --project <id>',
|
|
595
684
|
' Trigger a Vercel deployment from the bound Git source.',
|
|
@@ -612,6 +701,8 @@ async function help(_args, flags, _session) {
|
|
|
612
701
|
' PUGI_SKIP_SPLASH=1.',
|
|
613
702
|
' --no-tool-stream Hide the live tool stream pane (α6.12).',
|
|
614
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.',
|
|
615
706
|
'',
|
|
616
707
|
PUGI_TAGLINE,
|
|
617
708
|
'Execution defaults to local. Use --remote or --web to create a handoff bundle.',
|
|
@@ -681,6 +772,7 @@ async function init(_args, flags, _session) {
|
|
|
681
772
|
ensureDir(pugiDir, created, skipped);
|
|
682
773
|
ensureDir(resolve(pugiDir, 'artifacts'), created, skipped);
|
|
683
774
|
ensureDir(resolve(pugiDir, 'sessions'), created, skipped);
|
|
775
|
+
ensureDir(resolve(pugiDir, 'skills'), created, skipped);
|
|
684
776
|
writeJsonIfMissing(resolve(pugiDir, 'settings.json'), {
|
|
685
777
|
schema: 1,
|
|
686
778
|
workflow: {
|
|
@@ -750,17 +842,50 @@ async function init(_args, flags, _session) {
|
|
|
750
842
|
// Ensure `.pugi/` is git-ignored so users do not accidentally commit
|
|
751
843
|
// local audit logs, artifacts, or triple-review request payloads.
|
|
752
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
|
+
}
|
|
753
868
|
const payload = {
|
|
754
869
|
status: 'initialized',
|
|
755
870
|
root: cwd,
|
|
756
871
|
created,
|
|
757
872
|
skipped,
|
|
873
|
+
defaultSkills,
|
|
758
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
|
+
];
|
|
759
883
|
writeOutput(flags, payload, [
|
|
760
884
|
'Pugi initialized',
|
|
761
885
|
`Root: ${cwd}`,
|
|
762
886
|
created.length ? `Created:\n${created.map((path) => ` ${path}`).join('\n')}` : 'Created: none',
|
|
763
887
|
skipped.length ? `Already present:\n${skipped.map((path) => ` ${path}`).join('\n')}` : 'Already present: none',
|
|
888
|
+
...defaultSkillLines,
|
|
764
889
|
].join('\n'));
|
|
765
890
|
}
|
|
766
891
|
async function idea(args, flags, session) {
|
|
@@ -2105,6 +2230,26 @@ function runEngineTask(kind) {
|
|
|
2105
2230
|
const config = credential
|
|
2106
2231
|
? buildRuntimeConfig({ apiUrl: credential.apiUrl, apiKey: credential.apiKey })
|
|
2107
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
|
+
}
|
|
2108
2253
|
// Offline fallback: preserves the local-first invariant. `plan` /
|
|
2109
2254
|
// `build` / `explain` drop back to their pre-Sprint-2 stub
|
|
2110
2255
|
// behaviour so an operator without an API key (or with --offline)
|
|
@@ -2157,6 +2302,17 @@ function runEngineTask(kind) {
|
|
|
2157
2302
|
throw new Error(`pugi ${label} requires a prompt`);
|
|
2158
2303
|
}
|
|
2159
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
|
+
}
|
|
2160
2316
|
// Narrow `config` for the type checker — the offline branches above
|
|
2161
2317
|
// return whenever `config` is null, so by this point it must be set.
|
|
2162
2318
|
if (!config) {
|
|
@@ -2266,6 +2422,41 @@ function runEngineTask(kind) {
|
|
|
2266
2422
|
statusEvents,
|
|
2267
2423
|
});
|
|
2268
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
|
+
}
|
|
2269
2460
|
// Pull the headline metrics out of `eventRefs` so the summary and
|
|
2270
2461
|
// JSON envelope match without re-parsing strings in two places.
|
|
2271
2462
|
const metrics = parseEventRefs(result.eventRefs);
|
|
@@ -2325,6 +2516,20 @@ function runEngineTask(kind) {
|
|
|
2325
2516
|
reason: dr.reason,
|
|
2326
2517
|
detail: dr.detail,
|
|
2327
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,
|
|
2328
2533
|
// The full event stream is useful for cabinet UI replay. We surface
|
|
2329
2534
|
// it in JSON mode only — text mode operators want the summary, not
|
|
2330
2535
|
// 30 turn-level lines.
|
|
@@ -2334,6 +2539,13 @@ function runEngineTask(kind) {
|
|
|
2334
2539
|
if (kind === 'plan' && planArtifact) {
|
|
2335
2540
|
textLines.push(`Pugi plan written to ${planArtifact.relPath}`);
|
|
2336
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
|
+
}
|
|
2337
2549
|
textLines.push(`Pugi ${label}: ${result.status}`);
|
|
2338
2550
|
textLines.push(`Summary: ${result.summary}`);
|
|
2339
2551
|
if (result.filesChanged.length > 0) {
|