@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.
- 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/anvil-client.js +16 -0
- package/dist/core/engine/budgets.js +89 -0
- package/dist/core/engine/native-pugi.js +112 -12
- package/dist/core/engine/prompts.js +8 -0
- package/dist/core/engine/tool-bridge.js +267 -8
- package/dist/core/init/scaffold.js +195 -0
- package/dist/core/lsp/client.js +719 -0
- package/dist/core/repl/codebase-survey.js +308 -0
- package/dist/core/repl/init-interview.js +457 -0
- package/dist/core/repl/onboarding-state.js +297 -0
- package/dist/core/repl/session.js +72 -1
- package/dist/core/repl/slash-commands.js +41 -0
- package/dist/core/settings.js +28 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/runtime/cli.js +366 -14
- 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/ask-user.js +115 -0
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/conversation-pane.js +1 -1
- package/dist/tui/input-box.js +1 -1
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/repl-render.js +169 -10
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +18 -5
- package/dist/tui/splash.js +1 -1
- package/dist/tui/update-banner.js +1 -1
- 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,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
|
-
|
|
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
|
|
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
|
|
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
|
|
463
|
-
// pane shows
|
|
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
|
|
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
|
-
|
|
614
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${
|
|
699
|
-
created.length
|
|
700
|
-
|
|
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
|