@pugi/cli 0.1.0-beta.93 → 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.
- package/dist/commands/retro.js +210 -0
- package/dist/core/diagnostics/probes/sandbox.js +65 -33
- package/dist/core/engine/native-pugi.js +184 -10
- package/dist/core/engine/tool-bridge.js +35 -0
- package/dist/core/engine/verification-patterns.js +9 -9
- package/dist/core/mcp/orchestrator-config.js +192 -0
- package/dist/core/mcp/orchestrator-tools.js +147 -3
- package/dist/core/pugi-gitignore.js +52 -0
- package/dist/core/repl/engine-bridge.js +199 -0
- package/dist/core/repl/session.js +395 -6
- package/dist/core/repl/tool-route.js +382 -0
- package/dist/core/retro/git-collector.js +251 -0
- package/dist/core/retro/health-card.js +25 -0
- package/dist/core/retro/metrics.js +342 -0
- package/dist/core/retro/narrative.js +249 -0
- package/dist/core/retro/plane-collector.js +274 -0
- package/dist/core/retro/pr-issue-link.js +65 -0
- package/dist/core/retro/types.js +16 -0
- package/dist/core/sandboxing/adapter.js +29 -0
- package/dist/core/sandboxing/index.js +49 -0
- package/dist/core/sandboxing/none.js +19 -0
- package/dist/core/sandboxing/seatbelt.js +183 -0
- package/dist/core/session.js +27 -0
- package/dist/core/settings.js +22 -0
- package/dist/runtime/cli.js +167 -33
- package/dist/runtime/commands/mcp.js +64 -8
- package/dist/runtime/deprecation-warning.js +69 -0
- package/dist/runtime/headless.js +8 -3
- package/dist/runtime/stream-renderer.js +195 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tui/agent-tree.js +11 -0
- package/dist/tui/ask-user-question-chips.js +1 -1
- package/dist/tui/multi-file-diff-approval.js +3 -3
- package/dist/tui/repl-render.js +42 -0
- package/package.json +2 -2
package/dist/core/settings.js
CHANGED
|
@@ -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.
|
package/dist/runtime/cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
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
|
|
@@ -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
|
-
'
|
|
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
|
-
|
|
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
|
|
861
|
+
pugiBin,
|
|
806
862
|
apiUrl: credential?.apiUrl ?? DEFAULT_API_URL,
|
|
807
863
|
apiKey: credential?.apiKey ?? null,
|
|
808
864
|
capabilities: {
|
|
809
|
-
exec:
|
|
810
|
-
publish:
|
|
811
|
-
deploy:
|
|
865
|
+
exec: execEnabled,
|
|
866
|
+
publish: publishEnabled,
|
|
867
|
+
deploy: deployEnabled,
|
|
812
868
|
},
|
|
813
869
|
sshAlias: process.env.PUGI_MCP_SSH_ALIAS ?? 'codeforge',
|
|
814
870
|
};
|
|
@@ -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
|
package/dist/runtime/headless.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|