@pugi/cli 0.1.0-beta.92 → 0.1.0-beta.94

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/dist/commands/retro.js +210 -0
  2. package/dist/core/diagnostics/probes/sandbox.js +65 -33
  3. package/dist/core/engine/native-pugi.js +185 -11
  4. package/dist/core/engine/prompts.js +1 -1
  5. package/dist/core/engine/tool-bridge.js +35 -0
  6. package/dist/core/engine/verification-patterns.js +195 -0
  7. package/dist/core/mcp/orchestrator-config.js +192 -0
  8. package/dist/core/mcp/orchestrator-tools.js +147 -3
  9. package/dist/core/pugi-gitignore.js +52 -0
  10. package/dist/core/repl/engine-bridge.js +199 -0
  11. package/dist/core/repl/session.js +395 -6
  12. package/dist/core/repl/tool-route.js +382 -0
  13. package/dist/core/retro/git-collector.js +251 -0
  14. package/dist/core/retro/health-card.js +25 -0
  15. package/dist/core/retro/metrics.js +342 -0
  16. package/dist/core/retro/narrative.js +249 -0
  17. package/dist/core/retro/plane-collector.js +274 -0
  18. package/dist/core/retro/pr-issue-link.js +65 -0
  19. package/dist/core/retro/types.js +16 -0
  20. package/dist/core/sandboxing/adapter.js +29 -0
  21. package/dist/core/sandboxing/index.js +49 -0
  22. package/dist/core/sandboxing/none.js +19 -0
  23. package/dist/core/sandboxing/seatbelt.js +183 -0
  24. package/dist/core/session.js +27 -0
  25. package/dist/core/settings.js +22 -0
  26. package/dist/runtime/cli.js +167 -33
  27. package/dist/runtime/commands/compact.js +1 -1
  28. package/dist/runtime/commands/config.js +1 -1
  29. package/dist/runtime/commands/mcp.js +64 -8
  30. package/dist/runtime/commands/memory.js +1 -1
  31. package/dist/runtime/deprecation-warning.js +69 -0
  32. package/dist/runtime/headless.js +8 -3
  33. package/dist/runtime/stream-renderer.js +195 -0
  34. package/dist/runtime/version.js +1 -1
  35. package/dist/skills/bundled/remember.js +2 -2
  36. package/dist/tui/agent-tree.js +11 -0
  37. package/dist/tui/ask-user-question-chips.js +1 -1
  38. package/dist/tui/multi-file-diff-approval.js +3 -3
  39. package/dist/tui/repl-render.js +42 -0
  40. package/package.json +2 -2
  41. package/test/scenarios/identity.scenario.txt +0 -1
@@ -203,6 +203,28 @@ const pugiSettingsSchema = z.object({
203
203
  cleanupPeriodDays: z.number().int().min(0).max(365).optional(),
204
204
  })
205
205
  .optional(),
206
+ // Trust Sprint item 6 — bash sandbox adapter selection.
207
+ //
208
+ // `none` — passthrough (existing behaviour, default).
209
+ // `macOS-seatbelt` — wraps spawn calls in `/usr/bin/sandbox-exec`
210
+ // with a profile that allows reads anywhere,
211
+ // denies writes outside workspace + ~/.pugi,
212
+ // and permits standard network egress so
213
+ // `npm install` / `git fetch` still work.
214
+ // `docker` — Linux fallback (NOT shipped in this PR;
215
+ // accepted in the schema so settings.json
216
+ // does not error when operators forward-look
217
+ // at the keyword. Adapter throws at boot if
218
+ // selected today).
219
+ //
220
+ // The bash classifier denylist + permission FSM remain in force on
221
+ // top of the sandbox. This is defence-in-depth: the sandbox bounds
222
+ // what a tool CAN do; the classifier bounds what a tool TRIES.
223
+ bash: z
224
+ .object({
225
+ sandbox: z.enum(['none', 'macOS-seatbelt', 'docker']).optional(),
226
+ })
227
+ .optional(),
206
228
  });
207
229
  /**
208
230
  * #20 — the upstream tool drop-in compat ingest.
@@ -6,8 +6,10 @@ import { homedir } from 'node:os';
6
6
  import { dirname, relative, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { linkArtifact } from '../core/format/osc8-link.js';
9
+ import { ensurePugiGitIgnore } from '../core/pugi-gitignore.js';
9
10
  import { AnvilEngineLoopClient } from '../core/engine/anvil-client.js';
10
11
  import { NativePugiEngineAdapter } from '../core/engine/native-pugi.js';
12
+ import { attachStreamRenderer } from './stream-renderer.js';
11
13
  import { profileFor, resolveIntensity, resolveMaxTurns, } from '../core/engine/intensity.js';
12
14
  import { loadMcpRegistry } from '../core/mcp/registry.js';
13
15
  import { loadHookRegistryOrExit } from './load-hooks-or-exit.js';
@@ -29,6 +31,7 @@ import { clearApiKey, DEFAULT_API_URL, listStoredCredentials, maskApiKey, normal
29
31
  import { resolveAndValidateEnvLogin, } from '../core/auth/env-provider.js';
30
32
  import { runDeployCommand } from '../commands/deploy.js';
31
33
  import { runJobsCommand } from '../commands/jobs.js';
34
+ import { runRetroCommand } from '../commands/retro.js';
32
35
  import { runConfigCommand } from './commands/config.js';
33
36
  import { runStyleCommand } from './commands/style.js';
34
37
  import { runThemeCommand } from './commands/theme.js';
@@ -176,6 +179,10 @@ const handlers = {
176
179
  // most-recent failed session as a redacted bundle so operators can
177
180
  // file clean bug reports without manual log-grepping.
178
181
  report: dispatchReport,
182
+ // PUGI-RETRO-CMD : `pugi retro` mines git history into a
183
+ // weekly engineering retrospective. M1 git+markdown, M2 Plane bridge,
184
+ // M3 health card + optional `--post-plane` upload.
185
+ retro: dispatchRetro,
179
186
  review,
180
187
  resume,
181
188
  roster: dispatchRoster,
@@ -2064,6 +2071,19 @@ function parseArgs(argv) {
2064
2071
  else if (arg === '--no-tools') {
2065
2072
  flags.noTools = true;
2066
2073
  }
2074
+ else if (arg === '--stream') {
2075
+ // Trust Sprint item 5 — force the stderr streaming renderer ON
2076
+ // regardless of TTY detection. Useful for non-interactive
2077
+ // shells that still want progress lines (recorded sessions,
2078
+ // tmux pipes, log-tail consumers).
2079
+ flags.stream = true;
2080
+ }
2081
+ else if (arg === '--no-stream') {
2082
+ // Trust Sprint item 5 — opt out of the streaming renderer even
2083
+ // on a TTY. CI scripts that grep stderr for known patterns
2084
+ // pass this to keep the stream clean.
2085
+ flags.stream = false;
2086
+ }
2067
2087
  else if (arg === '--max-turns') {
2068
2088
  const next = argv[index + 1];
2069
2089
  if (!next || next.startsWith('--'))
@@ -2650,6 +2670,25 @@ const COMMAND_HELP_BODIES = {
2650
2670
  '',
2651
2671
  'Optional: --target-env production|preview, --ref <ref>, --integration <id>.',
2652
2672
  ],
2673
+ retro: [
2674
+ 'pugi retro — engineering retrospective from git history.',
2675
+ '',
2676
+ ' pugi retro Default 7d window, midnight-aligned in local TZ.',
2677
+ ' pugi retro 24h | 14d | 30d Custom window size.',
2678
+ ' pugi retro compare [<win>] Current window vs the prior same-length window.',
2679
+ ' --plane Enrich with closed + created Plane issues, cycle, modules.',
2680
+ ' --post-plane After rendering, post the retro as a Plane issue',
2681
+ ' (idempotent on date + sequence). Implies --plane.',
2682
+ ' --json Emit JSON envelope to stdout.',
2683
+ '',
2684
+ 'Outputs: `.pugi/retros/<date>-<seq>.md` plus a JSON snapshot mirror.',
2685
+ 'Metrics: commits, LOC, test ratio, sessions, hourly histogram, top 10',
2686
+ 'hotspots, focus score, ship of the week, personal + team streak.',
2687
+ '',
2688
+ 'Plane env: PLANE_BASE_URL (default https://plane.pugi.io), PLANE_API_KEY,',
2689
+ 'PLANE_WORKSPACE_SLUG (default pugi-customers), PLANE_PROJECT_ID. The same',
2690
+ 'fields also load from `.pugi/settings.json::plane.{baseUrl,apiKey,...}`.',
2691
+ ],
2653
2692
  };
2654
2693
  async function help(args, flags, _session) {
2655
2694
  // task: per-command help bodies. When dispatcher
@@ -2733,6 +2772,11 @@ async function help(args, flags, _session) {
2733
2772
  ' PUGI_SKIP_SPLASH=1.',
2734
2773
  ' --no-tool-stream Hide the live tool stream pane .',
2735
2774
  ' Pairs with PUGI_HIDE_TOOL_STREAM=1.',
2775
+ ' --stream Force stderr progress lines (tool calls,',
2776
+ ' thinking pulses) ON regardless of TTY.',
2777
+ ' Trust Sprint item 5.',
2778
+ ' --no-stream Force stderr progress lines OFF even on a',
2779
+ ' TTY. --json implicitly disables stream.',
2736
2780
  ' --no-defaults Skip bundled default-skills install on',
2737
2781
  ' `pugi init`. Pairs with PUGI_INIT_NO_DEFAULTS=1.',
2738
2782
  ' --no-scan Skip codebase scan + PUGI.md auto-gen on',
@@ -4982,7 +5026,43 @@ const ENGINE_EXIT_CODES = {
4982
5026
  failed: 8,
4983
5027
  blocked: 9,
4984
5028
  engine_unavailable: 1,
5029
+ // PUGI-VERIFY-GATE: needs_verification is the engine telling the
5030
+ // operator "you did not run a verification command — I cannot
5031
+ // confirm correctness". CI scripts treating any non-zero as
5032
+ // failure keep working; exit 2 historically means "misuse" (the
5033
+ // engine completed but the operator missed a required step),
5034
+ // which matches the semantic here.
5035
+ needs_verification: 2,
4985
5036
  };
5037
+ /**
5038
+ * PUGI-VERIFY-GATE — Codex dogfood 2026-06-04 surfaced a P0 where
5039
+ * `pugi code` returned exit 0 while npm test failed. The spec
5040
+ * locks the contract to exit 1 on a verification failure AND exit
5041
+ * 2 on `needs_verification`. Legacy callers reading exit 8/9 still
5042
+ * see "any non-zero = failure" but the new codes (1/2) are the
5043
+ * authoritative signal for the verification gate.
5044
+ *
5045
+ * The override fires when `result.status` carries the new
5046
+ * `needs_verification` literal OR when `result.unverifiedReason`
5047
+ * is set to `verification_command_failed` — the latter pins exit
5048
+ * 1 even if a future producer left `status: 'failed'` while
5049
+ * inferring the cause from the reason field.
5050
+ */
5051
+ function resolveEngineExitCode(result) {
5052
+ if (result.status === 'needs_verification') {
5053
+ return ENGINE_EXIT_CODES.needs_verification;
5054
+ }
5055
+ if (result.unverifiedReason === 'verification_command_failed') {
5056
+ // Spec: verified=false because a verification command failed
5057
+ // maps to CLI exit 1 (clear "this is broken" signal).
5058
+ return 1;
5059
+ }
5060
+ if (result.status === 'done')
5061
+ return ENGINE_EXIT_CODES.done;
5062
+ if (result.status === 'failed')
5063
+ return ENGINE_EXIT_CODES.failed;
5064
+ return ENGINE_EXIT_CODES.blocked;
5065
+ }
4986
5066
  function commandLabel(kind) {
4987
5067
  return kind === 'build_task' ? 'build' : kind;
4988
5068
  }
@@ -5464,6 +5544,16 @@ function runEngineTask(kind) {
5464
5544
  }
5465
5545
  };
5466
5546
  process.on('SIGINT', onSigint);
5547
+ // Trust Sprint item 5 — attach the stderr streaming renderer so
5548
+ // the operator sees tool calls + thinking pulses as they happen
5549
+ // rather than staring at an empty terminal until the dispatch
5550
+ // completes. TTY default ON; `--stream`/`--no-stream` override;
5551
+ // `--json` implicitly disables (machine consumers want stdout
5552
+ // pristine and stderr quiet).
5553
+ const streamerHandle = attachStreamRenderer(adapter.streamEmitter, {
5554
+ isTty: Boolean(process.stderr.isTTY),
5555
+ explicit: flags.json ? false : (flags.stream ?? null),
5556
+ });
5467
5557
  // β4 r2 P1 #3 — try/finally so loaded MCP child processes are
5468
5558
  // reaped regardless of run outcome (success, blocked, failed,
5469
5559
  // thrown). Triple-review P1 : the try now wraps BOTH the
@@ -5521,6 +5611,23 @@ function runEngineTask(kind) {
5521
5611
  filesChanged: event.result.filesChanged,
5522
5612
  eventRefs: event.result.eventRefs,
5523
5613
  risks: event.result.risks,
5614
+ // PUGI-VERIFY-GATE: pass verification state through. All
5615
+ // fields optional on the SDK schema; forward only what
5616
+ // the adapter populated so MCP/JSON consumers see the
5617
+ // gate.
5618
+ ...(event.result.verified !== undefined ? { verified: event.result.verified } : {}),
5619
+ ...(event.result.verificationCommands !== undefined
5620
+ ? { verificationCommands: event.result.verificationCommands }
5621
+ : {}),
5622
+ ...(event.result.verificationFailures !== undefined
5623
+ ? { verificationFailures: event.result.verificationFailures }
5624
+ : {}),
5625
+ ...(event.result.unverifiedReason !== undefined
5626
+ ? { unverifiedReason: event.result.unverifiedReason }
5627
+ : {}),
5628
+ ...(event.result.regressionOwnershipDispute !== undefined
5629
+ ? { regressionOwnershipDispute: event.result.regressionOwnershipDispute }
5630
+ : {}),
5524
5631
  };
5525
5632
  }
5526
5633
  }
@@ -5627,7 +5734,14 @@ function runEngineTask(kind) {
5627
5734
  // Pull the headline metrics out of `eventRefs` so the summary and
5628
5735
  // JSON envelope match without re-parsing strings in two places.
5629
5736
  const metrics = parseEventRefs(result.eventRefs);
5630
- const finalStatus = result.status === 'failed' ? 'error' : 'success';
5737
+ // PUGI-VERIFY-GATE: `needs_verification` is a soft failure
5738
+ // the loop completed but lacks proof of correctness. Record it
5739
+ // as `error` so the session log marker matches the non-zero
5740
+ // exit code; a future audit query can distinguish the two via
5741
+ // the structured outcome.
5742
+ const finalStatus = result.status === 'failed' || result.status === 'needs_verification'
5743
+ ? 'error'
5744
+ : 'success';
5631
5745
  recordToolResult(session, toolCallId, finalStatus, result.summary);
5632
5746
  // Exit code policy (spec §1-§5):
5633
5747
  // code/fix/build → 0 done, 8 failed, 9 blocked
@@ -5649,6 +5763,14 @@ function runEngineTask(kind) {
5649
5763
  metrics.outcome === 'budget_exhausted') {
5650
5764
  process.exitCode = ENGINE_EXIT_CODES.blocked;
5651
5765
  }
5766
+ else if (result.status === 'needs_verification') {
5767
+ // PUGI-VERIFY-GATE: plan rarely runs verification commands
5768
+ // (plan is read-only), but the new status reaches here when
5769
+ // a plan run completed without verification AND the gate
5770
+ // promoted `done` to `needs_verification`. Surface exit 2
5771
+ // so CI sees the missing signal.
5772
+ process.exitCode = ENGINE_EXIT_CODES.needs_verification;
5773
+ }
5652
5774
  else {
5653
5775
  // `done`, or `blocked` with outcome=tool_refused (= the plan-mode
5654
5776
  // gate fired, which is the contract working as designed), or
@@ -5658,7 +5780,12 @@ function runEngineTask(kind) {
5658
5780
  }
5659
5781
  }
5660
5782
  else {
5661
- process.exitCode = ENGINE_EXIT_CODES[result.status];
5783
+ // PUGI-VERIFY-GATE: route through `resolveEngineExitCode` so
5784
+ // `needs_verification` maps to exit 2 and a verification
5785
+ // command failure maps to exit 1 (per spec). Legacy `failed`
5786
+ // / `blocked` still produce 8 / 9 to preserve scripts that
5787
+ // already branch on them.
5788
+ process.exitCode = resolveEngineExitCode(result);
5662
5789
  }
5663
5790
  const payload = {
5664
5791
  command: label,
@@ -5673,6 +5800,15 @@ function runEngineTask(kind) {
5673
5800
  sessionEventsMirror: metrics.mirror,
5674
5801
  risks: result.risks,
5675
5802
  plan: planArtifact ? { path: planArtifact.relPath } : undefined,
5803
+ // PUGI-VERIFY-GATE: thread verification state into the JSON
5804
+ // envelope so MCP wrappers, CI scripts, and the cabinet UI
5805
+ // can branch on `verified` / `unverifiedReason` without
5806
+ // grepping the summary string.
5807
+ verified: result.verified,
5808
+ verificationCommands: result.verificationCommands,
5809
+ verificationFailures: result.verificationFailures,
5810
+ unverifiedReason: result.unverifiedReason,
5811
+ regressionOwnershipDispute: result.regressionOwnershipDispute,
5676
5812
  // — per-edit dispatcher trace. Empty array when no inline
5677
5813
  // markers were detected in the model's final response.
5678
5814
  diffEdits: dispatchResults.map((dr) => ({
@@ -5751,6 +5887,11 @@ function runEngineTask(kind) {
5751
5887
  writeOutput(flags, payload, textLines.join('\n'));
5752
5888
  }
5753
5889
  finally {
5890
+ // Trust Sprint item 5 — detach the streaming renderer first so
5891
+ // it never paints during the MCP shutdown / LSP teardown phase
5892
+ // below. The handle is a no-op when streaming was disabled, so
5893
+ // this is unconditional and safe.
5894
+ streamerHandle.detach();
5754
5895
  // CEO P1 #25 — detach the per-run SIGINT handler so a second
5755
5896
  // engine run from the same process (tests, scripts iterating
5756
5897
  // `runEngineTask`, future REPL non-watch dispatches) starts
@@ -7275,39 +7416,32 @@ function notImplemented(command) {
7275
7416
  writeOutput(flags, payload, payload.message);
7276
7417
  };
7277
7418
  }
7278
- function ensurePugiGitIgnore(cwd, created, skipped) {
7279
- const gitignorePath = resolve(cwd, '.gitignore');
7280
- // PUGI-487 - also ensure `.claude/worktrees/` is git-ignored so the
7281
- // user-facing `pugi --worktree` flag does not surface its created
7282
- // trees as untracked status noise.
7283
- const markers = ['.pugi/', '.claude/worktrees/'];
7284
- if (!existsSync(gitignorePath)) {
7285
- writeFileSync(gitignorePath, `${markers.join('\n')}\n`, { encoding: 'utf8', mode: 0o600 });
7286
- created.push(gitignorePath);
7287
- return;
7288
- }
7289
- const current = readFileSync(gitignorePath, 'utf8');
7290
- const lines = current.split('\n').map((line) => line.trim());
7291
- const equivalents = {
7292
- '.pugi/': ['.pugi/', '/.pugi/', '.pugi'],
7293
- '.claude/worktrees/': ['.claude/worktrees/', '/.claude/worktrees/', '.claude/worktrees'],
7294
- };
7295
- const toAppend = [];
7296
- for (const marker of markers) {
7297
- const eq = equivalents[marker] ?? [marker];
7298
- const present = eq.some((variant) => lines.includes(variant));
7299
- if (!present)
7300
- toAppend.push(marker);
7301
- }
7302
- if (toAppend.length === 0) {
7303
- skipped.push(gitignorePath);
7304
- return;
7419
+ /**
7420
+ * `pugi retro` — engineering retrospective. PUGI-RETRO-CMD.
7421
+ *
7422
+ * Thin shim onto `runRetroCommand`. Forwards the global `--json` flag
7423
+ * so JSON envelope mode survives the dispatcher even though
7424
+ * `parseArgs` strips it before the per-command args reach the runner.
7425
+ * Same pattern as `dispatchDeploy` above.
7426
+ */
7427
+ async function dispatchRetro(args, flags, _session) {
7428
+ const exitCode = await runRetroCommand({
7429
+ args,
7430
+ flags: { json: flags.json },
7431
+ cwd: process.cwd(),
7432
+ io: {
7433
+ write: (text) => process.stdout.write(text),
7434
+ writeError: (text) => process.stderr.write(text.endsWith('\n') ? text : `${text}\n`),
7435
+ },
7436
+ });
7437
+ if (exitCode !== 0) {
7438
+ process.exitCode = exitCode;
7305
7439
  }
7306
- const trailing = current.endsWith('\n') ? '' : '\n';
7307
- const next = `${current}${trailing}${toAppend.join('\n')}\n`;
7308
- writeFileSync(gitignorePath, next, { encoding: 'utf8' });
7309
- created.push(`${gitignorePath} (+${toAppend.join(', ')})`);
7310
7440
  }
7441
+ // ensurePugiGitIgnore extracted к `apps/pugi-cli/src/core/pugi-gitignore.ts`
7442
+ // ( triple-review P1 dedup). Re-exported here so existing call
7443
+ // sites at cli.ts:3732 keep their import path unchanged.
7444
+ export { ensurePugiGitIgnore } from '../core/pugi-gitignore.js';
7311
7445
  /**
7312
7446
  * Compute the workspace label surfaced in the REPL header bar
7313
7447
  * (Sprint ). We prefer the basename of the workspace root because
@@ -132,7 +132,7 @@ export async function runCompactCommand(_args, ctx) {
132
132
  summary = await summarizeEvents({
133
133
  events: sourceSlice,
134
134
  client: engineClient,
135
- personaSlug: 'mira',
135
+ personaSlug: 'pugi',
136
136
  });
137
137
  }
138
138
  catch (error) {
@@ -335,7 +335,7 @@ async function runConfigMcpFlip(args, ctx, state) {
335
335
  /* ------------------------------------------------------------------ */
336
336
  /**
337
337
  * Closed sets — match
338
- * `apps/admin-api/src/mira/routing/dispatch-tag.ts` verbatim. Pinning
338
+ * `apps/admin-api/src/pugi/routing/dispatch-tag.ts` verbatim. Pinning
339
339
  * them in the CLI lets us reject typos client-side before round-tripping
340
340
  * to the admin-api (better UX, smaller blast radius for a wrong typo on
341
341
  * a flaky network).
@@ -11,6 +11,7 @@ import { listMcpTrust, setMcpTrust } from '../../core/mcp/trust.js';
11
11
  import { createPugiMcpServer, serveStdio } from '../../core/mcp/server.js';
12
12
  import { buildPugiMcpTools } from '../../core/mcp/server-tools.js';
13
13
  import { buildOrchestratorTools } from '../../core/mcp/orchestrator-tools.js';
14
+ import { resolveOrchestratorConfig, describeOrchestratorConfig, } from '../../core/mcp/orchestrator-config.js';
14
15
  import { serveHttp } from '../../core/mcp/http-server.js';
15
16
  import { resolveActiveCredential, DEFAULT_API_URL } from '../../core/credentials.js';
16
17
  import { listMcpPermissions, clearMcpPermission, } from '../../core/mcp/permission.js';
@@ -80,7 +81,12 @@ const USAGE_LINES = [
80
81
  ' pugi.dispatch / pugi.publish / pugi.deploy instead of',
81
82
  ' the engine surface. Designed for external the upstream tool',
82
83
  ' / Cursor sessions driving fix-publish-test loops.',
83
- ' Each tool family is gated by an env switch:',
84
+ ' --orchestrator-bundle Single-flag form (Trust Sprint item 7). Equivalent to',
85
+ ' --orchestrator --allow-bash with PUGI_MCP_EXEC_ENABLED=1',
86
+ ' and auto-resolves the pugi binary from PATH (or the',
87
+ ' local dev binary when invoked from the monorepo).',
88
+ ' Also enabled by setting PUGI_MCP_ORCHESTRATOR=1.',
89
+ ' Legacy gates still work as deprecated aliases:',
84
90
  ' PUGI_MCP_EXEC_ENABLED=1 enables pugi.run',
85
91
  ' PUGI_MCP_PUBLISH_ENABLED=1 enables pugi.publish',
86
92
  ' PUGI_MCP_DEPLOY_ENABLED=1 enables pugi.deploy',
@@ -545,15 +551,31 @@ async function runMcpServe(args, ctx) {
545
551
  // `buildPugiMcpTools` call (advertising `bash` without the gate flag)
546
552
  // would still be refused at dispatch.
547
553
  const readOnly = flags.readOnly === true;
554
+ // Trust Sprint item 7 — resolve orchestrator config FIRST so it can
555
+ // contribute to bash gating downstream. The resolver collapses the
556
+ // legacy multi-knob mode (PUGI_MCP_EXEC_ENABLED + --allow-bash +
557
+ // PUGI_MCP_PUGI_BIN) into one toggle (PUGI_MCP_ORCHESTRATOR=1 or
558
+ // --orchestrator-bundle).
559
+ const orchestratorConfig = flags.orchestrator
560
+ ? resolveOrchestratorConfig({
561
+ env: process.env,
562
+ bundleFlag: flags.orchestratorBundle,
563
+ bashFlag: flags.bashAllowed,
564
+ workspaceRoot: ctx.workspaceRoot,
565
+ })
566
+ : null;
548
567
  const writeAllowed = !readOnly && flags.writeAllowed;
549
- const bashAllowed = !readOnly && flags.bashAllowed;
568
+ // Bundle implies bash. We OR the resolver's decision into the
569
+ // bash-gating knob so a customer who set PUGI_MCP_ORCHESTRATOR=1 does
570
+ // not also need to pass --allow-bash (item 7 acceptance criterion).
571
+ const bashAllowed = !readOnly && (flags.bashAllowed || (orchestratorConfig?.bashAllowed ?? false));
550
572
  // P1 — when `--orchestrator` is set the surface swaps to the
551
573
  // CLI-orchestrator family (pugi.run / pugi.read / pugi.write /
552
574
  // pugi.dispatch / pugi.publish / pugi.deploy). The engine surface is
553
575
  // intentionally dropped — the two are mutually exclusive on the wire
554
576
  // to keep tool-name resolution unambiguous on the consumer side.
555
577
  const tools = flags.orchestrator
556
- ? buildOrchestratorTools(buildOrchestratorContext(ctx.workspaceRoot))
578
+ ? buildOrchestratorTools(buildOrchestratorContext(ctx.workspaceRoot, orchestratorConfig))
557
579
  : buildPugiMcpTools(toolCtx, {
558
580
  bashAllowed,
559
581
  // Keep the legacy contract: `readOnly` for the tool-builder means
@@ -669,6 +691,15 @@ async function runMcpServe(args, ctx) {
669
691
  // request that returns a response. Operator sees one info line on
670
692
  // stderr so they know the server is up.
671
693
  process.stderr.write(`pugi-mcp (stdio, ${flags.orchestrator ? 'orchestrator' : 'engine'}): ${tools.length} tool(s) — ${tools.map((t) => t.name).join(', ')}\n`);
694
+ // Trust Sprint item 7 — when the orchestrator surface is live, surface
695
+ // the resolved capability table on stderr so the operator can confirm
696
+ // which gates ended up armed without running `pugi doctor` in another
697
+ // pane. Each line is prefixed for grep-ability.
698
+ if (orchestratorConfig) {
699
+ for (const line of describeOrchestratorConfig(orchestratorConfig)) {
700
+ process.stderr.write(`pugi-mcp orchestrator: ${line}\n`);
701
+ }
702
+ }
672
703
  await serveStdio({
673
704
  server,
674
705
  stdin: ctx.stdin ?? process.stdin,
@@ -685,6 +716,7 @@ function parseServeFlags(args) {
685
716
  writeAllowed: false,
686
717
  bashAllowed: false,
687
718
  orchestrator: false,
719
+ orchestratorBundle: false,
688
720
  };
689
721
  for (let i = 0; i < args.length; i += 1) {
690
722
  const arg = args[i] ?? '';
@@ -745,6 +777,14 @@ function parseServeFlags(args) {
745
777
  else if (arg === '--orchestrator') {
746
778
  flags.orchestrator = true;
747
779
  }
780
+ else if (arg === '--orchestrator-bundle') {
781
+ // Trust Sprint item 7 — single-flag form. Implies orchestrator
782
+ // surface + bash + exec. Mutual upgrade with --orchestrator: if
783
+ // both are passed the bundle wins (it is a strict superset).
784
+ flags.orchestrator = true;
785
+ flags.orchestratorBundle = true;
786
+ flags.bashAllowed = true;
787
+ }
748
788
  else if (arg === '--help') {
749
789
  // Caller renders USAGE_LINES. We surface the same via top-level
750
790
  // dispatch — nothing to do here, just don't error.
@@ -796,19 +836,35 @@ function buildServePermissionGate(opts) {
796
836
  *
797
837
  * P1 .
798
838
  */
799
- function buildOrchestratorContext(workspaceRoot) {
839
+ function buildOrchestratorContext(workspaceRoot, resolved = null) {
800
840
  const envRoot = process.env.PUGI_MCP_WORKSPACE_ROOT;
801
841
  const root = envRoot && envRoot.length > 0 ? resolve(envRoot) : workspaceRoot;
802
842
  const credential = resolveActiveCredential();
843
+ // Trust Sprint item 7 — when `resolved` is provided we honour the
844
+ // consolidated capability bits. When absent (legacy direct call from
845
+ // a test) we fall back to the raw env so existing harnesses keep
846
+ // working untouched.
847
+ const execEnabled = resolved
848
+ ? resolved.execEnabled
849
+ : process.env.PUGI_MCP_EXEC_ENABLED === '1';
850
+ const publishEnabled = resolved
851
+ ? resolved.publishEnabled
852
+ : process.env.PUGI_MCP_PUBLISH_ENABLED === '1';
853
+ const deployEnabled = resolved
854
+ ? resolved.deployEnabled
855
+ : process.env.PUGI_MCP_DEPLOY_ENABLED === '1';
856
+ const pugiBin = resolved
857
+ ? resolved.pugiBin
858
+ : (process.env.PUGI_MCP_PUGI_BIN ?? 'pugi');
803
859
  return {
804
860
  workspaceRoot: root,
805
- pugiBin: process.env.PUGI_MCP_PUGI_BIN ?? 'pugi',
861
+ pugiBin,
806
862
  apiUrl: credential?.apiUrl ?? DEFAULT_API_URL,
807
863
  apiKey: credential?.apiKey ?? null,
808
864
  capabilities: {
809
- exec: process.env.PUGI_MCP_EXEC_ENABLED === '1',
810
- publish: process.env.PUGI_MCP_PUBLISH_ENABLED === '1',
811
- deploy: process.env.PUGI_MCP_DEPLOY_ENABLED === '1',
865
+ exec: execEnabled,
866
+ publish: publishEnabled,
867
+ deploy: deployEnabled,
812
868
  },
813
869
  sshAlias: process.env.PUGI_MCP_SSH_ALIAS ?? 'codeforge',
814
870
  };
@@ -43,7 +43,7 @@ const SUB_USAGE = [
43
43
  'pugi memory forget <id>',
44
44
  'pugi memory sync',
45
45
  ].join('\n ');
46
- const DEFAULT_PERSONA = 'mira';
46
+ const DEFAULT_PERSONA = 'pugi';
47
47
  /** Single CLI entry — top-level `pugi memory` AND the in-REPL `/memory` slash both call this. */
48
48
  export async function runMemoryCommand(args, ctx) {
49
49
  const sub = (args[0] ?? '').toLowerCase();
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Centralised deprecation warning helper (Trust Sprint item 7).
3
+ *
4
+ * Pugi CLI is in 0.x and we are introducing the first generation of
5
+ * collapsed env vars. Operators with existing scripts need a clear
6
+ * forward path without breakage. Pattern:
7
+ *
8
+ * warnDeprecation('PUGI_OLD_FLAG', 'PUGI_NEW_FLAG=1', 'rationale...');
9
+ *
10
+ * Behaviour:
11
+ * - Emits ONE stderr line per (old, new) pair per process. Repeat
12
+ * calls for the same pair are suppressed so the operator does not
13
+ * see the same noise across many tool invocations in a session.
14
+ * - Silenced entirely when `PUGI_SUPPRESS_DEPRECATION_WARNINGS=1` is
15
+ * set (CI escape hatch; documented in operator-deploy-checklist).
16
+ * - Silenced in test runs via the standard `NODE_ENV=test` /
17
+ * `PUGI_TEST_MODE=1` checks so spec output stays clean.
18
+ *
19
+ * Brand rule: no AI attribution, no em dashes. Copy stays short and
20
+ * actionable so the operator can paste the new form into their script
21
+ * in one read.
22
+ */
23
+ const warnedPairs = new Set();
24
+ /**
25
+ * Reset the warned-once memo. Tests call this between cases so the
26
+ * one-warning-per-pair guard does not bleed across specs.
27
+ */
28
+ export function resetDeprecationWarningsForTest() {
29
+ warnedPairs.clear();
30
+ }
31
+ /**
32
+ * Emit a single deprecation warning to stderr.
33
+ *
34
+ * @param oldName literal name of the deprecated env/flag the operator
35
+ * is using (e.g. 'PUGI_MCP_EXEC_ENABLED').
36
+ * @param newName literal name of the replacement env/flag, including
37
+ * the form an operator should type (e.g.
38
+ * 'PUGI_MCP_ORCHESTRATOR=1').
39
+ * @param hint optional one-line rationale appended after the
40
+ * redirect. Keep under 80 chars; multi-line wrapping
41
+ * is the operator's terminal job, not ours.
42
+ */
43
+ export function warnDeprecation(oldName, newName, hint) {
44
+ if (isSilenced())
45
+ return;
46
+ const key = `${oldName}->${newName}`;
47
+ if (warnedPairs.has(key))
48
+ return;
49
+ warnedPairs.add(key);
50
+ const baseLine = `pugi: deprecation: ${oldName} is deprecated; prefer ${newName}.`;
51
+ const line = hint && hint.length > 0 ? `${baseLine} ${hint}` : baseLine;
52
+ process.stderr.write(`${line}\n`);
53
+ }
54
+ /**
55
+ * Internal helper. Lifted so tests can mock or override.
56
+ */
57
+ function isSilenced() {
58
+ const env = process.env;
59
+ if (env.PUGI_SUPPRESS_DEPRECATION_WARNINGS === '1')
60
+ return true;
61
+ if (env.PUGI_TEST_MODE === '1')
62
+ return true;
63
+ // NODE_ENV=test is set by `node --test`'s harness and by the
64
+ // pugi-cli vitest workflows.
65
+ if (env.NODE_ENV === 'test')
66
+ return true;
67
+ return false;
68
+ }
69
+ //# sourceMappingURL=deprecation-warning.js.map
@@ -403,6 +403,10 @@ export async function runHeadlessPrint(opts) {
403
403
  // every richer event already flew through `streamEmitter` above.
404
404
  const kind = opts.kind ?? 'code';
405
405
  const taskId = `print-${Date.now()}`;
406
+ // PUGI-VERIFY-GATE: `needs_verification` is the new fourth
407
+ // status the engine adapter surfaces when a `completed` loop ran
408
+ // no verification command. Widen the local var to match the
409
+ // engine result schema.
406
410
  let finalStatus = 'failed';
407
411
  let finalSummary = '';
408
412
  let resultRisks = [];
@@ -475,14 +479,15 @@ export async function runHeadlessPrint(opts) {
475
479
  },
476
480
  });
477
481
  // 9. Emit session.end. Exit code policy per spec:
478
- // - done → 0
482
+ // - done → 0
483
+ // - needs_verification → 2 (PUGI-VERIFY-GATE)
479
484
  // - blocked + outcome=budget_exhausted → 2
480
485
  // - blocked + any other reason → 2 (turn limit, tool refused — both
481
486
  // count as "did not complete")
482
- // - failed → 1
487
+ // - failed → 1
483
488
  const exitCode = finalStatus === 'done'
484
489
  ? 0
485
- : finalStatus === 'blocked'
490
+ : finalStatus === 'blocked' || finalStatus === 'needs_verification'
486
491
  ? 2
487
492
  : 1;
488
493
  const durationMs = Date.now() - startedAt;