@pugi/cli 0.1.0-beta.1 → 0.1.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -40
  3. package/dist/core/edits/worktree.js +322 -0
  4. package/dist/core/engine/anvil-client.js +16 -0
  5. package/dist/core/engine/budgets.js +89 -0
  6. package/dist/core/engine/native-pugi.js +112 -12
  7. package/dist/core/engine/prompts.js +8 -0
  8. package/dist/core/engine/tool-bridge.js +267 -8
  9. package/dist/core/init/scaffold.js +195 -0
  10. package/dist/core/lsp/client.js +719 -0
  11. package/dist/core/repl/codebase-survey.js +308 -0
  12. package/dist/core/repl/init-interview.js +457 -0
  13. package/dist/core/repl/onboarding-state.js +297 -0
  14. package/dist/core/repl/session.js +72 -1
  15. package/dist/core/repl/slash-commands.js +41 -0
  16. package/dist/core/settings.js +28 -0
  17. package/dist/core/skills/defaults.js +457 -0
  18. package/dist/runtime/cli.js +366 -14
  19. package/dist/runtime/commands/delegate.js +289 -0
  20. package/dist/runtime/commands/lsp.js +206 -0
  21. package/dist/runtime/commands/patch.js +128 -0
  22. package/dist/runtime/commands/roster.js +117 -0
  23. package/dist/runtime/commands/worktree.js +177 -0
  24. package/dist/runtime/plan-decompose.js +531 -0
  25. package/dist/tools/apply-patch.js +495 -0
  26. package/dist/tools/ask-user.js +115 -0
  27. package/dist/tools/lsp-tools.js +189 -0
  28. package/dist/tools/registry.js +26 -0
  29. package/dist/tools/skill-tool.js +96 -0
  30. package/dist/tools/tasks.js +208 -0
  31. package/dist/tui/ask-modal.js +2 -2
  32. package/dist/tui/conversation-pane.js +1 -1
  33. package/dist/tui/input-box.js +1 -1
  34. package/dist/tui/markdown-render.js +4 -4
  35. package/dist/tui/repl-render.js +169 -10
  36. package/dist/tui/repl-splash.js +2 -2
  37. package/dist/tui/repl.js +18 -5
  38. package/dist/tui/splash.js +1 -1
  39. package/dist/tui/update-banner.js +1 -1
  40. package/docs/examples/codegraph.mcp.json +10 -0
  41. package/package.json +6 -4
@@ -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,37 @@ 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.1";
54
+ /**
55
+ * β1 housekeeping (#51): defensive semver sanitizer. If a future
56
+ * refactor moves PUGI_CLI_VERSION reading to a JSON import (resolveJson)
57
+ * the npm publish pipeline can leak `workspace:*` from a partially-bumped
58
+ * package.json — `npm publish` rewrites these but a local `pnpm pack`
59
+ * does not, and the failure mode is silently shipping an unsemver
60
+ * version that breaks `pugi --version` JSON consumers. Sanitize at the
61
+ * read site so even a leaked literal lands as a deterministic
62
+ * "0.0.0-unknown" rather than `workspace:*`.
63
+ */
64
+ function sanitizeSemver(raw) {
65
+ if (typeof raw !== 'string')
66
+ return '0.0.0-unknown';
67
+ const trimmed = raw.trim();
68
+ if (!trimmed)
69
+ return '0.0.0-unknown';
70
+ // Strip a `workspace:` / `npm:` / `file:` protocol prefix that pnpm
71
+ // can emit when a partial publish runs.
72
+ const stripped = trimmed.replace(/^(workspace:|npm:|file:)/, '');
73
+ // Accept anything that begins with major.minor.patch + optional
74
+ // prerelease/build per semver 2.0. Reject `*`, `^x`, `~x`, ranges, etc.
75
+ if (/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(stripped)) {
76
+ return stripped;
77
+ }
78
+ return '0.0.0-unknown';
79
+ }
80
+ // Main bumped to 0.1.0-beta.9 (PR #430 REPL-hang fix). β1a r1 rebase
81
+ // preserves the main bump and runs it through the β1 sanitizer added
82
+ // here so a future workspace:* leak from a partial publish lands as
83
+ // "0.0.0-unknown" instead of corrupting `pugi --version` JSON output.
84
+ const PUGI_CLI_VERSION = sanitizeSemver("0.1.0-beta.11");
48
85
  const handlers = {
49
86
  accounts,
50
87
  agents: dispatchAgents,
@@ -53,6 +90,7 @@ const handlers = {
53
90
  budget: dispatchBudget,
54
91
  code: runEngineTask('code'),
55
92
  config: dispatchConfig,
93
+ delegate: dispatchDelegate,
56
94
  deploy: dispatchDeploy,
57
95
  doctor,
58
96
  explain: runEngineTask('explain'),
@@ -64,11 +102,14 @@ const handlers = {
64
102
  jobs,
65
103
  login,
66
104
  logout,
105
+ lsp: dispatchLsp,
106
+ patch: dispatchPatch,
67
107
  plan: runEngineTask('plan'),
68
108
  'plan-review': dispatchPlanReview,
69
109
  privacy: dispatchPrivacy,
70
110
  review,
71
111
  resume,
112
+ roster: dispatchRoster,
72
113
  sessions,
73
114
  skills: dispatchSkills,
74
115
  sync,
@@ -76,6 +117,7 @@ const handlers = {
76
117
  version,
77
118
  web: dispatchWeb,
78
119
  whoami,
120
+ worktree: dispatchWorktree,
79
121
  };
80
122
  /**
81
123
  * α6.3 `pugi ask "<question>"` — surface the office-hours forcing-question
@@ -246,6 +288,59 @@ async function dispatchPrivacy(args, flags, _session) {
246
288
  writeOutput: (payload, text) => writeOutput(flags, payload, text),
247
289
  });
248
290
  }
291
+ /**
292
+ * `pugi roster` - α7.5 Phase 1.
293
+ *
294
+ * List the live Tier 1 personas with display name, role, and routing
295
+ * tag. Walks the remote /api/pugi/sessions/roster endpoint when a
296
+ * credential is available; falls back to the local @pugi/personas
297
+ * roster when offline so the operator can still see who is on the team.
298
+ */
299
+ async function dispatchRoster(_args, flags, _session) {
300
+ const credential = resolveActiveCredential();
301
+ const config = credential
302
+ ? buildRuntimeConfig({ apiUrl: credential.apiUrl, apiKey: credential.apiKey })
303
+ : null;
304
+ const { rows, warning } = await resolveRoster(config);
305
+ const payload = {
306
+ ok: true,
307
+ personas: rows,
308
+ warning,
309
+ };
310
+ const text = (warning ? `# warning: ${warning}\n\n` : '') +
311
+ renderRosterTable(rows);
312
+ writeOutput(flags, payload, text);
313
+ }
314
+ /**
315
+ * `pugi delegate <slug> "<brief>"` - α7.5 Phase 1.
316
+ *
317
+ * Open a fresh REPL session and POST the brief to one Tier 1 persona,
318
+ * bypassing Mira's coordinator pass. Non-interactive: the CLI prints
319
+ * the dispatch id on success and exits; the operator (or a script) can
320
+ * subscribe to the session stream separately if they want the live
321
+ * lifecycle. Interactive operators use `/delegate` from inside the REPL
322
+ * instead so the dispatch lifecycle surfaces inline.
323
+ */
324
+ async function dispatchDelegate(args, flags, _session) {
325
+ await runDelegateCommand(args, {
326
+ workspaceCwd: process.cwd(),
327
+ writeOutput: (payload, text) => writeOutput(flags, payload, text),
328
+ resolveConfig: () => {
329
+ const credential = resolveActiveCredential();
330
+ if (!credential)
331
+ return null;
332
+ return buildRuntimeConfig({ apiUrl: credential.apiUrl, apiKey: credential.apiKey });
333
+ },
334
+ fetchRoster: fetchPersonaRoster,
335
+ submitDelegate,
336
+ openSession: async (config, workspaceCwd) => {
337
+ const result = await openPugiSession(config, { workspaceCwd });
338
+ if (result.status === 'ok')
339
+ return { sessionId: result.response.sessionId };
340
+ return { error: `${result.status}: ${result.message}` };
341
+ },
342
+ });
343
+ }
249
344
  async function dispatchUndo(args, flags, session) {
250
345
  await runUndoCommand(args, {
251
346
  workspaceRoot: process.cwd(),
@@ -314,6 +409,59 @@ async function dispatchWeb(args, flags, _session) {
314
409
  }
315
410
  writeOutput(flags, result, `# ${result.title}\n# ${result.url}\n# fetched ${result.fetched_at}\n\n${result.content_md}`);
316
411
  }
412
+ /**
413
+ * α7.7: `pugi lsp <op> <file> [args]` — direct LSP queries. Delegated
414
+ * to the standalone runner in `./commands/lsp.ts` so the giant cli.ts
415
+ * dispatch table stays narrow. The runner spawns + tears down the LSP
416
+ * server per invocation (no daemon yet — that ships in α7.7b).
417
+ */
418
+ async function dispatchLsp(args, flags, _session) {
419
+ const result = await runLspCommand(args, { cwd: process.cwd(), json: flags.json });
420
+ console.log(result.text);
421
+ if (result.exitCode !== 0)
422
+ process.exitCode = result.exitCode;
423
+ }
424
+ /**
425
+ * α7.7: `pugi patch` — apply a unified-diff patch from stdin or a file.
426
+ * Routes through the same security gate as the Layer A/B/C applicators
427
+ * (see `src/core/edits/security-gate.ts`). Exit codes mirror the
428
+ * security taxonomy so CI loops can alert on hostile patches without
429
+ * confusing them with operator typos.
430
+ *
431
+ * R1 fix (2026-05-26, PR #413 r1): pass `flags.dryRun` through so the
432
+ * top-level parser's consumption of `--dry-run` does not silently
433
+ * disable dry-run mode on `pugi patch --dry-run < diff.patch`.
434
+ */
435
+ async function dispatchPatch(args, flags, _session) {
436
+ const result = await runPatchCommand(args, {
437
+ cwd: process.cwd(),
438
+ json: flags.json,
439
+ dryRun: flags.dryRun,
440
+ });
441
+ console.log(result.text);
442
+ if (result.exitCode !== 0)
443
+ process.exitCode = result.exitCode;
444
+ }
445
+ /**
446
+ * α7.7: `pugi worktree <op>` — manual scratch worktree management.
447
+ * The `pugi build` and `pugi review --consensus` paths use the same
448
+ * primitives internally (`createWorktree` / `promoteWorktree`); this
449
+ * surface is the operator escape hatch for debug + experiment flows.
450
+ *
451
+ * R1 fix (2026-05-26, PR #413 r1): forward `flags.dryRun` so the
452
+ * top-level parser's consumption of `--dry-run` does not silently
453
+ * disable dry-run mode on `pugi worktree promote --dry-run <path>`.
454
+ */
455
+ async function dispatchWorktree(args, flags, _session) {
456
+ const result = await runWorktreeCommand(args, {
457
+ cwd: process.cwd(),
458
+ json: flags.json,
459
+ dryRun: flags.dryRun,
460
+ });
461
+ console.log(result.text);
462
+ if (result.exitCode !== 0)
463
+ process.exitCode = result.exitCode;
464
+ }
317
465
  export async function runCli(argv) {
318
466
  const { command, args, flags, isBareInvocation } = parseArgs(argv);
319
467
  // Bare `pugi` on a TTY enters the REPL-by-default agentic session
@@ -399,11 +547,13 @@ function parseArgs(argv) {
399
547
  // "Mira: ..." prose, never "Read(path)" prefix) OR fires falsely on
400
548
  // accidental `Verb(noun)` shapes producing stuck `running` rows.
401
549
  // Hide by default; opt-in via `--tool-stream` OR PUGI_TOOL_STREAM=1
402
- // for development/testing. Will flip к default ON when backend
550
+ // for development/testing. Will flip to default ON when backend
403
551
  // emits real tool events (filed as α6.13.X follow-up).
404
552
  noToolStream: process.env.PUGI_TOOL_STREAM === '1' || process.env.PUGI_HIDE_TOOL_STREAM === '1'
405
553
  ? process.env.PUGI_HIDE_TOOL_STREAM === '1'
406
554
  : true,
555
+ noDefaults: process.env.PUGI_INIT_NO_DEFAULTS === '1',
556
+ decompose: false,
407
557
  };
408
558
  const args = [];
409
559
  // Sprint 2E: `pugi --version` / `-v` are universal install-test conventions
@@ -436,7 +586,7 @@ function parseArgs(argv) {
436
586
  else if (arg === '--consensus') {
437
587
  // α6.7: customer-facing 3-model consensus review. Routes through
438
588
  // the SSE-based runtime gate rather than the legacy artifact
439
- // writer. The triple flag stays unset так the existing
589
+ // writer. The triple flag stays unset so the existing
440
590
  // performRemoteTripleReview path is never accidentally entered.
441
591
  flags.consensus = true;
442
592
  }
@@ -459,10 +609,21 @@ function parseArgs(argv) {
459
609
  flags.noToolStream = true;
460
610
  }
461
611
  else if (arg === '--tool-stream') {
462
- // Opt-in для α6.12 dev/testing — backend tool events not live yet,
463
- // pane shows синтесайз heuristic OR empty placeholder
612
+ // Opt-in for α6.12 dev/testing — backend tool events not live yet,
613
+ // pane shows synthesized heuristic OR empty placeholder
464
614
  flags.noToolStream = false;
465
615
  }
616
+ else if (arg === '--no-defaults') {
617
+ // Init-only flag: skip the bundled default-skills install. Parsed
618
+ // at the global level for consistency with --no-splash / --no-tool-stream.
619
+ flags.noDefaults = true;
620
+ }
621
+ else if (arg === '--decompose') {
622
+ // α6.8 EXTEND PR1: plan-only flag. Other engine commands ignore
623
+ // it. Parsed globally for symmetry with the rest of the flag
624
+ // grammar; `runEngineTask('plan')` is the single consumer.
625
+ flags.decompose = true;
626
+ }
466
627
  else if (arg.startsWith('--privacy=')) {
467
628
  flags.privacy = parsePrivacyMode(arg.slice('--privacy='.length));
468
629
  }
@@ -478,13 +639,41 @@ function parseArgs(argv) {
478
639
  }
479
640
  }
480
641
  const isBareInvocation = args.length === 0;
642
+ const command = args.shift() ?? 'help';
643
+ // Sprint α6.X CEO dogfood 2026-05-26 (P0 hot-fix): trailing `--help`
644
+ // / `-h` on ANY sub-command must route to the help printer rather
645
+ // than dispatching the real engine. Before this guard `pugi build
646
+ // --help` burned 86k tokens running the actual build loop because
647
+ // the dispatcher saw `--help` as an opaque arg and forwarded it
648
+ // through to the engine. Re-routing here means `pugi <cmd> --help`
649
+ // becomes `pugi help <cmd>` deterministically across the entire
650
+ // command tree.
651
+ //
652
+ // β1 Tt3 carve-out: commands that ship their OWN `--help` block
653
+ // (login, init, ...) must keep `--help` in their args so the
654
+ // command-local printer fires. Without this carve-out
655
+ // `pugi login --help` produces the global help and the per-variant
656
+ // reference (`--provider device|token|env`) gets lost. The carve-out
657
+ // list mirrors handlers whose source carries an
658
+ // `args.includes('--help')` short-circuit.
659
+ if ((args.includes('--help') || args.includes('-h')) && !COMMAND_LOCAL_HELP.has(command)) {
660
+ return { command: 'help', args: [command], flags, isBareInvocation: false };
661
+ }
481
662
  return {
482
- command: args.shift() ?? 'help',
663
+ command,
483
664
  args,
484
665
  flags,
485
666
  isBareInvocation,
486
667
  };
487
668
  }
669
+ /**
670
+ * β1 Tt3: commands that own their `--help` rendering. The bare-help
671
+ * redirect leaves their `--help` arg in place so the command-local
672
+ * printer fires instead of the global summary.
673
+ */
674
+ const COMMAND_LOCAL_HELP = new Set([
675
+ 'login',
676
+ ]);
488
677
  async function version(_args, flags, _session) {
489
678
  const payload = {
490
679
  name: 'pugi',
@@ -527,6 +716,15 @@ async function help(_args, flags, _session) {
527
716
  ' pugi ask "<question>" Surface a yes/no question modal locally.',
528
717
  ' pugi plan-review <task> Generate + present a plan-review modal.',
529
718
  '',
719
+ 'Persona dispatch (α7.5):',
720
+ ' pugi roster List the live Tier 1 personas + roles.',
721
+ ' pugi delegate <slug> "<brief>" Dispatch a brief to one specialist.',
722
+ '',
723
+ 'Plan decomposition (α6.8):',
724
+ ' pugi plan --decompose <idea> Split a high-level idea into 3-7 components.',
725
+ ' Writes .pugi/plan/<session-id>/splits/NN-<name>/spec.md',
726
+ ' plus manifest.md with the dependency DAG.',
727
+ '',
530
728
  'Deploy:',
531
729
  ' pugi deploy --target vercel <vercelProject> --project <id>',
532
730
  ' Trigger a Vercel deployment from the bound Git source.',
@@ -549,6 +747,8 @@ async function help(_args, flags, _session) {
549
747
  ' PUGI_SKIP_SPLASH=1.',
550
748
  ' --no-tool-stream Hide the live tool stream pane (α6.12).',
551
749
  ' Pairs with PUGI_HIDE_TOOL_STREAM=1.',
750
+ ' --no-defaults Skip bundled default-skills install on',
751
+ ' `pugi init`. Pairs with PUGI_INIT_NO_DEFAULTS=1.',
552
752
  '',
553
753
  PUGI_TAGLINE,
554
754
  'Execution defaults to local. Use --remote or --web to create a handoff bundle.',
@@ -610,14 +810,28 @@ async function doctor(_args, flags, _session) {
610
810
  `Release guard: ${payload.releaseGuard}`,
611
811
  ].join('\n'));
612
812
  }
613
- async function init(_args, flags, _session) {
614
- const cwd = process.cwd();
813
+ /**
814
+ * Programmatic init scaffolder. Idempotent — every helper call is a
815
+ * `*_IfMissing` write, so re-running over an existing .pugi/ workspace
816
+ * adds nothing to `created` and the operator sees the "Already
817
+ * initialized" copy. Default skills install is best-effort: failure
818
+ * does not throw, the error is appended to the result via stderr so
819
+ * the slash dispatcher can surface it in the REPL system pane.
820
+ *
821
+ * Callers MUST provide `cwd` explicitly; the function does not read
822
+ * `process.cwd()` so REPL invocations from an arbitrary workspace
823
+ * cannot accidentally scaffold the binary's install directory.
824
+ */
825
+ export async function scaffoldPugiWorkspace(input) {
826
+ const cwd = input.cwd;
827
+ const log = input.log ?? ((line) => process.stderr.write(line));
615
828
  const pugiDir = resolve(cwd, '.pugi');
616
829
  const created = [];
617
830
  const skipped = [];
618
831
  ensureDir(pugiDir, created, skipped);
619
832
  ensureDir(resolve(pugiDir, 'artifacts'), created, skipped);
620
833
  ensureDir(resolve(pugiDir, 'sessions'), created, skipped);
834
+ ensureDir(resolve(pugiDir, 'skills'), created, skipped);
621
835
  writeJsonIfMissing(resolve(pugiDir, 'settings.json'), {
622
836
  schema: 1,
623
837
  workflow: {
@@ -687,17 +901,67 @@ async function init(_args, flags, _session) {
687
901
  // Ensure `.pugi/` is git-ignored so users do not accidentally commit
688
902
  // local audit logs, artifacts, or triple-review request payloads.
689
903
  ensurePugiGitIgnore(cwd, created, skipped);
690
- const payload = {
904
+ // Bundled default skills (brand-voice, endpoint-probe, readme-sync).
905
+ // Skipped when --no-defaults is passed OR when PUGI_INIT_NO_DEFAULTS=1.
906
+ // Idempotent: a skill whose target directory already exists is left
907
+ // alone so re-running `pugi init` after the operator customised one of
908
+ // the defaults does not clobber their edits.
909
+ let defaultSkills = [];
910
+ if (!input.noDefaults) {
911
+ try {
912
+ defaultSkills = await installDefaultSkills({
913
+ workspaceRoot: cwd,
914
+ log,
915
+ });
916
+ }
917
+ catch (error) {
918
+ // Default-skills install is a convenience layer. A failure here
919
+ // (bad sha256 hashing, permission error on .pugi/skills/) must not
920
+ // leave `pugi init` in a half-state where settings.json exists but
921
+ // the operator sees an unexplained crash. Log the error to stderr
922
+ // and continue — the operator can still install skills manually.
923
+ const message = error instanceof Error ? error.message : String(error);
924
+ log(`[pugi init] default-skills install failed: ${message}\n`);
925
+ }
926
+ }
927
+ return {
691
928
  status: 'initialized',
692
929
  root: cwd,
693
930
  created,
694
931
  skipped,
932
+ defaultSkills,
933
+ alreadyInitialized: created.length === 0,
695
934
  };
696
- writeOutput(flags, payload, [
935
+ }
936
+ /**
937
+ * Standalone `pugi init` CLI entry. Thin wrapper around
938
+ * `scaffoldPugiWorkspace` that handles flag plumbing + writeOutput
939
+ * formatting. β1a r1: extracted from the previous inline init so the
940
+ * REPL's `/init` slash can call `scaffoldPugiWorkspace` directly.
941
+ */
942
+ async function init(_args, flags, _session) {
943
+ const result = await scaffoldPugiWorkspace({
944
+ cwd: process.cwd(),
945
+ noDefaults: flags.noDefaults,
946
+ });
947
+ const defaultSkillLines = flags.noDefaults
948
+ ? ['Default skills: skipped (--no-defaults)']
949
+ : result.defaultSkills.length === 0
950
+ ? ['Default skills: none installed']
951
+ : [
952
+ 'Default skills:',
953
+ ...result.defaultSkills.map((entry) => ` ${entry.name.padEnd(18)} ${entry.status}`),
954
+ ];
955
+ writeOutput(flags, result, [
697
956
  'Pugi initialized',
698
- `Root: ${cwd}`,
699
- created.length ? `Created:\n${created.map((path) => ` ${path}`).join('\n')}` : 'Created: none',
700
- skipped.length ? `Already present:\n${skipped.map((path) => ` ${path}`).join('\n')}` : 'Already present: none',
957
+ `Root: ${result.root}`,
958
+ result.created.length
959
+ ? `Created:\n${result.created.map((path) => ` ${path}`).join('\n')}`
960
+ : 'Created: none',
961
+ result.skipped.length
962
+ ? `Already present:\n${result.skipped.map((path) => ` ${path}`).join('\n')}`
963
+ : 'Already present: none',
964
+ ...defaultSkillLines,
701
965
  ].join('\n'));
702
966
  }
703
967
  async function idea(args, flags, session) {
@@ -2042,6 +2306,26 @@ function runEngineTask(kind) {
2042
2306
  const config = credential
2043
2307
  ? buildRuntimeConfig({ apiUrl: credential.apiUrl, apiKey: credential.apiKey })
2044
2308
  : envConfig;
2309
+ // α6.8 EXTEND PR1 v2: `--decompose` gating runs BEFORE the offline
2310
+ // fallback. Two reasons:
2311
+ // 1. The flag is plan-only — surfacing the rejection for
2312
+ // `pugi build --decompose` before we drop into `offlineBuild`
2313
+ // means the operator gets a deterministic error instead of a
2314
+ // silent no-op stub.
2315
+ // 2. The decompose post-processor depends on the engine's final
2316
+ // text. The offline plan stub does not invoke the engine, so
2317
+ // `pugi plan --decompose --offline` would silently skip the
2318
+ // decomposition step. Refusing the combination up front is the
2319
+ // cheapest way to keep the contract honest.
2320
+ if (flags.decompose && kind !== 'plan') {
2321
+ throw new Error(`--decompose is only valid for \`pugi plan\` (got \`pugi ${label}\`)`);
2322
+ }
2323
+ if (flags.decompose && flags.offline) {
2324
+ throw new Error('--decompose requires the engine — drop --offline (decomposition needs the model to emit a fenced JSON block)');
2325
+ }
2326
+ if (flags.decompose && !config) {
2327
+ 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)');
2328
+ }
2045
2329
  // Offline fallback: preserves the local-first invariant. `plan` /
2046
2330
  // `build` / `explain` drop back to their pre-Sprint-2 stub
2047
2331
  // behaviour so an operator without an API key (or with --offline)
@@ -2094,6 +2378,17 @@ function runEngineTask(kind) {
2094
2378
  throw new Error(`pugi ${label} requires a prompt`);
2095
2379
  }
2096
2380
  }
2381
+ // α6.8 EXTEND PR1: when `--decompose` is set, augment the user
2382
+ // prompt with the decomposition-request suffix BEFORE the adapter
2383
+ // run. The system prompt for `plan` already constrains the model
2384
+ // to read-only tools + a plan deliverable; the suffix layers the
2385
+ // JSON-emission contract on top so the post-run parser can lift
2386
+ // the structured payload out of the final answer. The plan-only /
2387
+ // engine-required gates fired before the offline fallback above,
2388
+ // so by here we know we are on the engine path with a plan task.
2389
+ if (flags.decompose && kind === 'plan') {
2390
+ prompt = `${prompt}\n${DECOMPOSE_PROMPT_SUFFIX}`;
2391
+ }
2097
2392
  // Narrow `config` for the type checker — the offline branches above
2098
2393
  // return whenever `config` is null, so by this point it must be set.
2099
2394
  if (!config) {
@@ -2203,6 +2498,41 @@ function runEngineTask(kind) {
2203
2498
  statusEvents,
2204
2499
  });
2205
2500
  }
2501
+ // α6.8 EXTEND PR1: `--decompose` post-processing. We only attempt
2502
+ // the parse on a `done` plan (a blocked/failed plan is already
2503
+ // captured in plan.md with its reason; no JSON to extract). The
2504
+ // model's final answer arrives via `result.summary` — on success
2505
+ // the adapter prefix is empty so it is the raw final text. We
2506
+ // strip any leading/trailing whitespace then run the parser
2507
+ // against the contents. On parse failure we surface a non-fatal
2508
+ // structured error in the payload — the operator still gets the
2509
+ // plan.md artifact and can re-run.
2510
+ //
2511
+ // TODO(α7.x): `result.summary` is currently a string contract that
2512
+ // doubles as both "human-readable headline" and "raw final model
2513
+ // text". Split into `{ summary, finalText }` on the adapter so the
2514
+ // parser does not have to assume the prefix is empty. Tracked in
2515
+ // PR #423 v2 retro (P2.6, Claude review).
2516
+ let decomposeArtifact = null;
2517
+ let decomposeError = null;
2518
+ if (flags.decompose && kind === 'plan' && result.status === 'done') {
2519
+ const parsed = parseDecompositionFromText(result.summary);
2520
+ if (parsed.ok) {
2521
+ decomposeArtifact = writeDecomposition({
2522
+ root,
2523
+ sessionId: session.id,
2524
+ // Persist the OPERATOR's original prompt, not the prompt+suffix
2525
+ // we sent to the engine. The suffix is plumbing; the manifest
2526
+ // header reads naturally only with the operator text.
2527
+ prompt: args.join(' ').trim() || prompt,
2528
+ decomposition: parsed.decomposition,
2529
+ rationale: parsed.rationale,
2530
+ });
2531
+ }
2532
+ else {
2533
+ decomposeError = { reason: parsed.reason, detail: parsed.detail };
2534
+ }
2535
+ }
2206
2536
  // Pull the headline metrics out of `eventRefs` so the summary and
2207
2537
  // JSON envelope match without re-parsing strings in two places.
2208
2538
  const metrics = parseEventRefs(result.eventRefs);
@@ -2262,6 +2592,20 @@ function runEngineTask(kind) {
2262
2592
  reason: dr.reason,
2263
2593
  detail: dr.detail,
2264
2594
  })),
2595
+ // α6.8 EXTEND PR1: decompose artifacts (only present when
2596
+ // `--decompose` was passed AND the model emitted a parseable
2597
+ // JSON block). The `error` shape lands when the model returned
2598
+ // unparseable output; the operator can re-run with a tighter
2599
+ // prompt without losing the plain plan.md artifact.
2600
+ decompose: decomposeArtifact !== null
2601
+ ? {
2602
+ manifest: relative(root, decomposeArtifact.manifestPath),
2603
+ planDir: relative(root, decomposeArtifact.planDir),
2604
+ splits: decomposeArtifact.splitPaths,
2605
+ }
2606
+ : decomposeError !== null
2607
+ ? { error: decomposeError }
2608
+ : undefined,
2265
2609
  // The full event stream is useful for cabinet UI replay. We surface
2266
2610
  // it in JSON mode only — text mode operators want the summary, not
2267
2611
  // 30 turn-level lines.
@@ -2271,6 +2615,13 @@ function runEngineTask(kind) {
2271
2615
  if (kind === 'plan' && planArtifact) {
2272
2616
  textLines.push(`Pugi plan written to ${planArtifact.relPath}`);
2273
2617
  }
2618
+ if (decomposeArtifact !== null) {
2619
+ textLines.push(`Decomposition: ${decomposeArtifact.splitPaths.length} component spec${decomposeArtifact.splitPaths.length === 1 ? '' : 's'} under ${relative(root, decomposeArtifact.planDir)}`);
2620
+ textLines.push(`Manifest: ${relative(root, decomposeArtifact.manifestPath)}`);
2621
+ }
2622
+ else if (decomposeError !== null) {
2623
+ textLines.push(`Decomposition: skipped (${decomposeError.reason}) — plan.md still written`);
2624
+ }
2274
2625
  textLines.push(`Pugi ${label}: ${result.status}`);
2275
2626
  textLines.push(`Summary: ${result.summary}`);
2276
2627
  if (result.filesChanged.length > 0) {
@@ -4087,5 +4438,6 @@ export function packageRoot() {
4087
4438
  export const __test__ = {
4088
4439
  sleep,
4089
4440
  pollDeviceFlowUntilTerminal,
4441
+ sanitizeSemver,
4090
4442
  };
4091
4443
  //# sourceMappingURL=cli.js.map