@bridge_gpt/mcp-server 0.2.36 → 0.2.38
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/README.md +48 -8
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-user-config-doctor.js +317 -0
- package/build/commands.generated.js +2 -1
- package/build/conductor/bridge-api-client.js +178 -4
- package/build/conductor-bin.js +1 -1
- package/build/conductor-bundle-artifacts.js +7 -6
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +239 -80
- package/build/executor/cli.js +51 -1
- package/build/executor/credentials.js +1 -7
- package/build/executor/deps.js +18 -1
- package/build/executor/env.js +51 -25
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +422 -22
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +104 -8
- package/build/executor/preflight.js +32 -0
- package/build/executor/runner.js +8 -0
- package/build/executor/test-clock.js +67 -3
- package/build/executor/types.js +4 -1
- package/build/executor/worker-command.js +11 -3
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +614 -244
- package/build/init.js +363 -73
- package/build/install-bridge.js +568 -80
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +30 -7
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +22 -9
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -0
- package/build/setup-epic.js +662 -27
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +70 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-body.js +92 -0
- package/build/sfcc/read-projection.js +185 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +57 -34
- package/build/sfcc/reads-site-preference.js +86 -33
- package/build/sfcc/reads-system-object.js +50 -38
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +71 -15
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-custom-object-def.js +6 -2
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +417 -101
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +23 -8
- package/package.json +3 -3
- package/pipelines/implement-ticket.json +15 -5
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BAPI-763 — plan step routing and the durable execution ledger.
|
|
3
|
+
*
|
|
4
|
+
* Plan generation commissions several agents and concatenates their output into one
|
|
5
|
+
* flat markdown plan. Two of those agents exist *only* to author verification work:
|
|
6
|
+
* the reviewer's grep-driven checks and its scored rendered-UI remediation loop.
|
|
7
|
+
* Neither was ever executed. The production phase deferred everything that
|
|
8
|
+
* "validates rather than produces", and the post-PR verification phase had a fixed
|
|
9
|
+
* depth — builds, touched-area tests, scoped lint — that none of those steps fit.
|
|
10
|
+
* The work fell through the gap in silence. That is Miss 20.
|
|
11
|
+
*
|
|
12
|
+
* This module is the part of the fix that makes silence impossible:
|
|
13
|
+
*
|
|
14
|
+
* - **Routing is derived, not guessed.** {@link resolveOwnedSteps} answers "which
|
|
15
|
+
* steps does this phase own?" from provenance metadata assembled in code, rather
|
|
16
|
+
* than from keyword-matching over plan prose.
|
|
17
|
+
* - **Coverage is asserted.** {@link assertStepClassCoverage} fails when any class of
|
|
18
|
+
* step a planner can emit has no executing phase. Run against the pre-BAPI-763
|
|
19
|
+
* ownership registry it fails on the review and rendered-UI classes — that failing
|
|
20
|
+
* assertion is the regression test for Miss 20.
|
|
21
|
+
* - **Every owned step gets a terminal disposition.** {@link validatePhaseResult}
|
|
22
|
+
* rejects a phase result that leaves an owned step unsettled. There is no
|
|
23
|
+
* "skipped": a step is executed, mechanically adapted, escalated, or declared
|
|
24
|
+
* advisory in the plan itself.
|
|
25
|
+
* - **The record is durable and monotonic.** {@link mergePlanStepRecords} keeps
|
|
26
|
+
* chronological evidence and refuses to silently downgrade or drop a settled step,
|
|
27
|
+
* so the record survives an approval pause or a resumed session — the conversational
|
|
28
|
+
* hand-off it replaces did not.
|
|
29
|
+
*
|
|
30
|
+
* Pure domain logic: no I/O, no git, no network, no filesystem, no MCP transport.
|
|
31
|
+
*/
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Vocabulary — closed sets, mirrored from src/python/llms/agents/code_writer/
|
|
34
|
+
// plan_provenance.py. Both sides must agree; the Python side assigns the classes
|
|
35
|
+
// and this side routes on them.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
export const PLAN_METADATA_VERSION = 1;
|
|
38
|
+
export const PLAN_PROVENANCE_CLASSES = [
|
|
39
|
+
"implementation",
|
|
40
|
+
"documentation",
|
|
41
|
+
"unit_tests",
|
|
42
|
+
"e2e_tests",
|
|
43
|
+
"rendered_ui_review",
|
|
44
|
+
"test_gap_review",
|
|
45
|
+
"final_plan_review",
|
|
46
|
+
];
|
|
47
|
+
export const PLAN_PHASES = [
|
|
48
|
+
"produce",
|
|
49
|
+
"pre_pr_verification",
|
|
50
|
+
"post_pr_gap_close",
|
|
51
|
+
];
|
|
52
|
+
export const PLAN_STEP_DISPOSITIONS = [
|
|
53
|
+
"executed",
|
|
54
|
+
"adapted",
|
|
55
|
+
"escalated",
|
|
56
|
+
"unrun-advisory",
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* The only three corrections a phase may make on its own authority.
|
|
60
|
+
*
|
|
61
|
+
* The bound is deliberately mechanical. "Preserve the objective" was considered and
|
|
62
|
+
* rejected as too permissive: it licenses an agent to decide what a step was really
|
|
63
|
+
* for and substitute its own approach, which is indistinguishable from ignoring the
|
|
64
|
+
* step. These three are corrections to how a step is *addressed*, never to what it
|
|
65
|
+
* asks for.
|
|
66
|
+
*/
|
|
67
|
+
export const MECHANICAL_ADAPTATION_KINDS = [
|
|
68
|
+
"locator-correction",
|
|
69
|
+
"repository-command-correction",
|
|
70
|
+
"equivalent-implementation-recognized",
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Change categories that may never be adapted. Meeting one of these means the step's
|
|
74
|
+
* premise is wrong in a way only a human should resolve, so the run escalates.
|
|
75
|
+
*/
|
|
76
|
+
export const ESCALATION_ONLY_CATEGORIES = [
|
|
77
|
+
"design",
|
|
78
|
+
"schema",
|
|
79
|
+
"public-api",
|
|
80
|
+
"dependencies",
|
|
81
|
+
"security",
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Which phase must settle each class of step.
|
|
85
|
+
*
|
|
86
|
+
* This registry IS the fix. Before BAPI-763 the entries below for the two review
|
|
87
|
+
* classes and the rendered-UI class did not exist in any form — no phase claimed
|
|
88
|
+
* them, and nothing noticed.
|
|
89
|
+
*
|
|
90
|
+
* Ownership here means "must record a terminal disposition for every step in the
|
|
91
|
+
* class". It is not the same as "is the only phase that touches those steps": the
|
|
92
|
+
* produce phase authors every artifact the plan calls for, including the test files
|
|
93
|
+
* a test-planner step describes. What produce does not do is *run* them, so the
|
|
94
|
+
* phase that settles a test step is the one that executes it.
|
|
95
|
+
*/
|
|
96
|
+
export const PLAN_CLASS_OWNERSHIP = Object.freeze({
|
|
97
|
+
implementation: "produce",
|
|
98
|
+
documentation: "produce",
|
|
99
|
+
// Authored during produce; settled by the phase that actually runs them. The
|
|
100
|
+
// test planners are required to emit a final execution step ("one command per
|
|
101
|
+
// test"), which is verification work by construction.
|
|
102
|
+
unit_tests: "pre_pr_verification",
|
|
103
|
+
e2e_tests: "pre_pr_verification",
|
|
104
|
+
// The remediation loop CHANGES code. Settling it after the commit gate would
|
|
105
|
+
// guarantee the pull request's first head carried the un-iterated draft, so it
|
|
106
|
+
// must land before the PR opens.
|
|
107
|
+
rendered_ui_review: "pre_pr_verification",
|
|
108
|
+
test_gap_review: "pre_pr_verification",
|
|
109
|
+
final_plan_review: "pre_pr_verification",
|
|
110
|
+
});
|
|
111
|
+
export const PRE_PR_VERIFICATION_LIMITS = Object.freeze({
|
|
112
|
+
maxCorrectionTurns: 3,
|
|
113
|
+
maxChangedFiles: 40,
|
|
114
|
+
maxDiffLines: 2000,
|
|
115
|
+
});
|
|
116
|
+
/** The plan-authored rendered-UI remediation loop's hard cycle cap. */
|
|
117
|
+
export const RENDERED_UI_MAX_CYCLES = 3;
|
|
118
|
+
export class PlanLedgerError extends Error {
|
|
119
|
+
constructor(message) {
|
|
120
|
+
super(message);
|
|
121
|
+
this.name = "PlanLedgerError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Metadata validation
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
function isPlainObject(value) {
|
|
128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
|
+
}
|
|
130
|
+
function isPositiveInteger(value) {
|
|
131
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Validate a stored provenance envelope, returning it narrowed.
|
|
135
|
+
*
|
|
136
|
+
* Throws on every malformed shape rather than repairing one. A repaired envelope
|
|
137
|
+
* routes *something*, and something plausible-but-wrong is harder to notice than a
|
|
138
|
+
* loud failure — which is how the original defect survived.
|
|
139
|
+
*/
|
|
140
|
+
export function validatePlanMetadata(value) {
|
|
141
|
+
if (value === null || value === undefined) {
|
|
142
|
+
throw new PlanLedgerError("plan metadata is absent; routing requires provenance and must not be inferred from plan prose");
|
|
143
|
+
}
|
|
144
|
+
if (!isPlainObject(value)) {
|
|
145
|
+
throw new PlanLedgerError(`plan metadata must be an object, received ${Array.isArray(value) ? "array" : typeof value}`);
|
|
146
|
+
}
|
|
147
|
+
if (value.version !== PLAN_METADATA_VERSION) {
|
|
148
|
+
throw new PlanLedgerError(`unsupported plan metadata version ${String(value.version)}; expected ${PLAN_METADATA_VERSION}`);
|
|
149
|
+
}
|
|
150
|
+
const rawParts = value.parts;
|
|
151
|
+
if (!Array.isArray(rawParts) || rawParts.length === 0) {
|
|
152
|
+
throw new PlanLedgerError("plan metadata must carry a non-empty parts array");
|
|
153
|
+
}
|
|
154
|
+
const parts = [];
|
|
155
|
+
const seenIds = new Set();
|
|
156
|
+
let previousEnd = 0;
|
|
157
|
+
let previousId = "";
|
|
158
|
+
for (const [index, raw] of rawParts.entries()) {
|
|
159
|
+
if (!isPlainObject(raw)) {
|
|
160
|
+
throw new PlanLedgerError(`plan metadata part at index ${index} is not an object`);
|
|
161
|
+
}
|
|
162
|
+
const partId = raw.part_id;
|
|
163
|
+
if (typeof partId !== "string" || partId.trim() === "") {
|
|
164
|
+
throw new PlanLedgerError(`plan metadata part at index ${index} has an empty part_id`);
|
|
165
|
+
}
|
|
166
|
+
if (seenIds.has(partId)) {
|
|
167
|
+
throw new PlanLedgerError(`duplicate plan metadata part_id '${partId}'`);
|
|
168
|
+
}
|
|
169
|
+
seenIds.add(partId);
|
|
170
|
+
const producer = raw.producer;
|
|
171
|
+
if (typeof producer !== "string" || producer.trim() === "") {
|
|
172
|
+
throw new PlanLedgerError(`plan metadata part '${partId}' has an empty producer`);
|
|
173
|
+
}
|
|
174
|
+
const provenanceClass = raw.provenance_class;
|
|
175
|
+
if (typeof provenanceClass !== "string" ||
|
|
176
|
+
!PLAN_PROVENANCE_CLASSES.includes(provenanceClass)) {
|
|
177
|
+
throw new PlanLedgerError(`plan metadata part '${partId}' has unknown provenance class '${String(provenanceClass)}'`);
|
|
178
|
+
}
|
|
179
|
+
const startStep = raw.start_step;
|
|
180
|
+
const endStep = raw.end_step;
|
|
181
|
+
if (!isPositiveInteger(startStep)) {
|
|
182
|
+
throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive start_step ${String(startStep)}`);
|
|
183
|
+
}
|
|
184
|
+
if (!isPositiveInteger(endStep)) {
|
|
185
|
+
throw new PlanLedgerError(`plan metadata part '${partId}' has a non-integer or non-positive end_step ${String(endStep)}`);
|
|
186
|
+
}
|
|
187
|
+
if (endStep < startStep) {
|
|
188
|
+
throw new PlanLedgerError(`plan metadata part '${partId}' has a reversed range ${startStep}-${endStep}`);
|
|
189
|
+
}
|
|
190
|
+
if (startStep <= previousEnd) {
|
|
191
|
+
throw new PlanLedgerError(`plan metadata part '${partId}' range ${startStep}-${endStep} overlaps or precedes '${previousId}' ending at ${previousEnd}; ranges must be disjoint and ascending`);
|
|
192
|
+
}
|
|
193
|
+
if (startStep > previousEnd + 1) {
|
|
194
|
+
// A gap is as dangerous as an overlap and far less visible. Routing iterates
|
|
195
|
+
// only the declared ranges, so a step falling in the gap is owned by no phase
|
|
196
|
+
// and settled by nobody — Miss 20's shape, reached by arithmetic rather than by
|
|
197
|
+
// a missing registry entry. Rejected here, before any phase runs.
|
|
198
|
+
throw new PlanLedgerError(`plan metadata part '${partId}' starts at step ${startStep} but '${previousId}' ended at ${previousEnd}; steps ${previousEnd + 1}-${startStep - 1} are claimed by no part and would be executed by no phase`);
|
|
199
|
+
}
|
|
200
|
+
previousEnd = endStep;
|
|
201
|
+
previousId = partId;
|
|
202
|
+
parts.push({
|
|
203
|
+
part_id: partId,
|
|
204
|
+
producer,
|
|
205
|
+
provenance_class: provenanceClass,
|
|
206
|
+
start_step: startStep,
|
|
207
|
+
end_step: endStep,
|
|
208
|
+
declared_advisory: raw.declared_advisory === true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const totalSteps = value.total_steps;
|
|
212
|
+
if (typeof totalSteps !== "number" || !Number.isInteger(totalSteps)) {
|
|
213
|
+
throw new PlanLedgerError("plan metadata total_steps must be an integer");
|
|
214
|
+
}
|
|
215
|
+
if (totalSteps < previousEnd) {
|
|
216
|
+
throw new PlanLedgerError(`plan metadata total_steps ${totalSteps} is below the highest declared step ${previousEnd}`);
|
|
217
|
+
}
|
|
218
|
+
const declaredClasses = value.provenance_classes;
|
|
219
|
+
const derivedClasses = [];
|
|
220
|
+
for (const part of parts) {
|
|
221
|
+
if (!derivedClasses.includes(part.provenance_class)) {
|
|
222
|
+
derivedClasses.push(part.provenance_class);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(declaredClasses)) {
|
|
226
|
+
for (const declared of declaredClasses) {
|
|
227
|
+
if (!derivedClasses.includes(declared)) {
|
|
228
|
+
throw new PlanLedgerError(`plan metadata declares provenance class '${String(declared)}' that no part produces`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
version: PLAN_METADATA_VERSION,
|
|
234
|
+
parts,
|
|
235
|
+
provenance_classes: derivedClasses,
|
|
236
|
+
total_steps: totalSteps,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Routing
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
/** Inclusive step IDs owned by `phase`, in plan order. */
|
|
243
|
+
export function resolveOwnedSteps(metadata, phase) {
|
|
244
|
+
if (!PLAN_PHASES.includes(phase)) {
|
|
245
|
+
throw new PlanLedgerError(`unknown phase '${phase}'`);
|
|
246
|
+
}
|
|
247
|
+
const owned = [];
|
|
248
|
+
for (const part of metadata.parts) {
|
|
249
|
+
if (PLAN_CLASS_OWNERSHIP[part.provenance_class] !== phase)
|
|
250
|
+
continue;
|
|
251
|
+
for (let step = part.start_step; step <= part.end_step; step += 1) {
|
|
252
|
+
owned.push(step);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return owned;
|
|
256
|
+
}
|
|
257
|
+
/** The parts owned by `phase`, so a phase can report ranges rather than bare IDs. */
|
|
258
|
+
export function resolveOwnedParts(metadata, phase) {
|
|
259
|
+
return metadata.parts.filter((part) => PLAN_CLASS_OWNERSHIP[part.provenance_class] === phase);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Fail unless every provenance class in the plan is claimed by an executing phase or
|
|
263
|
+
* explicitly declared advisory.
|
|
264
|
+
*
|
|
265
|
+
* `ownership` is injectable so a test can prove this assertion catches the historical
|
|
266
|
+
* defect: pass the pre-BAPI-763 registry (implementation and documentation only) and
|
|
267
|
+
* it fails on the review and rendered-UI classes.
|
|
268
|
+
*/
|
|
269
|
+
export function assertStepClassCoverage(metadata, ownership = PLAN_CLASS_OWNERSHIP) {
|
|
270
|
+
const unowned = [];
|
|
271
|
+
for (const part of metadata.parts) {
|
|
272
|
+
const owner = ownership[part.provenance_class];
|
|
273
|
+
if (owner === undefined) {
|
|
274
|
+
// Declared advisory is the ONLY sanctioned way for a class to go unexecuted,
|
|
275
|
+
// and it requires the plan to have said so. A phase's inability to run
|
|
276
|
+
// something never makes it advisory — that is how the gap stayed silent.
|
|
277
|
+
if (part.declared_advisory)
|
|
278
|
+
continue;
|
|
279
|
+
unowned.push(`class '${part.provenance_class}' (part '${part.part_id}', steps ${part.start_step}-${part.end_step}) has no executing phase and is not declared advisory`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (!PLAN_PHASES.includes(owner)) {
|
|
283
|
+
unowned.push(`class '${part.provenance_class}' is mapped to unknown phase '${String(owner)}'`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (unowned.length > 0) {
|
|
287
|
+
throw new PlanLedgerError(`plan step-class coverage failed — every class a planner can emit must be executed by some phase or declared advisory in the plan:\n ${unowned.join("\n ")}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Phase results
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
function validateRecordShape(record, metadata) {
|
|
294
|
+
if (!isPositiveInteger(record.stepId)) {
|
|
295
|
+
throw new PlanLedgerError(`ledger record has a non-positive stepId ${String(record.stepId)}`);
|
|
296
|
+
}
|
|
297
|
+
if (!PLAN_STEP_DISPOSITIONS.includes(record.disposition)) {
|
|
298
|
+
throw new PlanLedgerError(`step ${record.stepId} has unsupported disposition '${String(record.disposition)}'; expected one of ${PLAN_STEP_DISPOSITIONS.join(", ")}`);
|
|
299
|
+
}
|
|
300
|
+
if (record.disposition === "adapted") {
|
|
301
|
+
const adaptation = record.adaptation;
|
|
302
|
+
if (!adaptation) {
|
|
303
|
+
throw new PlanLedgerError(`step ${record.stepId} is 'adapted' but carries no adaptation record`);
|
|
304
|
+
}
|
|
305
|
+
if (!MECHANICAL_ADAPTATION_KINDS.includes(adaptation.kind)) {
|
|
306
|
+
throw new PlanLedgerError(`step ${record.stepId} uses unapproved adaptation kind '${String(adaptation.kind)}'; only ${MECHANICAL_ADAPTATION_KINDS.join(", ")} are mechanical — anything touching ${ESCALATION_ONLY_CATEGORIES.join(", ")} must escalate`);
|
|
307
|
+
}
|
|
308
|
+
for (const field of ["originalPremise", "correction", "rationale"]) {
|
|
309
|
+
const value = adaptation[field];
|
|
310
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
311
|
+
throw new PlanLedgerError(`step ${record.stepId} adaptation is missing required evidence '${field}'`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (record.adaptation) {
|
|
316
|
+
throw new PlanLedgerError(`step ${record.stepId} carries an adaptation record but its disposition is '${record.disposition}'`);
|
|
317
|
+
}
|
|
318
|
+
if (record.disposition === "escalated") {
|
|
319
|
+
if (typeof record.escalationReason !== "string" || record.escalationReason.trim() === "") {
|
|
320
|
+
throw new PlanLedgerError(`step ${record.stepId} is 'escalated' but gives no blocking reason; an escalation must never read as completion`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (record.disposition === "unrun-advisory") {
|
|
324
|
+
const owningPart = metadata.parts.find((part) => record.stepId >= part.start_step && record.stepId <= part.end_step);
|
|
325
|
+
if (!owningPart || !owningPart.declared_advisory) {
|
|
326
|
+
throw new PlanLedgerError(`step ${record.stepId} claims 'unrun-advisory' but its plan part is not declared advisory; advisory status must be declared in the plan, never asserted by the phase that could not run it`);
|
|
327
|
+
}
|
|
328
|
+
if (typeof record.advisoryExplanation !== "string" ||
|
|
329
|
+
record.advisoryExplanation.trim() === "") {
|
|
330
|
+
throw new PlanLedgerError(`step ${record.stepId} is 'unrun-advisory' but gives no explanation of what was not run`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (record.renderedUi) {
|
|
334
|
+
const { cycles, cycleCount } = record.renderedUi;
|
|
335
|
+
if (!Array.isArray(cycles) || cycles.length === 0) {
|
|
336
|
+
throw new PlanLedgerError(`step ${record.stepId} reports rendered-UI work with no cycle rubric; the per-cycle table is the evidence that the loop actually ran`);
|
|
337
|
+
}
|
|
338
|
+
if (!isPositiveInteger(cycleCount) || cycleCount > RENDERED_UI_MAX_CYCLES) {
|
|
339
|
+
throw new PlanLedgerError(`step ${record.stepId} reports ${String(cycleCount)} rendered-UI cycles; the plan's cap is ${RENDERED_UI_MAX_CYCLES}`);
|
|
340
|
+
}
|
|
341
|
+
for (const cycle of cycles) {
|
|
342
|
+
if (!Array.isArray(cycle.rubric) || cycle.rubric.length === 0) {
|
|
343
|
+
throw new PlanLedgerError(`step ${record.stepId} rendered-UI cycle ${String(cycle.cycle)} has an empty rubric`);
|
|
344
|
+
}
|
|
345
|
+
if (!Array.isArray(cycle.acceptedFixes)) {
|
|
346
|
+
throw new PlanLedgerError(`step ${record.stepId} rendered-UI cycle ${String(cycle.cycle)} is missing its acceptedFixes list`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Validate one phase's reported outcome against the steps it owns.
|
|
353
|
+
*
|
|
354
|
+
* Every owned step must be settled exactly once. An omission is the historical bug
|
|
355
|
+
* (work quietly not done), a duplicate makes the record ambiguous, and an unowned
|
|
356
|
+
* step means the phase acted outside its routing.
|
|
357
|
+
*/
|
|
358
|
+
export function validatePhaseResult(result, metadata, phase) {
|
|
359
|
+
if (!result || typeof result !== "object") {
|
|
360
|
+
throw new PlanLedgerError("phase result is missing");
|
|
361
|
+
}
|
|
362
|
+
if (result.version !== PLAN_METADATA_VERSION) {
|
|
363
|
+
throw new PlanLedgerError(`unsupported phase result version ${String(result.version)}; expected ${PLAN_METADATA_VERSION}`);
|
|
364
|
+
}
|
|
365
|
+
if (result.phase !== phase) {
|
|
366
|
+
throw new PlanLedgerError(`phase result declares phase '${String(result.phase)}' but was produced for '${phase}'`);
|
|
367
|
+
}
|
|
368
|
+
if (!Array.isArray(result.records)) {
|
|
369
|
+
throw new PlanLedgerError(`phase '${phase}' result has no records array`);
|
|
370
|
+
}
|
|
371
|
+
const owned = new Set(resolveOwnedSteps(metadata, phase));
|
|
372
|
+
const settled = new Set();
|
|
373
|
+
for (const record of result.records) {
|
|
374
|
+
validateRecordShape(record, metadata);
|
|
375
|
+
if (!owned.has(record.stepId)) {
|
|
376
|
+
throw new PlanLedgerError(`phase '${phase}' reported step ${record.stepId}, which it does not own`);
|
|
377
|
+
}
|
|
378
|
+
if (settled.has(record.stepId)) {
|
|
379
|
+
throw new PlanLedgerError(`phase '${phase}' reported step ${record.stepId} more than once`);
|
|
380
|
+
}
|
|
381
|
+
settled.add(record.stepId);
|
|
382
|
+
}
|
|
383
|
+
const missing = [...owned].filter((step) => !settled.has(step)).sort((a, b) => a - b);
|
|
384
|
+
if (missing.length > 0) {
|
|
385
|
+
throw new PlanLedgerError(`phase '${phase}' left owned steps unsettled: ${missing.join(", ")}. Every owned step must end as executed, adapted, escalated, or declared-advisory — there is no silent skip.`);
|
|
386
|
+
}
|
|
387
|
+
return result.records.map((record) => ({ ...record, phase }));
|
|
388
|
+
}
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Durable merge
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
/** Terminal states that must not be silently walked back. */
|
|
393
|
+
const SETTLED_DISPOSITIONS = new Set([
|
|
394
|
+
"executed",
|
|
395
|
+
"adapted",
|
|
396
|
+
]);
|
|
397
|
+
/**
|
|
398
|
+
* Merge a phase's validated records into the durable ledger.
|
|
399
|
+
*
|
|
400
|
+
* Chronological evidence is preserved: the returned ledger keeps every prior record
|
|
401
|
+
* and appends the new ones, so a resumed session reads history rather than a summary.
|
|
402
|
+
* A settled step may be superseded only by a record that explicitly says what
|
|
403
|
+
* invalidated it (`invalidatedBy`); anything else that would downgrade or drop it
|
|
404
|
+
* throws, because a quietly reverted "executed" is a false completion claim.
|
|
405
|
+
*/
|
|
406
|
+
export function mergePlanStepRecords(existing, incoming) {
|
|
407
|
+
const merged = [...existing];
|
|
408
|
+
for (const record of incoming) {
|
|
409
|
+
const priorIndex = [...merged]
|
|
410
|
+
.map((entry, index) => ({ entry, index }))
|
|
411
|
+
.filter(({ entry }) => entry.stepId === record.stepId)
|
|
412
|
+
.pop()?.index;
|
|
413
|
+
if (priorIndex !== undefined) {
|
|
414
|
+
const prior = merged[priorIndex];
|
|
415
|
+
const priorWasSettled = SETTLED_DISPOSITIONS.has(prior.disposition);
|
|
416
|
+
const wouldRegress = priorWasSettled && !SETTLED_DISPOSITIONS.has(record.disposition);
|
|
417
|
+
const isRerun = priorWasSettled && SETTLED_DISPOSITIONS.has(record.disposition);
|
|
418
|
+
if ((wouldRegress || isRerun) && !record.invalidatedBy) {
|
|
419
|
+
throw new PlanLedgerError(`step ${record.stepId} is already recorded as '${prior.disposition}'; re-recording it as '${record.disposition}' requires an explicit invalidatedBy note stating what invalidated the prior evidence`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
merged.push({ ...record });
|
|
423
|
+
}
|
|
424
|
+
return merged;
|
|
425
|
+
}
|
|
426
|
+
/** The latest record per step — what "current state" means over a chronological log. */
|
|
427
|
+
export function currentLedgerState(ledger) {
|
|
428
|
+
const latest = new Map();
|
|
429
|
+
for (const record of ledger) {
|
|
430
|
+
latest.set(record.stepId, record);
|
|
431
|
+
}
|
|
432
|
+
return latest;
|
|
433
|
+
}
|
|
434
|
+
/** Records still needing human attention: escalations and declared-advisory gaps. */
|
|
435
|
+
export function unresolvedLedgerRecords(ledger) {
|
|
436
|
+
return [...currentLedgerState(ledger).values()]
|
|
437
|
+
.filter((record) => record.disposition === "escalated" || record.disposition === "unrun-advisory")
|
|
438
|
+
.sort((a, b) => a.stepId - b.stepId);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Compare orchestration-observed usage against the configured bounds.
|
|
442
|
+
*
|
|
443
|
+
* Values exactly at a limit pass; only a value above it breaches. Returns the
|
|
444
|
+
* breaches rather than throwing so the caller can turn them into an escalation
|
|
445
|
+
* record — a bound that only produced a warning in prompt text is not a bound.
|
|
446
|
+
*/
|
|
447
|
+
export function validateExecutionLimits(observed, limits = PRE_PR_VERIFICATION_LIMITS) {
|
|
448
|
+
const breaches = [];
|
|
449
|
+
const checks = [
|
|
450
|
+
["correctionTurns", observed.correctionTurns, limits.maxCorrectionTurns],
|
|
451
|
+
["changedFiles", observed.changedFiles, limits.maxChangedFiles],
|
|
452
|
+
["diffLines", observed.diffLines, limits.maxDiffLines],
|
|
453
|
+
];
|
|
454
|
+
for (const [metric, value, limit] of checks) {
|
|
455
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
456
|
+
throw new PlanLedgerError(`observed ${metric} is not a number; limits are enforced from orchestration-observed values, never from an agent's own account`);
|
|
457
|
+
}
|
|
458
|
+
if (value > limit) {
|
|
459
|
+
breaches.push({ metric, observed: value, limit });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return breaches;
|
|
463
|
+
}
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Secret safety
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
const REDACTED = "[REDACTED]";
|
|
468
|
+
/**
|
|
469
|
+
* Patterns for values that must never reach durable state or a log line.
|
|
470
|
+
*
|
|
471
|
+
* Each rewrites only the secret itself and keeps the surrounding key or scheme, so a
|
|
472
|
+
* redacted record still shows *that* a credential was involved — useful for audit,
|
|
473
|
+
* and materially different from dropping the field.
|
|
474
|
+
*/
|
|
475
|
+
const SECRET_PATTERNS = [
|
|
476
|
+
// Bearer / token schemes in an Authorization header or bare.
|
|
477
|
+
[/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/gi, `$1 ${REDACTED}`],
|
|
478
|
+
// authorization: <anything>
|
|
479
|
+
[/\b(authorization)\s*[:=]\s*\S+/gi, `$1: ${REDACTED}`],
|
|
480
|
+
// Well-known provider key shapes.
|
|
481
|
+
[/\b(sk|pk|rk)-[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
482
|
+
[/\bgh[pousr]_[A-Za-z0-9]{16,}/g, REDACTED],
|
|
483
|
+
// key/secret/password/token assignments in prose, env, JSON, or CLI form.
|
|
484
|
+
[
|
|
485
|
+
/\b([A-Za-z0-9_-]*(?:api[_-]?key|secret|password|passwd|token|credential)[A-Za-z0-9_-]*)\s*[:=]\s*("[^"]*"|'[^']*'|\S+)/gi,
|
|
486
|
+
`$1=${REDACTED}`,
|
|
487
|
+
],
|
|
488
|
+
// Credentials embedded in a URL.
|
|
489
|
+
[/\b([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, `$1${REDACTED}@`],
|
|
490
|
+
];
|
|
491
|
+
/** Redact secret-bearing substrings while leaving safe technical text intact. */
|
|
492
|
+
export function redactSecrets(value) {
|
|
493
|
+
let output = value;
|
|
494
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
495
|
+
output = output.replace(pattern, replacement);
|
|
496
|
+
}
|
|
497
|
+
return output;
|
|
498
|
+
}
|
|
499
|
+
function redactDeep(value) {
|
|
500
|
+
if (typeof value === "string")
|
|
501
|
+
return redactSecrets(value);
|
|
502
|
+
if (Array.isArray(value))
|
|
503
|
+
return value.map((entry) => redactDeep(entry));
|
|
504
|
+
if (isPlainObject(value)) {
|
|
505
|
+
const output = {};
|
|
506
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
507
|
+
output[key] = redactDeep(entry);
|
|
508
|
+
}
|
|
509
|
+
return output;
|
|
510
|
+
}
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Redact a record before it is persisted or serialized.
|
|
515
|
+
*
|
|
516
|
+
* Redaction, not rejection: paths, commands, step identifiers, SHAs, and rubric text
|
|
517
|
+
* are exactly what makes the ledger auditable, so the record survives with its
|
|
518
|
+
* technical content and loses only the credential values.
|
|
519
|
+
*/
|
|
520
|
+
export function sanitizePlanStepRecord(record) {
|
|
521
|
+
return redactDeep(record);
|
|
522
|
+
}
|
|
523
|
+
export function sanitizePlanStepRecords(records) {
|
|
524
|
+
return records.map((record) => sanitizePlanStepRecord(record));
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Build the audit-ready view of a run's ledger.
|
|
528
|
+
*
|
|
529
|
+
* Unresolved records come first and separately: an escalation buried at position 40
|
|
530
|
+
* of a chronological log reads as a completed run at a glance, which defeats the
|
|
531
|
+
* point of recording it.
|
|
532
|
+
*/
|
|
533
|
+
export function buildLedgerRunDetail(ledger, options = {}) {
|
|
534
|
+
const sanitized = sanitizePlanStepRecords(ledger);
|
|
535
|
+
const latest = [...currentLedgerState(sanitized).values()];
|
|
536
|
+
const summary = {
|
|
537
|
+
executed: latest.filter((r) => r.disposition === "executed").length,
|
|
538
|
+
adapted: latest.filter((r) => r.disposition === "adapted").length,
|
|
539
|
+
escalated: latest.filter((r) => r.disposition === "escalated").length,
|
|
540
|
+
unrunAdvisory: latest.filter((r) => r.disposition === "unrun-advisory").length,
|
|
541
|
+
total: latest.length,
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
metadataVersion: PLAN_METADATA_VERSION,
|
|
545
|
+
summary,
|
|
546
|
+
unresolved: unresolvedLedgerRecords(sanitized),
|
|
547
|
+
ledger: sanitized,
|
|
548
|
+
...(options.checkpoint ? { checkpoint: options.checkpoint } : {}),
|
|
549
|
+
};
|
|
550
|
+
}
|