@bridge_gpt/mcp-server 0.2.68 → 0.2.70
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 +226 -320
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +4 -4
- package/build/artifact-diff.js +110 -0
- package/build/bin-entrypoint.js +36 -0
- package/build/bridge-permissions.js +650 -98
- package/build/check-update-cli.js +74 -0
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/claude-settings.js +84 -2
- package/build/cli-help.js +7 -1
- package/build/codex-bridge-profile.js +1 -1
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/commands.generated.js +8 -8
- package/build/conductor/bridge-api-client.js +598 -19
- package/build/conductor/bring-up-facts.js +9 -9
- package/build/conductor/doctor.js +466 -18
- package/build/conductor/epic-child-continuation.js +54 -3
- package/build/conductor/epic-implementer-cli.js +132 -34
- package/build/conductor/epic-runtime.js +2 -2
- package/build/conductor/epic-state.js +1 -1
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +4 -2
- package/build/conductor/launch-need-probes.js +337 -0
- package/build/conductor/local-merge.js +241 -56
- package/build/conductor/merge-credential.js +172 -0
- package/build/conductor/merge-execution-eligibility.js +1 -1
- package/build/conductor/merged-child-check.js +95 -0
- package/build/conductor/plan-trim.js +30 -0
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +11 -6
- package/build/conductor/readiness-prerequisites.js +41 -0
- package/build/conductor/readiness-sections.js +345 -55
- package/build/conductor/readiness.js +99 -87
- package/build/conductor/recovery-cli.js +606 -21
- package/build/conductor/recovery-operations.js +306 -7
- package/build/conductor/repair-contract.js +47 -2
- package/build/conductor/repository-workflow-remedy.js +8 -6
- package/build/conductor/stored-merge-credential.js +76 -0
- package/build/conductor/stored-merge-token-probe.js +28 -0
- package/build/conductor/temporary-plan.js +50 -0
- package/build/conductor/wait-run-cli.js +759 -0
- package/build/conductor/worker-database-doctor-inspector.js +49 -0
- package/build/conductor-bin.js +2 -2
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
- package/build/conductor-removed-stub-bin.js +2 -5
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +356 -13
- package/build/docs.generated.js +27 -14
- package/build/doctor.js +203 -38
- package/build/drive-epic-help.js +212 -0
- package/build/drive-epic.js +1840 -373
- package/build/epic-ci-workflow-drift.js +77 -0
- package/build/epic-ci-workflow-install.js +183 -0
- package/build/epic-ci-workflow.js +157 -0
- package/build/epic-contract-context.js +326 -0
- package/build/epic-implementer-bin.js +131 -120
- package/build/epic-implementer-claude-hook-bin.js +2 -2
- package/build/escalation-reachability.js +134 -0
- package/build/executor/cleanup-report.js +99 -0
- package/build/executor/cli.js +12 -1
- package/build/executor/completion-contract.js +68 -11
- package/build/executor/completion-supervision.js +99 -0
- package/build/executor/delivery-outcome.js +179 -0
- package/build/executor/deps.js +3 -0
- package/build/executor/env.js +71 -0
- package/build/executor/failure-reason-codes.js +126 -0
- package/build/executor/heartbeat.js +38 -0
- package/build/executor/http-client.js +115 -4
- package/build/executor/integration-job.js +298 -5
- package/build/executor/job-errors.js +36 -6
- package/build/executor/job-runner.js +720 -36
- package/build/executor/likely-tests.js +36 -2
- package/build/executor/merge-job.js +145 -2
- package/build/executor/observation.js +73 -4
- package/build/executor/permissions.js +193 -97
- package/build/executor/preflight.js +14 -3
- package/build/executor/process-containment.js +28 -2
- package/build/executor/prompt-spec.js +50 -5
- package/build/executor/rebase-finalization.js +38 -3
- package/build/executor/reconcile-overlap-job.js +35 -7
- package/build/executor/required-work-ledger.js +236 -81
- package/build/executor/resume-pre-spawn.js +38 -1
- package/build/executor/runner.js +91 -3
- package/build/executor/test-clock.js +8 -0
- package/build/executor/types.js +37 -2
- package/build/executor/worker-cli-launcher.js +259 -0
- package/build/executor/worker-command.js +41 -4
- package/build/executor/worker-database-target.js +91 -0
- package/build/executor/worker-database.js +908 -0
- package/build/executor/worker-finalization.js +106 -0
- package/build/executor/worker-guard-hook.js +203 -0
- package/build/executor/worker-resources.js +47 -0
- package/build/executor/worker-verification-guidance.js +15 -2
- package/build/executor/worktree.js +303 -15
- package/build/executor-launch.js +11 -1
- package/build/generated-assets-doctor.js +9 -1
- package/build/generated-assets-health.js +13 -4
- package/build/generated-assets-legacy.js +39 -0
- package/build/generated-assets-paths.js +12 -1
- package/build/generated-assets-repair.js +4 -1
- package/build/generated-assets-test-support.js +12 -0
- package/build/github-remote.js +33 -0
- package/build/github-sign-in.js +62 -0
- package/build/index.js +1214 -465
- package/build/init.js +39 -1
- package/build/install-bridge-conductor-gate.js +78 -0
- package/build/install-bridge-conductor-setup.js +413 -0
- package/build/install-bridge-conductor.js +1226 -267
- package/build/install-bridge.js +629 -350
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +8 -2
- package/build/merge-pull-request.js +6 -0
- package/build/merge-workflow-install.js +763 -345
- package/build/phase-result-artifacts.js +75 -20
- package/build/pipeline-orchestrator.js +247 -4
- package/build/pipelines.generated.js +35 -35
- package/build/plan-discovery.js +15 -2
- package/build/plan-execution-ledger.js +306 -17
- package/build/plan-phase-routing.js +48 -13
- package/build/plane/cli.js +105 -51
- package/build/plane/defaults.js +3 -0
- package/build/plane/plane-freshness.js +294 -0
- package/build/plane/schema-compatibility.js +137 -0
- package/build/plane/shutdown-deps.js +38 -0
- package/build/plane/shutdown.js +83 -8
- package/build/plane/terminal-run-cleanup.js +303 -0
- package/build/printed-remedy-commands.js +26 -4
- package/build/readiness-check.js +6 -2
- package/build/readiness-prerequisites.generated.js +532 -0
- package/build/readme.generated.js +1 -1
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +43 -15
- package/build/test-command-detection.js +247 -0
- package/build/test-command-validation.js +73 -0
- package/build/testing-config-client.js +150 -0
- package/build/ticket-delivery-artifacts.js +258 -0
- package/build/ticket-delivery-enrollment.js +306 -0
- package/build/ticket-delivery.js +205 -0
- package/build/ticket-review-summary.js +14 -0
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +28 -89
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +360 -20
- package/build/v2-readiness-requirements.js +552 -0
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +2 -2
- package/build/worktree-core.js +22 -7
- package/docs/BRIDGE-DIRECTORY-README.md +48 -0
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
- package/docs/EPIC-IMPLEMENTER.md +104 -63
- package/docs/epic-implementer/getting-started.md +131 -0
- package/docs/epic-implementer/self-hosting.md +133 -0
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +1 -1
- package/pipelines/implement-ticket.json +8 -8
- package/pipelines/review-ticket.json +2 -2
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
package/build/plan-discovery.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* inside the DAG — every one of those is a way to hand the wrong plan to a run
|
|
9
9
|
* that then creates real tickets' worth of work.
|
|
10
10
|
*
|
|
11
|
+
* A folder holding a regular DAG but no regular metadata is reported as a skipped
|
|
12
|
+
* `missing-meta` entry so the operator can see it. It is only reported: the DAG
|
|
13
|
+
* supplies no identity, so such a folder is never a candidate and never changes
|
|
14
|
+
* the result.
|
|
15
|
+
*
|
|
11
16
|
* Every filesystem read goes through the injected {@link PlanDiscoveryFs}, so a
|
|
12
17
|
* unit test reaches no real disk. Nothing here throws for a filesystem failure:
|
|
13
18
|
* an unreadable or malformed entry is named in `skipped` with a fixed reason and
|
|
@@ -22,6 +27,7 @@ export const PLAN_DISCOVERY_SKIP_REASONS = [
|
|
|
22
27
|
"not-an-object",
|
|
23
28
|
"missing-or-non-string-epic-key",
|
|
24
29
|
"missing-dag",
|
|
30
|
+
"missing-meta",
|
|
25
31
|
"unlistable-directory",
|
|
26
32
|
];
|
|
27
33
|
const META_FILE = "epic-plan.meta.json";
|
|
@@ -77,9 +83,16 @@ export async function discoverEpicPlan(epicKey, docsDir, fs) {
|
|
|
77
83
|
}
|
|
78
84
|
const isRegular = (entry) => entry.isFile() && entry.isSymbolicLink?.() !== true;
|
|
79
85
|
const meta = files.find((entry) => entry.name === META_FILE && isRegular(entry));
|
|
80
|
-
if (meta === undefined)
|
|
81
|
-
continue;
|
|
82
86
|
const metaPath = path.join(dirPath, META_FILE);
|
|
87
|
+
if (meta === undefined) {
|
|
88
|
+
// A folder with a regular DAG but no regular metadata is expected-but-
|
|
89
|
+
// missing metadata: name it, never count it. The DAG supplies no identity.
|
|
90
|
+
// A folder with neither regular file stays silent.
|
|
91
|
+
if (files.some((entry) => entry.name === DAG_FILE && isRegular(entry))) {
|
|
92
|
+
skipped.push({ path: metaPath, reason: "missing-meta" });
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
83
96
|
let raw;
|
|
84
97
|
try {
|
|
85
98
|
raw = await fs.readFile(metaPath);
|
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
* chronological evidence and refuses to silently downgrade or drop a settled step,
|
|
27
27
|
* so the record survives an approval pause or a resumed session — the conversational
|
|
28
28
|
* hand-off it replaces did not.
|
|
29
|
+
* - **Post-PR ownership is server-accepted, never self-granted (BAPI-1426).** A
|
|
30
|
+
* pre-PR verification step that genuinely needs an open PR escalates with a
|
|
31
|
+
* `prDependency` mark. `post_pr_gap_close` owns exactly the steps whose effective
|
|
32
|
+
* *server-accepted* record is such an escalation ({@link derivePostPrOwnedSteps}).
|
|
33
|
+
* The effective record is the latest one, except that a same-identity `remote_ci`
|
|
34
|
+
* deferral survives plain post-PR re-escalations (BAPI-1488). Without an accepted ledger context it owns nothing, so a local mark alone can
|
|
35
|
+
* never route work past the PR boundary. This mirrors
|
|
36
|
+
* `api/library/vcs/ticket_delivery_identity.py`; the server stays the authority.
|
|
29
37
|
*
|
|
30
38
|
* Pure domain logic: no I/O, no git, no network, no filesystem, no MCP transport.
|
|
31
39
|
*/
|
|
@@ -73,6 +81,46 @@ export const MECHANICAL_ADAPTATION_KINDS = [
|
|
|
73
81
|
"repository-command-correction",
|
|
74
82
|
"equivalent-implementation-recognized",
|
|
75
83
|
];
|
|
84
|
+
/**
|
|
85
|
+
* BAPI-1426: the closed set of open-PR checks a pre-PR verification step may wait on.
|
|
86
|
+
*
|
|
87
|
+
* Mirrored from `PR_DEPENDENCY_VALUES` in `api/library/vcs/ticket_delivery_identity.py`.
|
|
88
|
+
* A mark is accepted only on an escalated `pre_pr_verification` record for a step whose
|
|
89
|
+
* class that phase owns. Missing implementation can never cross the pre-PR boundary.
|
|
90
|
+
*/
|
|
91
|
+
export const PR_DEPENDENCY_VALUES = [
|
|
92
|
+
"remote_ci",
|
|
93
|
+
"pr_review",
|
|
94
|
+
"pr_preview_environment",
|
|
95
|
+
];
|
|
96
|
+
/**
|
|
97
|
+
* BAPI-1492: the verification hand-off vocabulary. Mirrored from `HANDOFF_CAUSES`,
|
|
98
|
+
* `HANDOFF_ELIGIBLE_CLASSES`, and `HANDOFF_FIELDS` in
|
|
99
|
+
* `api/library/vcs/ticket_delivery_identity.py`; both sides must agree.
|
|
100
|
+
*
|
|
101
|
+
* A hand-off is additive metadata on an escalated `pre_pr_verification` record for
|
|
102
|
+
* an E2E or rendered-UI step whose check could not run because a prerequisite was
|
|
103
|
+
* missing. It is never a disposition and never a `prDependency` value. Whether it
|
|
104
|
+
* may satisfy the step is decided only by the server, from persisted run state.
|
|
105
|
+
*/
|
|
106
|
+
export const HANDOFF_CAUSES = [
|
|
107
|
+
"no_live_server",
|
|
108
|
+
"no_database",
|
|
109
|
+
"toolchain_unavailable",
|
|
110
|
+
"credential_unavailable",
|
|
111
|
+
];
|
|
112
|
+
export const HANDOFF_ELIGIBLE_CLASSES = ["e2e_tests", "rendered_ui_review"];
|
|
113
|
+
export const HANDOFF_FIELDS = [
|
|
114
|
+
"cause",
|
|
115
|
+
"criterion",
|
|
116
|
+
"observation",
|
|
117
|
+
"observed_head_sha",
|
|
118
|
+
"next_action",
|
|
119
|
+
];
|
|
120
|
+
/** Mirrors the server's `MAX_EVIDENCE_TEXT`: longer text is truncated, not refused. */
|
|
121
|
+
export const HANDOFF_MAX_TEXT = 4000;
|
|
122
|
+
const HANDOFF_TEXT_FIELDS = ["criterion", "observation", "next_action"];
|
|
123
|
+
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
76
124
|
/**
|
|
77
125
|
* Change categories that may never be adapted. Meeting one of these means the step's
|
|
78
126
|
* premise is wrong in a way only a human should resolve, so the run escalates.
|
|
@@ -198,13 +246,12 @@ export function validatePlanMetadata(value) {
|
|
|
198
246
|
if (startStep <= previousEnd) {
|
|
199
247
|
throw new PlanLedgerError(`plan metadata part '${partId}' range ${startStep}-${endStep} overlaps or precedes '${previousId}' ending at ${previousEnd}; ranges must be disjoint and ascending`);
|
|
200
248
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
249
|
+
// A numbering SKIP between parts (one ends at 16, the next starts at 18) is
|
|
250
|
+
// legal (BAPI-1487). Ranges come from the step numbers in each part's own body,
|
|
251
|
+
// so a skipped number is a step no part wrote. resolveOwnedSteps iterates only
|
|
252
|
+
// declared ranges, so it is owned by no phase, which is correct: it does not
|
|
253
|
+
// exist. The plan generator separately refuses a numbered step that sits in
|
|
254
|
+
// the assembled plan outside every declared range.
|
|
208
255
|
previousEnd = endStep;
|
|
209
256
|
previousId = partId;
|
|
210
257
|
parts.push({
|
|
@@ -244,14 +291,164 @@ export function validatePlanMetadata(value) {
|
|
|
244
291
|
total_steps: totalSteps,
|
|
245
292
|
};
|
|
246
293
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
294
|
+
/** True when an accepted record is a pre-PR escalation carrying an accepted mark. */
|
|
295
|
+
export function isAcceptedPrDependentEscalation(record) {
|
|
296
|
+
return (record.disposition === "escalated" &&
|
|
297
|
+
typeof record.pr_dependency === "string" &&
|
|
298
|
+
PR_DEPENDENCY_VALUES.includes(record.pr_dependency) &&
|
|
299
|
+
record.phase === "pre_pr_verification");
|
|
300
|
+
}
|
|
301
|
+
/** An absent or empty text marker; any other type is malformed. Mirrors the server. */
|
|
302
|
+
function isEmptyMarker(value) {
|
|
303
|
+
return value === undefined || value === null || (typeof value === "string" && value.trim() === "");
|
|
304
|
+
}
|
|
305
|
+
/** The record's (plan digest, specification identity) pair, or undefined unless both are real. */
|
|
306
|
+
function identityPair(record) {
|
|
307
|
+
const { plan_digest: planDigest, specification_identity: specification } = record;
|
|
308
|
+
if (typeof planDigest === "string" &&
|
|
309
|
+
planDigest.trim() !== "" &&
|
|
310
|
+
typeof specification === "string" &&
|
|
311
|
+
specification.trim() !== "") {
|
|
312
|
+
return [planDigest, specification];
|
|
313
|
+
}
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
/** An unmarked, unfailed escalation in `phase` that carries real identities. */
|
|
317
|
+
function isCleanEscalation(record, phase) {
|
|
318
|
+
return (record.phase === phase &&
|
|
319
|
+
record.disposition === "escalated" &&
|
|
320
|
+
record.verification_failed === false &&
|
|
321
|
+
isEmptyMarker(record.invalidated_by) &&
|
|
322
|
+
identityPair(record) !== undefined);
|
|
323
|
+
}
|
|
324
|
+
/** A post-PR escalation that repeats no dependency mark and names no failure. */
|
|
325
|
+
function isPlainPostPrReescalation(record) {
|
|
326
|
+
return isCleanEscalation(record, "post_pr_gap_close") && isEmptyMarker(record.pr_dependency);
|
|
327
|
+
}
|
|
328
|
+
/** A clean pre-PR escalation that deferred the step to the PR's remote CI. */
|
|
329
|
+
function isRemoteCiDeferral(record) {
|
|
330
|
+
return isCleanEscalation(record, "pre_pr_verification") && record.pr_dependency === "remote_ci";
|
|
331
|
+
}
|
|
332
|
+
function sameIdentity(a, b) {
|
|
333
|
+
return a !== undefined && b !== undefined && a[0] === b[0] && a[1] === b[1];
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The record that currently decides each step (BAPI-1488). Mirrors
|
|
337
|
+
* `effective_accepted_records_by_step` in `api/library/vcs/ticket_delivery_identity.py`.
|
|
338
|
+
*
|
|
339
|
+
* Normally that is the latest accepted record: highest `revision`, with ties (and
|
|
340
|
+
* absent revisions) broken by array order. When the latest is a plain post-PR
|
|
341
|
+
* re-escalation, the walk goes backward through the contiguous run of them and returns
|
|
342
|
+
* the preceding `remote_ci` deferral, but only if every record in the chain shares its
|
|
343
|
+
* non-empty plan digest and specification identity. The first record that does not
|
|
344
|
+
* fit is a barrier, and the literal latest record stays effective.
|
|
345
|
+
*/
|
|
346
|
+
function effectiveAcceptedRecords(records) {
|
|
347
|
+
const histories = new Map();
|
|
348
|
+
for (const record of records) {
|
|
349
|
+
if (!record || !isPositiveInteger(record.step_id))
|
|
350
|
+
continue;
|
|
351
|
+
const revision = typeof record.revision === "number" ? record.revision : -Infinity;
|
|
352
|
+
const history = histories.get(record.step_id) ?? [];
|
|
353
|
+
history.push({ record, revision });
|
|
354
|
+
histories.set(record.step_id, history);
|
|
355
|
+
}
|
|
356
|
+
const effective = new Map();
|
|
357
|
+
for (const [stepId, history] of histories) {
|
|
358
|
+
// Array.prototype.sort is stable, so equal revisions keep array order.
|
|
359
|
+
const ordered = history
|
|
360
|
+
.map((entry, index) => ({ ...entry, index }))
|
|
361
|
+
.sort((a, b) => (a.revision === b.revision ? a.index - b.index : a.revision < b.revision ? -1 : 1))
|
|
362
|
+
.map((entry) => entry.record);
|
|
363
|
+
const latest = ordered[ordered.length - 1];
|
|
364
|
+
effective.set(stepId, latest);
|
|
365
|
+
if (!isPlainPostPrReescalation(latest))
|
|
366
|
+
continue;
|
|
367
|
+
const identity = identityPair(latest);
|
|
368
|
+
for (let i = ordered.length - 2; i >= 0; i -= 1) {
|
|
369
|
+
const earlier = ordered[i];
|
|
370
|
+
if (!sameIdentity(identityPair(earlier), identity))
|
|
371
|
+
break;
|
|
372
|
+
if (isRemoteCiDeferral(earlier)) {
|
|
373
|
+
effective.set(stepId, earlier);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
if (!isPlainPostPrReescalation(earlier))
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return effective;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The steps `post_pr_gap_close` owns: those whose EFFECTIVE accepted record is an
|
|
384
|
+
* escalated `pre_pr_verification` record with an accepted `pr_dependency`.
|
|
385
|
+
*
|
|
386
|
+
* The effective record is the latest one (highest `revision`, ties and absent
|
|
387
|
+
* revisions broken by array order), except that a same-identity `remote_ci` deferral
|
|
388
|
+
* survives plain post-PR re-escalations: the server refuses `prDependency` on post-PR
|
|
389
|
+
* records, so a restated step cannot repeat the mark. Failures, invalidation markers,
|
|
390
|
+
* resolutions, other dependencies, and missing or mismatched identities stop the
|
|
391
|
+
* carry-forward. No context means no ownership — a local mark alone never routes work.
|
|
392
|
+
*/
|
|
393
|
+
export function derivePostPrOwnedSteps(accepted) {
|
|
394
|
+
if (!accepted || !Array.isArray(accepted.acceptedRecords))
|
|
395
|
+
return [];
|
|
396
|
+
return [...effectiveAcceptedRecords(accepted.acceptedRecords).entries()]
|
|
397
|
+
.filter(([, record]) => isAcceptedPrDependentEscalation(record))
|
|
398
|
+
.map(([stepId]) => stepId)
|
|
399
|
+
.sort((a, b) => a - b);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* BAPI-1473: the latest server-accepted record per step, limited to `steps`.
|
|
403
|
+
*
|
|
404
|
+
* A head-moving engine job (`ci_fix`, `remediate`, `rebase`) can run in a fresh
|
|
405
|
+
* worktree whose local journal is empty. Its renewal call must still settle every
|
|
406
|
+
* step the phase owns, re-reporting a record that is still current as it stands,
|
|
407
|
+
* so it needs to see what the server last accepted. Latest uses the same rule as
|
|
408
|
+
* {@link derivePostPrOwnedSteps}: highest `revision`, ties broken by array order.
|
|
409
|
+
* No context means an empty list; the local journal is never rewritten from it.
|
|
410
|
+
*/
|
|
411
|
+
export function latestAcceptedRecordsForSteps(accepted, steps) {
|
|
412
|
+
if (!accepted || !Array.isArray(accepted.acceptedRecords))
|
|
413
|
+
return [];
|
|
414
|
+
const wanted = new Set(steps);
|
|
415
|
+
const latest = new Map();
|
|
416
|
+
for (const record of accepted.acceptedRecords) {
|
|
417
|
+
if (!record || !isPositiveInteger(record.step_id) || !wanted.has(record.step_id))
|
|
418
|
+
continue;
|
|
419
|
+
const revision = typeof record.revision === "number" ? record.revision : -Infinity;
|
|
420
|
+
const prior = latest.get(record.step_id);
|
|
421
|
+
if (prior === undefined || revision >= prior.revision) {
|
|
422
|
+
latest.set(record.step_id, { record, revision });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return [...latest.values()]
|
|
426
|
+
.map(({ record }) => ({
|
|
427
|
+
step_id: record.step_id,
|
|
428
|
+
phase: record.phase,
|
|
429
|
+
disposition: record.disposition,
|
|
430
|
+
pr_dependency: record.pr_dependency ?? null,
|
|
431
|
+
...(typeof record.revision === "number" ? { revision: record.revision } : {}),
|
|
432
|
+
}))
|
|
433
|
+
.sort((a, b) => a.step_id - b.step_id);
|
|
434
|
+
}
|
|
435
|
+
/** Every step present in some part of the plan. */
|
|
436
|
+
function stepIsInPlan(metadata, stepId) {
|
|
437
|
+
return metadata.parts.some((part) => stepId >= part.start_step && stepId <= part.end_step);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Inclusive step IDs owned by `phase`, in plan order.
|
|
441
|
+
*
|
|
442
|
+
* `produce` and `pre_pr_verification` own by provenance class. `post_pr_gap_close`
|
|
443
|
+
* owns dynamically, from the server-accepted ledger, limited to steps the plan has.
|
|
444
|
+
*/
|
|
445
|
+
export function resolveOwnedSteps(metadata, phase, accepted) {
|
|
252
446
|
if (!PLAN_PHASES.includes(phase)) {
|
|
253
447
|
throw new PlanLedgerError(`unknown phase '${phase}'`);
|
|
254
448
|
}
|
|
449
|
+
if (phase === "post_pr_gap_close") {
|
|
450
|
+
return derivePostPrOwnedSteps(accepted).filter((step) => stepIsInPlan(metadata, step));
|
|
451
|
+
}
|
|
255
452
|
const owned = [];
|
|
256
453
|
for (const part of metadata.parts) {
|
|
257
454
|
if (PLAN_CLASS_OWNERSHIP[part.provenance_class] !== phase)
|
|
@@ -298,7 +495,7 @@ export function assertStepClassCoverage(metadata, ownership = PLAN_CLASS_OWNERSH
|
|
|
298
495
|
// ---------------------------------------------------------------------------
|
|
299
496
|
// Phase results
|
|
300
497
|
// ---------------------------------------------------------------------------
|
|
301
|
-
function validateRecordShape(record, metadata) {
|
|
498
|
+
function validateRecordShape(record, metadata, phase) {
|
|
302
499
|
if (!isPositiveInteger(record.stepId)) {
|
|
303
500
|
throw new PlanLedgerError(`ledger record has a non-positive stepId ${String(record.stepId)}`);
|
|
304
501
|
}
|
|
@@ -328,6 +525,12 @@ function validateRecordShape(record, metadata) {
|
|
|
328
525
|
throw new PlanLedgerError(`step ${record.stepId} is 'escalated' but gives no blocking reason; an escalation must never read as completion`);
|
|
329
526
|
}
|
|
330
527
|
}
|
|
528
|
+
if (record.handoff !== undefined) {
|
|
529
|
+
validateHandoff(record, metadata, phase);
|
|
530
|
+
}
|
|
531
|
+
if (record.prDependency !== undefined) {
|
|
532
|
+
validatePrDependency(record, metadata, phase);
|
|
533
|
+
}
|
|
331
534
|
if (record.disposition === "unrun-advisory") {
|
|
332
535
|
const owningPart = metadata.parts.find((part) => record.stepId >= part.start_step && record.stepId <= part.end_step);
|
|
333
536
|
if (!owningPart || !owningPart.declared_advisory) {
|
|
@@ -356,14 +559,98 @@ function validateRecordShape(record, metadata) {
|
|
|
356
559
|
}
|
|
357
560
|
}
|
|
358
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* Structural rules for the BAPI-1426 `prDependency` mark, mirroring the server's
|
|
564
|
+
* `_validate_pr_dependency`. The server also checks that the reason names the check;
|
|
565
|
+
* here an escalation reason is already required, so only the shape is enforced.
|
|
566
|
+
*/
|
|
567
|
+
function validatePrDependency(record, metadata, phase) {
|
|
568
|
+
const value = record.prDependency;
|
|
569
|
+
if (typeof value !== "string" || !PR_DEPENDENCY_VALUES.includes(value)) {
|
|
570
|
+
throw new PlanLedgerError(`step ${record.stepId} has unsupported prDependency '${String(value)}'; expected one of ${PR_DEPENDENCY_VALUES.join(", ")}`);
|
|
571
|
+
}
|
|
572
|
+
if (record.disposition !== "escalated") {
|
|
573
|
+
throw new PlanLedgerError(`step ${record.stepId} carries prDependency but is '${record.disposition}'; the mark is accepted only on an escalated record`);
|
|
574
|
+
}
|
|
575
|
+
if (phase !== "pre_pr_verification") {
|
|
576
|
+
throw new PlanLedgerError(`step ${record.stepId} carries prDependency in phase '${phase}'; the mark is accepted only on a pre_pr_verification record, and post-PR records must omit it`);
|
|
577
|
+
}
|
|
578
|
+
const owningPart = metadata.parts.find((part) => record.stepId >= part.start_step && record.stepId <= part.end_step);
|
|
579
|
+
if (!owningPart || PLAN_CLASS_OWNERSHIP[owningPart.provenance_class] !== "pre_pr_verification") {
|
|
580
|
+
throw new PlanLedgerError(`step ${record.stepId} carries prDependency, which is never accepted on implementation or documentation work; missing implementation cannot cross the pre-PR boundary`);
|
|
581
|
+
}
|
|
582
|
+
if (typeof record.escalationReason !== "string" || record.escalationReason.trim() === "") {
|
|
583
|
+
throw new PlanLedgerError(`step ${record.stepId} carries prDependency but gives no escalationReason naming the check that needs an open PR`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* BAPI-1492: structural rules for a verification hand-off, mirroring the server's
|
|
588
|
+
* `_validate_handoff` with the same wording. Authority, prior-ledger history,
|
|
589
|
+
* verification failures, and the observed head are server-only decisions.
|
|
590
|
+
*/
|
|
591
|
+
export function validateHandoff(record, metadata, phase) {
|
|
592
|
+
const fail = (reason) => {
|
|
593
|
+
throw new PlanLedgerError(`step ${record.stepId}: ${reason}`);
|
|
594
|
+
};
|
|
595
|
+
const value = record.handoff;
|
|
596
|
+
const isObject = typeof value === "object" && value !== null && !Array.isArray(value);
|
|
597
|
+
const keys = isObject ? Object.keys(value).sort() : [];
|
|
598
|
+
const expected = [...HANDOFF_FIELDS].sort();
|
|
599
|
+
if (!isObject || keys.length !== expected.length || keys.some((k, i) => k !== expected[i])) {
|
|
600
|
+
fail(`handoff must be an object with exactly ${HANDOFF_FIELDS.join(", ")}`);
|
|
601
|
+
}
|
|
602
|
+
const handoff = value;
|
|
603
|
+
if (!HANDOFF_CAUSES.includes(handoff.cause)) {
|
|
604
|
+
fail(`handoff cause must be one of ${HANDOFF_CAUSES.join(", ")}`);
|
|
605
|
+
}
|
|
606
|
+
for (const field of HANDOFF_TEXT_FIELDS) {
|
|
607
|
+
const text = handoff[field];
|
|
608
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
609
|
+
fail(`handoff is missing ${field}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const sha = handoff.observed_head_sha;
|
|
613
|
+
if (typeof sha !== "string" || !FULL_SHA_PATTERN.test(sha.trim().toLowerCase())) {
|
|
614
|
+
fail("handoff observed_head_sha must be a full commit SHA");
|
|
615
|
+
}
|
|
616
|
+
if (record.disposition !== "escalated") {
|
|
617
|
+
fail("handoff is accepted only on an escalated record");
|
|
618
|
+
}
|
|
619
|
+
const owningPart = metadata.parts.find((part) => record.stepId >= part.start_step && record.stepId <= part.end_step);
|
|
620
|
+
if (phase !== "pre_pr_verification" ||
|
|
621
|
+
!owningPart ||
|
|
622
|
+
PLAN_CLASS_OWNERSHIP[owningPart.provenance_class] !== "pre_pr_verification") {
|
|
623
|
+
fail("handoff is accepted only on a pre_pr_verification record");
|
|
624
|
+
}
|
|
625
|
+
if (!HANDOFF_ELIGIBLE_CLASSES.includes(owningPart.provenance_class)) {
|
|
626
|
+
fail("handoff is accepted only for e2e_tests or rendered_ui_review steps; review and other verification steps always run");
|
|
627
|
+
}
|
|
628
|
+
if (record.prDependency !== undefined) {
|
|
629
|
+
fail("handoff cannot be combined with prDependency");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/** Trim, bound, and lowercase a validated hand-off the way the server stores it. */
|
|
633
|
+
function normalizeHandoff(handoff) {
|
|
634
|
+
const bound = (text) => text.trim().slice(0, HANDOFF_MAX_TEXT);
|
|
635
|
+
return {
|
|
636
|
+
cause: handoff.cause,
|
|
637
|
+
criterion: bound(handoff.criterion),
|
|
638
|
+
observation: bound(handoff.observation),
|
|
639
|
+
observed_head_sha: handoff.observed_head_sha.trim().toLowerCase(),
|
|
640
|
+
next_action: bound(handoff.next_action),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
359
643
|
/**
|
|
360
644
|
* Validate one phase's reported outcome against the steps it owns.
|
|
361
645
|
*
|
|
362
646
|
* Every owned step must be settled exactly once. An omission is the historical bug
|
|
363
647
|
* (work quietly not done), a duplicate makes the record ambiguous, and an unowned
|
|
364
648
|
* step means the phase acted outside its routing.
|
|
649
|
+
*
|
|
650
|
+
* For `post_pr_gap_close`, `accepted` is the server-accepted ledger as it stood
|
|
651
|
+
* BEFORE this envelope, so an envelope cannot grant itself post-PR ownership.
|
|
365
652
|
*/
|
|
366
|
-
export function validatePhaseResult(result, metadata, phase) {
|
|
653
|
+
export function validatePhaseResult(result, metadata, phase, accepted) {
|
|
367
654
|
if (!result || typeof result !== "object") {
|
|
368
655
|
throw new PlanLedgerError("phase result is missing");
|
|
369
656
|
}
|
|
@@ -376,10 +663,10 @@ export function validatePhaseResult(result, metadata, phase) {
|
|
|
376
663
|
if (!Array.isArray(result.records)) {
|
|
377
664
|
throw new PlanLedgerError(`phase '${phase}' result has no records array`);
|
|
378
665
|
}
|
|
379
|
-
const owned = new Set(resolveOwnedSteps(metadata, phase));
|
|
666
|
+
const owned = new Set(resolveOwnedSteps(metadata, phase, accepted));
|
|
380
667
|
const settled = new Set();
|
|
381
668
|
for (const record of result.records) {
|
|
382
|
-
validateRecordShape(record, metadata);
|
|
669
|
+
validateRecordShape(record, metadata, phase);
|
|
383
670
|
if (!owned.has(record.stepId)) {
|
|
384
671
|
throw new PlanLedgerError(`phase '${phase}' reported step ${record.stepId}, which it does not own`);
|
|
385
672
|
}
|
|
@@ -392,7 +679,9 @@ export function validatePhaseResult(result, metadata, phase) {
|
|
|
392
679
|
if (missing.length > 0) {
|
|
393
680
|
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.`);
|
|
394
681
|
}
|
|
395
|
-
return result.records.map((record) =>
|
|
682
|
+
return result.records.map((record) => record.handoff !== undefined
|
|
683
|
+
? { ...record, phase, handoff: normalizeHandoff(record.handoff) }
|
|
684
|
+
: { ...record, phase });
|
|
396
685
|
}
|
|
397
686
|
// ---------------------------------------------------------------------------
|
|
398
687
|
// Durable merge
|
|
@@ -89,17 +89,43 @@ function findPlanMetadataCandidate(value, depth) {
|
|
|
89
89
|
}
|
|
90
90
|
return undefined;
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The ranges a phase owns, for display alongside `ownedSteps`.
|
|
94
|
+
*
|
|
95
|
+
* Class-owned phases report whole canonical parts. `post_pr_gap_close` owns single
|
|
96
|
+
* steps picked from the accepted ledger (BAPI-1426), so it reports one single-step
|
|
97
|
+
* range per owned step and never claims the rest of the part.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveOwnedRanges(metadata, phase, accepted) {
|
|
100
|
+
if (phase === "post_pr_gap_close") {
|
|
101
|
+
const ranges = [];
|
|
102
|
+
for (const step of resolveOwnedSteps(metadata, phase, accepted)) {
|
|
103
|
+
const part = metadata.parts.find((entry) => step >= entry.start_step && step <= entry.end_step);
|
|
104
|
+
if (!part)
|
|
105
|
+
continue;
|
|
106
|
+
ranges.push({
|
|
107
|
+
partId: part.part_id,
|
|
108
|
+
provenanceClass: part.provenance_class,
|
|
109
|
+
startStep: step,
|
|
110
|
+
endStep: step,
|
|
111
|
+
declaredAdvisory: part.declared_advisory,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return ranges;
|
|
115
|
+
}
|
|
116
|
+
return resolveOwnedParts(metadata, phase).map((part) => ({
|
|
117
|
+
partId: part.part_id,
|
|
118
|
+
provenanceClass: part.provenance_class,
|
|
119
|
+
startStep: part.start_step,
|
|
120
|
+
endStep: part.end_step,
|
|
121
|
+
declaredAdvisory: part.declared_advisory,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
export function buildPhaseRoutingContext(metadata, phase, ledger, checkpoint, accepted) {
|
|
93
125
|
return {
|
|
94
126
|
phase,
|
|
95
|
-
ownedSteps: resolveOwnedSteps(metadata, phase),
|
|
96
|
-
ownedRanges:
|
|
97
|
-
partId: part.part_id,
|
|
98
|
-
provenanceClass: part.provenance_class,
|
|
99
|
-
startStep: part.start_step,
|
|
100
|
-
endStep: part.end_step,
|
|
101
|
-
declaredAdvisory: part.declared_advisory,
|
|
102
|
-
})),
|
|
127
|
+
ownedSteps: resolveOwnedSteps(metadata, phase, accepted),
|
|
128
|
+
ownedRanges: resolveOwnedRanges(metadata, phase, accepted),
|
|
103
129
|
ledger: sanitizePlanStepRecords(ledger),
|
|
104
130
|
...(checkpoint ? { checkpoint } : {}),
|
|
105
131
|
};
|
|
@@ -120,7 +146,14 @@ export function injectPhaseRoutingContext(instruction, context) {
|
|
|
120
146
|
`provenance routing and the durable ledger as it stands right now. Settle every ` +
|
|
121
147
|
`step in \`ownedSteps\` — each must end as \`executed\`, \`adapted\`, \`escalated\`, ` +
|
|
122
148
|
`or \`unrun-advisory\`. Steps outside \`ownedSteps\` belong to another phase.\n\n` +
|
|
123
|
-
`
|
|
149
|
+
`In \`ledger\`, a step whose latest record is \`executed\` or \`adapted\` is ` +
|
|
150
|
+
`settled; do not redo it. A step whose latest record is \`escalated\` is an ` +
|
|
151
|
+
`unresolved obligation, not finished work. A record marked \`invalidatedBy\` is ` +
|
|
152
|
+
`history only.\n\n` +
|
|
153
|
+
`Only an \`escalated\` \`pre_pr_verification\` record for a check that genuinely ` +
|
|
154
|
+
`needs an open PR may add \`"prDependency"\` (one of \`remote_ci\`, \`pr_review\`, ` +
|
|
155
|
+
`\`pr_preview_environment\`), with an \`escalationReason\` naming that check. ` +
|
|
156
|
+
`Post-PR records that resolve those steps omit \`prDependency\`.\n\n` +
|
|
124
157
|
"```json\n" +
|
|
125
158
|
`${block}\n` +
|
|
126
159
|
"```\n\n" +
|
|
@@ -216,10 +249,12 @@ export function parseCheckpointResult(agentResult) {
|
|
|
216
249
|
* as `escalated` with the reason, which is what surfaces as unresolved. The failure
|
|
217
250
|
* this ticket exists to fix was work vanishing with no trace, not work being flagged.
|
|
218
251
|
*/
|
|
219
|
-
export function settlePhaseResult(agentResult, metadata, phase, existingLedger) {
|
|
252
|
+
export function settlePhaseResult(agentResult, metadata, phase, existingLedger, accepted) {
|
|
220
253
|
const envelope = parsePhaseResultEnvelope(agentResult);
|
|
221
254
|
if (!envelope) {
|
|
222
|
-
|
|
255
|
+
// Generated escalations cover only steps owned right now; for post-PR work that
|
|
256
|
+
// means only the server-accepted PR-dependent steps.
|
|
257
|
+
const owned = resolveOwnedSteps(metadata, phase, accepted);
|
|
223
258
|
if (owned.length === 0) {
|
|
224
259
|
return { ledger: [...existingLedger], contractViolation: false };
|
|
225
260
|
}
|
|
@@ -239,7 +274,7 @@ export function settlePhaseResult(agentResult, metadata, phase, existingLedger)
|
|
|
239
274
|
};
|
|
240
275
|
}
|
|
241
276
|
try {
|
|
242
|
-
const validated = validatePhaseResult(envelope, metadata, phase);
|
|
277
|
+
const validated = validatePhaseResult(envelope, metadata, phase, accepted);
|
|
243
278
|
return {
|
|
244
279
|
ledger: mergePlanStepRecords(existingLedger, sanitizePlanStepRecords(validated)),
|
|
245
280
|
contractViolation: false,
|