@pugi/cli 0.1.0-beta.96 → 0.1.0-beta.98
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/core/engine/budgets.js +9 -3
- package/dist/core/engine/intensity.js +11 -6
- package/dist/core/engine/tool-bridge.js +60 -0
- package/dist/core/permissions/tool-class.js +14 -0
- package/dist/core/repl/session.js +72 -1
- package/dist/core/subagents/dispatcher.js +14 -9
- package/dist/runtime/cli.js +27 -42
- package/dist/runtime/engine-exit-code.js +50 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tools/http-request.js +336 -0
- package/dist/tools/registry.js +21 -1
- package/dist/tools/server-tools.js +892 -0
- package/package.json +2 -2
|
@@ -75,9 +75,15 @@ export const beta1DefaultBudgets = {
|
|
|
75
75
|
// real per-call token use is ~30-40% lower than legacy. Bump headroom
|
|
76
76
|
// so multi-file refactors no longer trip the cap. Anvil clamps per-call
|
|
77
77
|
// max_tokens to 128k (PR) so the engine envelope still safe.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// CEO escalation 2026-06-05: REPL React tic-tac-toe blew budget at
|
|
79
|
+
// 120214 > 120000 mid-build. the upstream customer expectation = 200K+
|
|
80
|
+
// context per session с recharge. Bump к 400K for code/fix + raise
|
|
81
|
+
// tool-call ceilings so multi-file front-end builds (React + tests +
|
|
82
|
+
// index.css + index.html + main.jsx) fit without forcing the operator
|
|
83
|
+
// to escalate к `--intensity=deep`.
|
|
84
|
+
fix: { maxTokens: 250_000, maxToolCalls: 40 },
|
|
85
|
+
code: { maxTokens: 400_000, maxToolCalls: 50 },
|
|
86
|
+
build: { maxTokens: 500_000, maxToolCalls: 60 },
|
|
81
87
|
plan: { maxTokens: 200_000, maxToolCalls: 8 },
|
|
82
88
|
explain: { maxTokens: 60_000, maxToolCalls: 10 },
|
|
83
89
|
review_triple: { maxTokens: 100_000, maxToolCalls: 10 },
|
|
@@ -49,26 +49,31 @@ const PROFILES = {
|
|
|
49
49
|
allowParallelAgents: false,
|
|
50
50
|
maxParallelAgents: 0,
|
|
51
51
|
},
|
|
52
|
+
// CEO 2026-06-05: 80K standard exhausted React multi-file build mid-
|
|
53
|
+
// turn (120K hardcoded budget). Customers compare to the upstream = 200K
|
|
54
|
+
// context per session. Bump standard к 200K so default REPL doesn't
|
|
55
|
+
// trip mid-build; deep к 500K for complex multi-file refactors;
|
|
56
|
+
// marathon к 1.5M for long-running autonomous work.
|
|
52
57
|
standard: {
|
|
53
58
|
level: 'standard',
|
|
54
|
-
maxTurns:
|
|
55
|
-
budgetTokens:
|
|
59
|
+
maxTurns: 30,
|
|
60
|
+
budgetTokens: 200_000,
|
|
56
61
|
modelTag: 'standard',
|
|
57
62
|
allowParallelAgents: false,
|
|
58
63
|
maxParallelAgents: 0,
|
|
59
64
|
},
|
|
60
65
|
deep: {
|
|
61
66
|
level: 'deep',
|
|
62
|
-
maxTurns:
|
|
63
|
-
budgetTokens:
|
|
67
|
+
maxTurns: 80,
|
|
68
|
+
budgetTokens: 500_000,
|
|
64
69
|
modelTag: 'standard',
|
|
65
70
|
allowParallelAgents: true,
|
|
66
71
|
maxParallelAgents: 3,
|
|
67
72
|
},
|
|
68
73
|
marathon: {
|
|
69
74
|
level: 'marathon',
|
|
70
|
-
maxTurns:
|
|
71
|
-
budgetTokens:
|
|
75
|
+
maxTurns: 300,
|
|
76
|
+
budgetTokens: 1_500_000,
|
|
72
77
|
modelTag: 'heavy',
|
|
73
78
|
allowParallelAgents: true,
|
|
74
79
|
maxParallelAgents: 3,
|
|
@@ -19,6 +19,12 @@ import { dispatchEnterWorktree, enterWorktreeJsonSchema, } from '../../tools/ent
|
|
|
19
19
|
import { dispatchExitWorktree, exitWorktreeJsonSchema, } from '../../tools/exit-worktree.js';
|
|
20
20
|
import { webFetchTool } from '../../tools/web-fetch.js';
|
|
21
21
|
import { webSearchTool } from '../../tools/web-search.js';
|
|
22
|
+
// Phase 1 runtime evidence pack (PUGI-291..295): server_* + http_request
|
|
23
|
+
// dispatchers + JSON schema fragments. Every primitive returns a
|
|
24
|
+
// sentinel string on validation failure (sleep/brief convention) so
|
|
25
|
+
// the engine adapter surfaces it as a recoverable tool result.
|
|
26
|
+
import { dispatchHttpRequest, httpRequestJsonSchema, } from '../../tools/http-request.js';
|
|
27
|
+
import { dispatchServerHealth, dispatchServerLogs, dispatchServerStart, dispatchServerStop, serverHealthJsonSchema, serverLogsJsonSchema, serverStartJsonSchema, serverStopJsonSchema, } from '../../tools/server-tools.js';
|
|
22
28
|
import { agentTool } from '../../tools/agent-tool.js';
|
|
23
29
|
import { multiEdit } from '../../tools/multi-edit.js';
|
|
24
30
|
import { recordVerificationCall } from '../session.js';
|
|
@@ -217,6 +223,15 @@ const WIRED_TOOLS = new Set([
|
|
|
217
223
|
'symbols_signature',
|
|
218
224
|
'symbols_type_definition',
|
|
219
225
|
'symbols_workspace_symbols',
|
|
226
|
+
// Phase 1 runtime evidence pack (PUGI-291..295): server_* + http_request.
|
|
227
|
+
// Off by default in plan mode (mutation surface for start/stop; even
|
|
228
|
+
// health/logs/http_request cross the plan-mode read-only contract
|
|
229
|
+
// because they are runtime evidence primitives, not source reads).
|
|
230
|
+
'http_request',
|
|
231
|
+
'server_health',
|
|
232
|
+
'server_logs',
|
|
233
|
+
'server_start',
|
|
234
|
+
'server_stop',
|
|
220
235
|
]);
|
|
221
236
|
export function buildToolsSchema(kind, options = { allowFetch: false, allowSearch: false }) {
|
|
222
237
|
const planMode = kind === 'plan';
|
|
@@ -831,6 +846,31 @@ export function buildToolsSchema(kind, options = { allowFetch: false, allowSearc
|
|
|
831
846
|
},
|
|
832
847
|
},
|
|
833
848
|
},
|
|
849
|
+
},
|
|
850
|
+
// Phase 1 runtime evidence pack (PUGI-291..295) - server lifecycle
|
|
851
|
+
// primitives + generic HTTP request. Off in plan mode because each
|
|
852
|
+
// tool produces side effects (a spawned process, an outbound HTTP
|
|
853
|
+
// call) the plan-mode read-only contract refuses.
|
|
854
|
+
{
|
|
855
|
+
name: 'server_start',
|
|
856
|
+
description: 'Spawn a server via /bin/sh -c <command> in the workspace and poll the health URL until it returns the expected status (default 200) or the timeout elapses. Persists pid + meta to `.pugi/runs/<runId>/server.json` and a stdout/stderr tail to `server.log`. Returns {ok, pid, port, healthStatus, logPath, durationMs, error?}. Use to materialise concrete pid + health 200 evidence rather than asserting a server is up in prose.',
|
|
857
|
+
parameters: serverStartJsonSchema,
|
|
858
|
+
}, {
|
|
859
|
+
name: 'server_stop',
|
|
860
|
+
description: 'Send SIGTERM to the supplied pid, wait graceMs (default 5000ms), then escalate to SIGKILL. Idempotent - a pid that already exited returns ok=true with signal=undefined. Returns {ok, pid, exitCode?, signal?, durationMs, error?}.',
|
|
861
|
+
parameters: serverStopJsonSchema,
|
|
862
|
+
}, {
|
|
863
|
+
name: 'server_health',
|
|
864
|
+
description: 'One-shot HTTP GET against the supplied URL with a short timeout (default 5000ms, max 60000ms). Returns {ok, status, durationMs, body?, error?} where ok=true only when the response status equals expectStatus (default 200). Body is capped at 1 KB so the envelope stays compact.',
|
|
865
|
+
parameters: serverHealthJsonSchema,
|
|
866
|
+
}, {
|
|
867
|
+
name: 'server_logs',
|
|
868
|
+
description: 'Read the trailing N lines of `.pugi/runs/<runId>/server.log`. Defaults to the most recent run when runId is omitted; pid-based lookup walks every run dir. Returns {ok, logPath, lines, totalLines, error?}.',
|
|
869
|
+
parameters: serverLogsJsonSchema,
|
|
870
|
+
}, {
|
|
871
|
+
name: 'http_request',
|
|
872
|
+
description: 'Issue an HTTP request (GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS) against a URL. Loopback hosts (localhost / 127.0.0.0/8 / ::1) always pass; non-loopback hosts require allowExternal: true so the default posture stays private. Body objects/arrays are JSON-serialised. Returns {ok, status, headers, body, json?, durationMs, error?, truncated?}.',
|
|
873
|
+
parameters: httpRequestJsonSchema,
|
|
834
874
|
});
|
|
835
875
|
}
|
|
836
876
|
// β4 M1/M3: append MCP tools last. Plan mode skips them because every
|
|
@@ -1235,6 +1275,26 @@ export function buildExecutor(input) {
|
|
|
1235
1275
|
sessionId,
|
|
1236
1276
|
});
|
|
1237
1277
|
}
|
|
1278
|
+
// Phase 1 runtime evidence pack (PUGI-291..295). Each tool returns
|
|
1279
|
+
// a JSON-string envelope or a sentinel string on validation
|
|
1280
|
+
// failure - both paths are recoverable from the model's POV, so
|
|
1281
|
+
// the engine adapter routes them as plain tool results and the
|
|
1282
|
+
// model can self-correct.
|
|
1283
|
+
if (name === 'server_start') {
|
|
1284
|
+
return dispatchServerStart({ workspaceRoot }, args);
|
|
1285
|
+
}
|
|
1286
|
+
if (name === 'server_stop') {
|
|
1287
|
+
return dispatchServerStop({ workspaceRoot }, args);
|
|
1288
|
+
}
|
|
1289
|
+
if (name === 'server_health') {
|
|
1290
|
+
return dispatchServerHealth({ workspaceRoot }, args);
|
|
1291
|
+
}
|
|
1292
|
+
if (name === 'server_logs') {
|
|
1293
|
+
return dispatchServerLogs({ workspaceRoot }, args);
|
|
1294
|
+
}
|
|
1295
|
+
if (name === 'http_request') {
|
|
1296
|
+
return dispatchHttpRequest({}, args);
|
|
1297
|
+
}
|
|
1238
1298
|
if (name === 'multi_edit') {
|
|
1239
1299
|
return dispatchMultiEdit(args, ctx);
|
|
1240
1300
|
}
|
|
@@ -47,11 +47,25 @@ const BUILT_IN_TOOL_CLASSES = Object.freeze({
|
|
|
47
47
|
skill: 'read',
|
|
48
48
|
task_get: 'read',
|
|
49
49
|
task_list: 'read',
|
|
50
|
+
// Phase 1 runtime evidence pack (PUGI-291..295): server_health and
|
|
51
|
+
// server_logs are read-only probes (HTTP GET + tail of a log file
|
|
52
|
+
// already on disk). Classifying them as `read` keeps the ask-mode
|
|
53
|
+
// prompts honest; the dispatcher still applies the network-perm gate
|
|
54
|
+
// for server_health and the workspace-read gate for server_logs.
|
|
55
|
+
server_health: 'read',
|
|
56
|
+
server_logs: 'read',
|
|
50
57
|
// Mutating actions.
|
|
51
58
|
write: 'write',
|
|
52
59
|
edit: 'write',
|
|
53
60
|
multi_edit: 'write',
|
|
54
61
|
bash: 'write',
|
|
62
|
+
// Phase 1 runtime evidence pack: server_start spawns a process,
|
|
63
|
+
// server_stop terminates one, http_request can POST/PUT/DELETE
|
|
64
|
+
// arbitrary data against a localhost service. All three are write
|
|
65
|
+
// class so plan mode refuses and ask mode prompts.
|
|
66
|
+
server_start: 'write',
|
|
67
|
+
server_stop: 'write',
|
|
68
|
+
http_request: 'write',
|
|
55
69
|
task_create: 'write',
|
|
56
70
|
task_update: 'write',
|
|
57
71
|
todo_write: 'write',
|
|
@@ -2831,6 +2831,24 @@ export class ReplSession {
|
|
|
2831
2831
|
try {
|
|
2832
2832
|
if (useDirectEngine) {
|
|
2833
2833
|
const persona = personaSlugFor('code');
|
|
2834
|
+
// PR C (PUGI-538-FU): thread the recent conversation
|
|
2835
|
+
// into the engine prompt so multi-turn refinements work. Without
|
|
2836
|
+
// this, the engine sees only the literal current brief — a
|
|
2837
|
+
// follow-up like "react" after "сделай крестики нолики" arrives
|
|
2838
|
+
// as a bare "react" with no prior context, and the engine ships
|
|
2839
|
+
// arbitrary nonsense or asks again ("нет конкретного feature
|
|
2840
|
+
// request"). The CEO reproduction 2026-06-05 (Python tic-tac-toe
|
|
2841
|
+
// shipped когда customer wanted React браузер game, then engine
|
|
2842
|
+
// claimed "нет feature request" on the correction turn) is
|
|
2843
|
+
// exactly this gap.
|
|
2844
|
+
//
|
|
2845
|
+
// Display channels (system line, transcript) keep using the bare
|
|
2846
|
+
// `brief` for UX cleanliness; only the engine's task.prompt gets
|
|
2847
|
+
// the full conversational context via the new `enginePrompt`
|
|
2848
|
+
// field. Engine-bridge falls back to brief when enginePrompt is
|
|
2849
|
+
// undefined (server-emitted parser-built tags), preserving the
|
|
2850
|
+
// legacy behaviour for those surfaces.
|
|
2851
|
+
const enginePrompt = this.buildEnginePromptWithContext(brief);
|
|
2834
2852
|
const tag = {
|
|
2835
2853
|
command: 'code',
|
|
2836
2854
|
brief,
|
|
@@ -2842,6 +2860,7 @@ export class ReplSession {
|
|
|
2842
2860
|
signature: signatureForToolRoute('code', persona, brief),
|
|
2843
2861
|
start: 0,
|
|
2844
2862
|
end: 0,
|
|
2863
|
+
...(enginePrompt !== brief ? { enginePrompt } : {}),
|
|
2845
2864
|
};
|
|
2846
2865
|
await this.runEngineBridge(tag);
|
|
2847
2866
|
}
|
|
@@ -2862,6 +2881,54 @@ export class ReplSession {
|
|
|
2862
2881
|
this.markDispatchFailed('post_brief_failed');
|
|
2863
2882
|
}
|
|
2864
2883
|
}
|
|
2884
|
+
/**
|
|
2885
|
+
* PR C (PUGI-538-FU): build the engine prompt with recent
|
|
2886
|
+
* conversation context prepended. The current brief is preserved as
|
|
2887
|
+
* the explicit "Current request:" terminal so the engine knows what
|
|
2888
|
+
* the user is asking right now, while the prior turns give it the
|
|
2889
|
+
* stack/framework/format hints from earlier in the dialog.
|
|
2890
|
+
*
|
|
2891
|
+
* Returns `brief` unchanged when there is no prior conversation —
|
|
2892
|
+
* the empty preamble would just waste tokens.
|
|
2893
|
+
*
|
|
2894
|
+
* Window policy: last 4 conversational exchanges (operator + persona
|
|
2895
|
+
* pairs), text truncated к 400 chars per row. Drops the trailing
|
|
2896
|
+
* operator row if it matches `brief` (which has already been appended
|
|
2897
|
+
* to the transcript by `appendOperatorLine` at line 3429 above and
|
|
2898
|
+
* would otherwise duplicate inside the prompt).
|
|
2899
|
+
*
|
|
2900
|
+
* Doc strings stay в English per repo convention; the rendered
|
|
2901
|
+
* preamble uses neutral English labels ("User", "Pugi") so the
|
|
2902
|
+
* engine's model treats it as standard transcript context rather
|
|
2903
|
+
* than a localized field name.
|
|
2904
|
+
*/
|
|
2905
|
+
buildEnginePromptWithContext(brief) {
|
|
2906
|
+
const MAX_TURNS = 4;
|
|
2907
|
+
const MAX_ROW_CHARS = 400;
|
|
2908
|
+
const conversational = this.state.transcript.filter((r) => r.source === 'operator' || r.source === 'persona');
|
|
2909
|
+
if (conversational.length === 0)
|
|
2910
|
+
return brief;
|
|
2911
|
+
// Take the last MAX_TURNS * 2 rows (each turn = 1 operator + 1 persona).
|
|
2912
|
+
const recent = conversational.slice(-(MAX_TURNS * 2));
|
|
2913
|
+
// Drop trailing operator row when it equals the brief we're about
|
|
2914
|
+
// to dispatch — the brief is the "current request" and already
|
|
2915
|
+
// landed in the transcript via `appendOperatorLine` earlier in
|
|
2916
|
+
// `dispatchBrief`. Including it twice would confuse the engine.
|
|
2917
|
+
const lastRow = recent[recent.length - 1];
|
|
2918
|
+
const trimmed = lastRow && lastRow.source === 'operator' && lastRow.text === brief
|
|
2919
|
+
? recent.slice(0, -1)
|
|
2920
|
+
: recent;
|
|
2921
|
+
if (trimmed.length === 0)
|
|
2922
|
+
return brief;
|
|
2923
|
+
const lines = trimmed.map((r) => {
|
|
2924
|
+
const role = r.source === 'operator' ? 'User' : 'Pugi';
|
|
2925
|
+
const truncated = r.text.length > MAX_ROW_CHARS
|
|
2926
|
+
? r.text.slice(0, MAX_ROW_CHARS) + '...'
|
|
2927
|
+
: r.text;
|
|
2928
|
+
return `- ${role}: ${truncated}`;
|
|
2929
|
+
});
|
|
2930
|
+
return `Recent conversation:\n${lines.join('\n')}\n\nCurrent request: ${brief}`;
|
|
2931
|
+
}
|
|
2865
2932
|
/**
|
|
2866
2933
|
* : reset the FSM to `idle` after a terminal transition so the
|
|
2867
2934
|
* next brief can start. The FSM does not allow direct
|
|
@@ -4137,7 +4204,11 @@ export class ReplSession {
|
|
|
4137
4204
|
result = await bridge({
|
|
4138
4205
|
command: tag.command,
|
|
4139
4206
|
persona: tag.persona,
|
|
4140
|
-
|
|
4207
|
+
// PR C (PUGI-538-FU): prefer the contextualized
|
|
4208
|
+
// engine prompt when the direct-engine path set it. Falls back
|
|
4209
|
+
// к the bare brief for parser-built tags from the server-emitted
|
|
4210
|
+
// envelope path (no conversation context available there).
|
|
4211
|
+
brief: tag.enginePrompt ?? tag.brief,
|
|
4141
4212
|
bridgeId,
|
|
4142
4213
|
signal: abort.signal,
|
|
4143
4214
|
onEvent,
|
|
@@ -142,16 +142,21 @@ const DENY_ALL_WRITES_READONLY = Object.freeze([
|
|
|
142
142
|
/* ------------------------------------------------------------------ */
|
|
143
143
|
/* Default budgets */
|
|
144
144
|
/* ------------------------------------------------------------------ */
|
|
145
|
+
// CEO escalation 2026-06-05: 120K coder budget exhausted mid-React-
|
|
146
|
+
// build (120214 > 120000). Match the engine-level `code` task bump
|
|
147
|
+
// (apps/pugi-cli/src/core/engine/budgets.ts:149 — 400K). Subagent
|
|
148
|
+
// dispatches inherit the upstream caller's headroom, so this needs
|
|
149
|
+
// to track the engine envelope.
|
|
145
150
|
const DEFAULT_BUDGETS = Object.freeze({
|
|
146
|
-
orchestrator: { tokens:
|
|
147
|
-
architect: { tokens:
|
|
148
|
-
coder: { tokens:
|
|
149
|
-
verifier: { tokens:
|
|
150
|
-
reviewer: { tokens:
|
|
151
|
-
researcher: { tokens:
|
|
152
|
-
release: { tokens:
|
|
153
|
-
devops: { tokens:
|
|
154
|
-
design_qa: { tokens:
|
|
151
|
+
orchestrator: { tokens: 400_000, dollars: 8, wallClockMs: 900_000 },
|
|
152
|
+
architect: { tokens: 200_000, dollars: 4, wallClockMs: 600_000 },
|
|
153
|
+
coder: { tokens: 400_000, dollars: 8, wallClockMs: 900_000 },
|
|
154
|
+
verifier: { tokens: 150_000, dollars: 3, wallClockMs: 600_000 },
|
|
155
|
+
reviewer: { tokens: 200_000, dollars: 4, wallClockMs: 600_000 },
|
|
156
|
+
researcher: { tokens: 150_000, dollars: 3, wallClockMs: 600_000 },
|
|
157
|
+
release: { tokens: 80_000, dollars: 2, wallClockMs: 300_000 },
|
|
158
|
+
devops: { tokens: 150_000, dollars: 3, wallClockMs: 600_000 },
|
|
159
|
+
design_qa: { tokens: 150_000, dollars: 3, wallClockMs: 600_000 },
|
|
155
160
|
});
|
|
156
161
|
/**
|
|
157
162
|
* Resolve the effective budget for a dispatch by merging task overrides
|
package/dist/runtime/cli.js
CHANGED
|
@@ -5021,48 +5021,26 @@ function renderLocalSessionList(rows, projectSlug) {
|
|
|
5021
5021
|
* 1 is reserved for the credential gate (engine_unavailable) so
|
|
5022
5022
|
* existing shell wrappers that branch on "any non-zero" still work.
|
|
5023
5023
|
*/
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
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
|
-
}
|
|
5024
|
+
// PUGI-VERIFY-GATE - Codex dogfood 2026-06-04 surfaced a P0 where
|
|
5025
|
+
// `pugi code` returned exit 0 while npm test failed. The spec locks
|
|
5026
|
+
// the contract to exit 1 on a verification failure AND exit 2 on
|
|
5027
|
+
// `needs_verification`. Legacy callers reading exit 8/9 still see
|
|
5028
|
+
// "any non-zero = failure" but the new codes (1/2) are the
|
|
5029
|
+
// authoritative signal for the verification gate.
|
|
5030
|
+
//
|
|
5031
|
+
// Phase 1 (PUGI-299) - runtime invariant additions:
|
|
5032
|
+
// 3. `verificationFailures` non-empty array → exit 1
|
|
5033
|
+
// 4. `verified === false && status === 'done'` → exit 2
|
|
5034
|
+
//
|
|
5035
|
+
// The two new rules are defensive: today `computeVerificationOutcome`
|
|
5036
|
+
// always populates `unverifiedReason` (rule 2) or downgrades the
|
|
5037
|
+
// status (rule 1) so the new branches never fire, but the invariant
|
|
5038
|
+
// locks the contract for future engine adapters.
|
|
5039
|
+
//
|
|
5040
|
+
// Implementation lives in `engine-exit-code.ts` so the runtime spec
|
|
5041
|
+
// (`test/engine-exit-code.spec.ts`) can exercise it without mounting
|
|
5042
|
+
// this 9700-LOC entry module.
|
|
5043
|
+
import { ENGINE_EXIT_CODES, resolveEngineExitCode, } from './engine-exit-code.js';
|
|
5066
5044
|
function commandLabel(kind) {
|
|
5067
5045
|
return kind === 'build_task' ? 'build' : kind;
|
|
5068
5046
|
}
|
|
@@ -8132,5 +8110,12 @@ export const __test__ = {
|
|
|
8132
8110
|
// env-driven $PUGI_HOME redirect) without going через the full
|
|
8133
8111
|
// engine-task dispatch path.
|
|
8134
8112
|
readPersistedContextTier,
|
|
8113
|
+
// Phase 1 (PUGI-299) - runtime verification gate exit-code resolver.
|
|
8114
|
+
// Re-exported from `engine-exit-code.ts` so the invariant spec can
|
|
8115
|
+
// assert that a `verificationFailures` non-empty array OR a
|
|
8116
|
+
// `verified=false` outcome MUST surface as a non-zero CLI exit
|
|
8117
|
+
// even when a malformed adapter forgets to flip the status.
|
|
8118
|
+
resolveEngineExitCode,
|
|
8119
|
+
ENGINE_EXIT_CODES,
|
|
8135
8120
|
};
|
|
8136
8121
|
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine-exit-code - Phase 1 verification gate invariant (PUGI-299).
|
|
3
|
+
*
|
|
4
|
+
* The runtime resolves the CLI exit code from the engine outcome via a
|
|
5
|
+
* deterministic ladder. Extracted from `cli.ts` so the invariant is
|
|
6
|
+
* exercisable from a focused spec without mounting the 9700-LOC entry
|
|
7
|
+
* module.
|
|
8
|
+
*
|
|
9
|
+
* Contract (in priority order):
|
|
10
|
+
* 1. `status === 'needs_verification'` → exit 2
|
|
11
|
+
* 2. `unverifiedReason === 'verification_command_failed'` → exit 1
|
|
12
|
+
* 3. `verificationFailures` is a non-empty array → exit 1
|
|
13
|
+
* 4. `verified === false && status === 'done'` → exit 2
|
|
14
|
+
* 5. `status === 'done'` → exit 0
|
|
15
|
+
* 6. `status === 'failed'` → exit 8
|
|
16
|
+
* 7. `status === 'blocked'` (catch-all) → exit 9
|
|
17
|
+
*
|
|
18
|
+
* The rule fires defensively: rules 3 + 4 cover producer bugs where a
|
|
19
|
+
* future engine adapter forgets to flip `unverifiedReason` or
|
|
20
|
+
* downgrade the status to `needs_verification`. Today
|
|
21
|
+
* `computeVerificationOutcome` always synthesises one of the two,
|
|
22
|
+
* but the invariant locks the contract.
|
|
23
|
+
*/
|
|
24
|
+
export const ENGINE_EXIT_CODES = {
|
|
25
|
+
done: 0,
|
|
26
|
+
failed: 8,
|
|
27
|
+
blocked: 9,
|
|
28
|
+
engine_unavailable: 1,
|
|
29
|
+
needs_verification: 2,
|
|
30
|
+
};
|
|
31
|
+
export function resolveEngineExitCode(result) {
|
|
32
|
+
if (result.status === 'needs_verification') {
|
|
33
|
+
return ENGINE_EXIT_CODES.needs_verification;
|
|
34
|
+
}
|
|
35
|
+
if (result.unverifiedReason === 'verification_command_failed') {
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(result.verificationFailures) && result.verificationFailures.length > 0) {
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
if (result.verified === false && result.status === 'done') {
|
|
42
|
+
return ENGINE_EXIT_CODES.needs_verification;
|
|
43
|
+
}
|
|
44
|
+
if (result.status === 'done')
|
|
45
|
+
return ENGINE_EXIT_CODES.done;
|
|
46
|
+
if (result.status === 'failed')
|
|
47
|
+
return ENGINE_EXIT_CODES.failed;
|
|
48
|
+
return ENGINE_EXIT_CODES.blocked;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=engine-exit-code.js.map
|
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.98');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|