@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
|
@@ -38,12 +38,17 @@
|
|
|
38
38
|
* Filesystem only: no MCP transport, no network, no git, no subprocesses, and no
|
|
39
39
|
* process-level state. The caller supplies the trusted docs directory and the
|
|
40
40
|
* already-fetched plan metadata.
|
|
41
|
+
*
|
|
42
|
+
* BAPI-1426: post-PR ownership comes from a server-accepted ledger the adapter
|
|
43
|
+
* fetched and passed in as `acceptedLedger`; this module never fetches it. Stored
|
|
44
|
+
* post-PR records stay readable when their step was authorized at the time they
|
|
45
|
+
* were written, even if today's accepted state has moved on.
|
|
41
46
|
*/
|
|
42
47
|
import { createHash } from "crypto";
|
|
43
48
|
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
44
49
|
import path from "path";
|
|
45
|
-
import { PLAN_PHASES, PLAN_STEP_DISPOSITIONS, PlanLedgerError, currentLedgerState,
|
|
46
|
-
import { assertPlanIsRoutable } from "./plan-phase-routing.js";
|
|
50
|
+
import { PLAN_PHASES, PLAN_STEP_DISPOSITIONS, PlanLedgerError, currentLedgerState, isAcceptedPrDependentEscalation, resolveOwnedSteps, sanitizePlanStepRecords, unresolvedLedgerRecords, validatePhaseResult, validatePlanMetadata, } from "./plan-execution-ledger.js";
|
|
51
|
+
import { assertPlanIsRoutable, resolveOwnedRanges } from "./plan-phase-routing.js";
|
|
47
52
|
// ---------------------------------------------------------------------------
|
|
48
53
|
// Layout
|
|
49
54
|
// ---------------------------------------------------------------------------
|
|
@@ -94,7 +99,7 @@ export const CHECKPOINT_STALE_REASONS = [
|
|
|
94
99
|
// ---------------------------------------------------------------------------
|
|
95
100
|
// Path safety
|
|
96
101
|
// ---------------------------------------------------------------------------
|
|
97
|
-
function assertValidTicketKey(ticketKey) {
|
|
102
|
+
export function assertValidTicketKey(ticketKey) {
|
|
98
103
|
if (typeof ticketKey !== "string" || !TICKET_KEY_PATTERN.test(ticketKey.trim())) {
|
|
99
104
|
throw new PlanLedgerError("ticket key must match PROJ-123; it becomes a filename segment and is never sanitized into a different one");
|
|
100
105
|
}
|
|
@@ -106,7 +111,7 @@ function assertValidPhase(phase) {
|
|
|
106
111
|
}
|
|
107
112
|
return phase;
|
|
108
113
|
}
|
|
109
|
-
function assertDocsDir(docsDir) {
|
|
114
|
+
export function assertDocsDir(docsDir) {
|
|
110
115
|
if (typeof docsDir !== "string" || docsDir.trim() === "") {
|
|
111
116
|
throw new PlanLedgerError("docs directory is required and must be a non-empty path");
|
|
112
117
|
}
|
|
@@ -121,7 +126,7 @@ function assertDocsDir(docsDir) {
|
|
|
121
126
|
* Checked before any `mkdir`, read, temporary write, or rename — a containment
|
|
122
127
|
* check that runs after the directory exists has already had its side effect.
|
|
123
128
|
*/
|
|
124
|
-
function resolveWithinRoot(root, ...segments) {
|
|
129
|
+
export function resolveWithinRoot(root, ...segments) {
|
|
125
130
|
const resolved = path.resolve(root, ...segments);
|
|
126
131
|
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
127
132
|
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
@@ -169,7 +174,7 @@ function isMissingFileError(err) {
|
|
|
169
174
|
err !== null &&
|
|
170
175
|
err.code === "ENOENT");
|
|
171
176
|
}
|
|
172
|
-
async function readJsonArtifact(absolutePath, relativePath) {
|
|
177
|
+
export async function readJsonArtifact(absolutePath, relativePath) {
|
|
173
178
|
let raw;
|
|
174
179
|
try {
|
|
175
180
|
raw = await readFile(absolutePath, "utf8");
|
|
@@ -221,9 +226,16 @@ function validateArtifactEnvelope(parsed, relativePath, ticketKey, phase, finger
|
|
|
221
226
|
* vocabulary are still enforced — a record naming a step this phase does not own,
|
|
222
227
|
* or a disposition outside the set, means the artifact does not describe this
|
|
223
228
|
* plan and is refused rather than partially trusted.
|
|
229
|
+
*
|
|
230
|
+
* Post-PR records are the exception to "owned under the current plan": post-PR
|
|
231
|
+
* ownership moves as steps resolve, so a stored post-PR record is checked against
|
|
232
|
+
* `postPrAuthorized` — the steps authorized at the time it was written — rather
|
|
233
|
+
* than today's ownership set. See {@link postPrHistoricalAuthorization}.
|
|
224
234
|
*/
|
|
225
|
-
function assertStoredRecordsRoutable(records, metadata, phase, relativePath) {
|
|
226
|
-
const owned =
|
|
235
|
+
function assertStoredRecordsRoutable(records, metadata, phase, relativePath, postPrAuthorized = new Set()) {
|
|
236
|
+
const owned = phase === "post_pr_gap_close"
|
|
237
|
+
? new Set([...postPrAuthorized].filter((step) => stepIsInPlan(metadata, step)))
|
|
238
|
+
: new Set(resolveOwnedSteps(metadata, phase));
|
|
227
239
|
for (const record of records) {
|
|
228
240
|
if (typeof record !== "object" || record === null) {
|
|
229
241
|
throw new PlanLedgerError(`${relativePath} holds a record that is not an object`);
|
|
@@ -237,6 +249,46 @@ function assertStoredRecordsRoutable(records, metadata, phase, relativePath) {
|
|
|
237
249
|
}
|
|
238
250
|
return records.map((record) => ({ ...record, phase }));
|
|
239
251
|
}
|
|
252
|
+
function stepIsInPlan(metadata, stepId) {
|
|
253
|
+
return metadata.parts.some((part) => stepId >= part.start_step && stepId <= part.end_step);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Steps a stored post-PR record may name: those authorized for post-PR work at any
|
|
257
|
+
* point in history.
|
|
258
|
+
*
|
|
259
|
+
* With an accepted ledger, that is every step that ever had an accepted PR-dependent
|
|
260
|
+
* escalation — not just the ones still open today, since a resolved step's post-PR
|
|
261
|
+
* record is exactly what resolved it. Without one, the local pre-PR records stand in
|
|
262
|
+
* for reading history only: a local escalated record carrying `prDependency` makes
|
|
263
|
+
* an already-written post-PR record readable. Ownership for a NEW post-PR write
|
|
264
|
+
* still comes only from the accepted ledger.
|
|
265
|
+
*/
|
|
266
|
+
function postPrHistoricalAuthorization(accepted, localPrePrRecords) {
|
|
267
|
+
const authorized = new Set();
|
|
268
|
+
if (accepted && Array.isArray(accepted.acceptedRecords)) {
|
|
269
|
+
for (const record of accepted.acceptedRecords) {
|
|
270
|
+
if (record && isAcceptedPrDependentEscalation(record))
|
|
271
|
+
authorized.add(record.step_id);
|
|
272
|
+
}
|
|
273
|
+
return authorized;
|
|
274
|
+
}
|
|
275
|
+
for (const record of localPrePrRecords) {
|
|
276
|
+
if (record.disposition === "escalated" && record.prDependency !== undefined) {
|
|
277
|
+
authorized.add(record.stepId);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return authorized;
|
|
281
|
+
}
|
|
282
|
+
/** Read and route-check the stored pre-PR records, or none when absent. */
|
|
283
|
+
async function readStoredPrePrRecords(docsDir, ticketKey, metadata, fingerprint) {
|
|
284
|
+
const relativePath = phaseResultRelativePath(ticketKey, "pre_pr_verification");
|
|
285
|
+
const absolutePath = resolveWithinRoot(docsDir, relativePath);
|
|
286
|
+
const parsed = await readJsonArtifact(absolutePath, relativePath);
|
|
287
|
+
if (parsed === undefined)
|
|
288
|
+
return [];
|
|
289
|
+
return assertStoredRecordsRoutable(validateArtifactEnvelope(parsed, relativePath, ticketKey, "pre_pr_verification", fingerprint)
|
|
290
|
+
.records, metadata, "pre_pr_verification", relativePath);
|
|
291
|
+
}
|
|
240
292
|
/**
|
|
241
293
|
* Append `incoming` to `existing`, refusing to walk a settled step backwards.
|
|
242
294
|
*
|
|
@@ -282,6 +334,7 @@ export async function loadInlinePhaseContext(options) {
|
|
|
282
334
|
assertPlanIsRoutable(metadata);
|
|
283
335
|
const fingerprint = planFingerprint(metadata);
|
|
284
336
|
let ledger = [];
|
|
337
|
+
let prePrRecords = [];
|
|
285
338
|
const artifactPaths = [];
|
|
286
339
|
// Latest record index per produce step, within the produce artifact's own list.
|
|
287
340
|
const produceLatestIndex = new Map();
|
|
@@ -297,7 +350,11 @@ export async function loadInlinePhaseContext(options) {
|
|
|
297
350
|
const artifact = validateArtifactEnvelope(parsed, relativePath, ticketKey, priorPhase, fingerprint);
|
|
298
351
|
// Re-check against the CURRENT metadata: an artifact that was valid when
|
|
299
352
|
// written must still describe steps this phase owns under the plan in play now.
|
|
300
|
-
const routable = assertStoredRecordsRoutable(artifact.records, metadata, priorPhase, relativePath
|
|
353
|
+
const routable = assertStoredRecordsRoutable(artifact.records, metadata, priorPhase, relativePath, priorPhase === "post_pr_gap_close"
|
|
354
|
+
? postPrHistoricalAuthorization(options.acceptedLedger, prePrRecords)
|
|
355
|
+
: undefined);
|
|
356
|
+
if (priorPhase === "pre_pr_verification")
|
|
357
|
+
prePrRecords = routable;
|
|
301
358
|
if (priorPhase === "produce") {
|
|
302
359
|
produceRecordCount = routable.length;
|
|
303
360
|
routable.forEach((record, index) => produceLatestIndex.set(record.stepId, index));
|
|
@@ -367,14 +424,8 @@ export async function loadInlinePhaseContext(options) {
|
|
|
367
424
|
return {
|
|
368
425
|
phase,
|
|
369
426
|
routingSource: INLINE_ROUTING_SOURCE,
|
|
370
|
-
ownedSteps: resolveOwnedSteps(metadata, phase),
|
|
371
|
-
ownedRanges:
|
|
372
|
-
partId: part.part_id,
|
|
373
|
-
provenanceClass: part.provenance_class,
|
|
374
|
-
startStep: part.start_step,
|
|
375
|
-
endStep: part.end_step,
|
|
376
|
-
declaredAdvisory: part.declared_advisory,
|
|
377
|
-
})),
|
|
427
|
+
ownedSteps: resolveOwnedSteps(metadata, phase, options.acceptedLedger),
|
|
428
|
+
ownedRanges: resolveOwnedRanges(metadata, phase, options.acceptedLedger),
|
|
378
429
|
ledger,
|
|
379
430
|
currentRecordsByStep,
|
|
380
431
|
terminalStepIds,
|
|
@@ -507,7 +558,7 @@ async function readProduceCoverage(docsDir, ticketKey) {
|
|
|
507
558
|
* half-written ledger. The temp file is removed on failure so a rejected write
|
|
508
559
|
* leaves nothing behind to be mistaken for state.
|
|
509
560
|
*/
|
|
510
|
-
async function writeAtomically(absolutePath, relativePath, payload) {
|
|
561
|
+
export async function writeAtomically(absolutePath, relativePath, payload) {
|
|
511
562
|
const tempPath = `${absolutePath}.tmp`;
|
|
512
563
|
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
513
564
|
try {
|
|
@@ -539,7 +590,7 @@ export async function writeInlinePhaseResult(options) {
|
|
|
539
590
|
throw new PlanLedgerError("phase result must be an object");
|
|
540
591
|
}
|
|
541
592
|
const phase = assertValidPhase(envelope.phase);
|
|
542
|
-
const validated = validatePhaseResult(envelope, metadata, phase);
|
|
593
|
+
const validated = validatePhaseResult(envelope, metadata, phase, options.acceptedLedger);
|
|
543
594
|
const sanitized = sanitizePlanStepRecords(validated);
|
|
544
595
|
const relativePath = phaseResultRelativePath(ticketKey, phase);
|
|
545
596
|
const absolutePath = resolveWithinRoot(docsDir, relativePath);
|
|
@@ -549,7 +600,11 @@ export async function writeInlinePhaseResult(options) {
|
|
|
549
600
|
const existing = await readJsonArtifact(absolutePath, relativePath);
|
|
550
601
|
let priorRecords = [];
|
|
551
602
|
if (existing !== undefined) {
|
|
552
|
-
priorRecords = assertStoredRecordsRoutable(validateArtifactEnvelope(existing, relativePath, ticketKey, phase, fingerprint).records, metadata, phase, relativePath
|
|
603
|
+
priorRecords = assertStoredRecordsRoutable(validateArtifactEnvelope(existing, relativePath, ticketKey, phase, fingerprint).records, metadata, phase, relativePath, phase === "post_pr_gap_close"
|
|
604
|
+
? postPrHistoricalAuthorization(options.acceptedLedger, options.acceptedLedger
|
|
605
|
+
? []
|
|
606
|
+
: await readStoredPrePrRecords(docsDir, ticketKey, metadata, fingerprint))
|
|
607
|
+
: undefined);
|
|
553
608
|
}
|
|
554
609
|
const merged = appendInlineRecords(priorRecords, sanitized);
|
|
555
610
|
const artifact = {
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
20
|
import { resolveRecipe } from "./pipeline-utils.js";
|
|
21
21
|
import { parseUnsupportedInLocalModeEnvelope } from "./tool-error-envelope.js";
|
|
22
|
+
import { TICKET_KEY_PATTERN } from "./ticket-key-utils.js";
|
|
22
23
|
import { PlanLedgerError } from "./plan-execution-ledger.js";
|
|
24
|
+
import { enqueueAndSync, enqueueExistingLocalRecords, ensureDeliveryBinding, fetchAcceptedLedgerContext, serverShaOrUndefined, syncPendingSubmissions, } from "./ticket-delivery-enrollment.js";
|
|
25
|
+
import { getReadiness, } from "./ticket-delivery.js";
|
|
23
26
|
import { assertPlanIsRoutable, buildPhaseRoutingContext, extractPlanMetadata, injectPhaseRoutingContext, parseCheckpointResult, phaseForInstructionFile, settlePhaseResult, } from "./plan-phase-routing.js";
|
|
24
27
|
/**
|
|
25
28
|
* Derive a STABLE, content-addressed hash of an idea — the basis for the
|
|
@@ -363,7 +366,15 @@ export async function resumePipeline(deps, input) {
|
|
|
363
366
|
const phase = phaseForInstructionFile(step.instruction_file);
|
|
364
367
|
const planMetadata = recordedPlanMetadata(accumulatedResults);
|
|
365
368
|
if (phase && planMetadata) {
|
|
366
|
-
|
|
369
|
+
// BAPI-1426: the binding normally exists already (made before the first
|
|
370
|
+
// routed phase paused). Post-PR settlement uses the freshly accepted server
|
|
371
|
+
// ledger, so a local-only dependency mark cannot claim ownership.
|
|
372
|
+
const binding = await ensureOrchestratedBinding(deps, recipe, accumulatedResults);
|
|
373
|
+
const accepted = phase === "post_pr_gap_close"
|
|
374
|
+
? await refreshAcceptedLedger(deps, recipe, binding)
|
|
375
|
+
: undefined;
|
|
376
|
+
const existingLedger = recordedLedger(accumulatedResults);
|
|
377
|
+
const outcome = settlePhaseResult(input.agent_result, planMetadata, phase, existingLedger, accepted);
|
|
367
378
|
entry.plan_step_records = outcome.ledger;
|
|
368
379
|
if (outcome.diagnostic) {
|
|
369
380
|
entry.plan_routing_diagnostic = outcome.diagnostic;
|
|
@@ -397,6 +408,13 @@ export async function resumePipeline(deps, input) {
|
|
|
397
408
|
results: accumulatedResults,
|
|
398
409
|
};
|
|
399
410
|
}
|
|
411
|
+
// BAPI-1426: queue the records this phase settled (a reported envelope or
|
|
412
|
+
// a generated escalation) and sync them. The ledger only ever appends, so
|
|
413
|
+
// the new records are the tail past the prior ledger. A sync failure keeps
|
|
414
|
+
// the batch pending; it never fails the run or claims delivery.
|
|
415
|
+
const deliverySync = await syncSettledPhase(deps, recipe, binding, phase, outcome.ledger.slice(existingLedger.length));
|
|
416
|
+
if (deliverySync)
|
|
417
|
+
entry.delivery_sync = deliverySync;
|
|
400
418
|
}
|
|
401
419
|
accumulatedResults.push(entry);
|
|
402
420
|
nextIndex = stepIndex + 1;
|
|
@@ -570,6 +588,214 @@ function attachPlanMetadata(entry) {
|
|
|
570
588
|
console.error(`[pipeline] plan routing unavailable: ${message}`);
|
|
571
589
|
}
|
|
572
590
|
}
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// BAPI-1426 — managed delivery, shared with the inline tools
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
/** Full adapter deps for this run, or undefined when delivery is not wired. */
|
|
595
|
+
function deliveryAdapter(deps) {
|
|
596
|
+
const delivery = deps.ticketDelivery;
|
|
597
|
+
if (!delivery)
|
|
598
|
+
return undefined;
|
|
599
|
+
return {
|
|
600
|
+
client: delivery.client ?? {
|
|
601
|
+
baseUrl: deps.baseUrl,
|
|
602
|
+
headers: async () => ({ "X-API-Key": deps.apiKey }),
|
|
603
|
+
},
|
|
604
|
+
repoName: deps.repoName,
|
|
605
|
+
docsDir: deps.docsDir,
|
|
606
|
+
resolveSourceBranch: delivery.resolveSourceBranch,
|
|
607
|
+
resolvedBaseBranch: delivery.resolvedBaseBranch?.trim() || "main",
|
|
608
|
+
...(delivery.executorJobId !== undefined ? { executorJobId: delivery.executorJobId } : {}),
|
|
609
|
+
...(delivery.store ? { store: delivery.store } : {}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
/** The ticket a recipe works on, read from its resolved `ticket_number` params. */
|
|
613
|
+
function recipeTicketKey(recipe) {
|
|
614
|
+
for (const step of recipe.steps) {
|
|
615
|
+
const value = step.params?.ticket_number;
|
|
616
|
+
if (typeof value !== "string")
|
|
617
|
+
continue;
|
|
618
|
+
const key = value.trim().toUpperCase();
|
|
619
|
+
if (TICKET_KEY_PATTERN.test(key))
|
|
620
|
+
return key;
|
|
621
|
+
}
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
/** True when the recipe has at least one routed plan phase. */
|
|
625
|
+
function recipeHasRoutedPhases(recipe) {
|
|
626
|
+
return recipe.steps.some((step) => phaseForInstructionFile(step.instruction_file) !== undefined);
|
|
627
|
+
}
|
|
628
|
+
/** The binding recorded on an earlier entry, if any. */
|
|
629
|
+
function recordedDeliveryBinding(results) {
|
|
630
|
+
for (let i = results.length - 1; i >= 0; i -= 1) {
|
|
631
|
+
const binding = results[i].delivery_binding;
|
|
632
|
+
if (binding)
|
|
633
|
+
return binding;
|
|
634
|
+
}
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Bind this run to a server attempt, once, and record the binding on the latest
|
|
639
|
+
* entry so the next persistence write carries it. Never throws. A run that was
|
|
640
|
+
* refused enrollment (`disabled`) is not asked again; an `unavailable` one is.
|
|
641
|
+
*/
|
|
642
|
+
async function ensureOrchestratedBinding(deps, recipe, results) {
|
|
643
|
+
const adapter = deliveryAdapter(deps);
|
|
644
|
+
const ticketKey = recipeTicketKey(recipe);
|
|
645
|
+
if (!adapter || !ticketKey || results.length === 0)
|
|
646
|
+
return undefined;
|
|
647
|
+
const recorded = recordedDeliveryBinding(results);
|
|
648
|
+
if (recorded?.binding)
|
|
649
|
+
return recorded.binding;
|
|
650
|
+
if (recorded?.enrollment === "disabled")
|
|
651
|
+
return undefined;
|
|
652
|
+
const ensured = await ensureDeliveryBinding(adapter, ticketKey);
|
|
653
|
+
if (ensured.binding && ensured.newlyRegistered) {
|
|
654
|
+
// Work recorded before this attempt existed is sent once for reconciliation.
|
|
655
|
+
try {
|
|
656
|
+
await enqueueExistingLocalRecords(adapter, ticketKey, recordedLedger(results));
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
console.error(`[pipeline] could not queue existing local records: ${String(err)}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const last = results.length - 1;
|
|
663
|
+
results[last] = {
|
|
664
|
+
...results[last],
|
|
665
|
+
delivery_binding: {
|
|
666
|
+
enrollment: ensured.enrollment,
|
|
667
|
+
attempt_id: ensured.binding?.attempt_id ?? null,
|
|
668
|
+
...(ensured.binding ? { binding: ensured.binding } : {}),
|
|
669
|
+
...(ensured.reason ? { reason: ensured.reason } : {}),
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
return ensured.binding;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* The server-accepted ledger, refreshed right before post-PR routing or
|
|
676
|
+
* settlement. Pending batches are retried first so ownership reflects the latest
|
|
677
|
+
* accepted evidence. Undefined (owns nothing) when unbound or unreadable: an
|
|
678
|
+
* unsynchronized local `prDependency` mark never creates post-PR ownership.
|
|
679
|
+
*/
|
|
680
|
+
async function refreshAcceptedLedger(deps, recipe, binding) {
|
|
681
|
+
const adapter = deliveryAdapter(deps);
|
|
682
|
+
const ticketKey = recipeTicketKey(recipe);
|
|
683
|
+
if (!adapter || !ticketKey || !binding)
|
|
684
|
+
return undefined;
|
|
685
|
+
await syncPendingSubmissions(adapter, ticketKey, binding);
|
|
686
|
+
const fetched = await fetchAcceptedLedgerContext(adapter, binding);
|
|
687
|
+
if (!fetched.ok) {
|
|
688
|
+
console.error(`[pipeline] accepted ledger unavailable: ${fetched.error}`);
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
return fetched.context;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Queue and sync the records one routed phase just settled — a reported envelope
|
|
695
|
+
* or a generated missing-envelope escalation alike. The journal write is local and
|
|
696
|
+
* comes first; a server or network failure leaves the batch pending.
|
|
697
|
+
*/
|
|
698
|
+
async function syncSettledPhase(deps, recipe, binding, phase, newRecords) {
|
|
699
|
+
const adapter = deliveryAdapter(deps);
|
|
700
|
+
const ticketKey = recipeTicketKey(recipe);
|
|
701
|
+
if (!adapter || !ticketKey || newRecords.length === 0)
|
|
702
|
+
return undefined;
|
|
703
|
+
const read = deps.ticketDelivery?.readGitValue;
|
|
704
|
+
const head = read ? serverShaOrUndefined(await read(["rev-parse", "HEAD"]).catch(() => undefined)) : undefined;
|
|
705
|
+
const tree = read
|
|
706
|
+
? serverShaOrUndefined(await read(["rev-parse", "HEAD^{tree}"]).catch(() => undefined))
|
|
707
|
+
: undefined;
|
|
708
|
+
const payload = {
|
|
709
|
+
phase_result: { version: 1, phase, records: newRecords },
|
|
710
|
+
...(head ? { tested_head_sha: head } : {}),
|
|
711
|
+
...(tree ? { tested_tree_sha: tree } : {}),
|
|
712
|
+
};
|
|
713
|
+
try {
|
|
714
|
+
const { submission, sync } = await enqueueAndSync(adapter, ticketKey, binding, { phase, payload });
|
|
715
|
+
const report = sync?.reports.find((r) => r.submission_id === submission.submission_id);
|
|
716
|
+
const stored = report !== undefined && report.operation_status !== null && !report.retryable;
|
|
717
|
+
const error = report?.error ??
|
|
718
|
+
(!binding
|
|
719
|
+
? "no managed binding; the batch is kept locally"
|
|
720
|
+
: !report
|
|
721
|
+
? "an earlier pending batch was not accepted yet; this batch stays pending"
|
|
722
|
+
: undefined);
|
|
723
|
+
return {
|
|
724
|
+
submission_id: submission.submission_id,
|
|
725
|
+
operation_status: report?.operation_status ?? null,
|
|
726
|
+
refusals: report?.refusals ?? [],
|
|
727
|
+
pending: !stored,
|
|
728
|
+
...(error ? { error } : {}),
|
|
729
|
+
readiness_outcome: sync?.readiness?.outcome ?? null,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
return {
|
|
734
|
+
submission_id: null,
|
|
735
|
+
operation_status: null,
|
|
736
|
+
refusals: [],
|
|
737
|
+
pending: false,
|
|
738
|
+
error: `the local delivery journal could not be updated: ${String(err instanceof Error ? err.message : err)}`,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/** The server's delivery decision for the completed envelope. Never throws. */
|
|
743
|
+
async function deliveryOutcomeForCompletion(deps, recipe, results) {
|
|
744
|
+
const recorded = recordedDeliveryBinding(results);
|
|
745
|
+
const base = {
|
|
746
|
+
managed: false,
|
|
747
|
+
enrollment: recorded?.enrollment ?? "not_configured",
|
|
748
|
+
attempt_id: recorded?.attempt_id ?? null,
|
|
749
|
+
outcome: null,
|
|
750
|
+
blockers: [],
|
|
751
|
+
identities: {},
|
|
752
|
+
preserved_branch: null,
|
|
753
|
+
preserved_head_sha: null,
|
|
754
|
+
publication: null,
|
|
755
|
+
checkpoint_only: false,
|
|
756
|
+
delivery_complete: false,
|
|
757
|
+
};
|
|
758
|
+
const adapter = deliveryAdapter(deps);
|
|
759
|
+
const ticketKey = recipeTicketKey(recipe);
|
|
760
|
+
if (!adapter || !ticketKey)
|
|
761
|
+
return { ...base, enrollment: "not_configured" };
|
|
762
|
+
const binding = recorded?.binding;
|
|
763
|
+
if (!binding) {
|
|
764
|
+
return {
|
|
765
|
+
...base,
|
|
766
|
+
...(recorded?.reason ? { unavailable_reason: recorded.reason } : {}),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const pass = await syncPendingSubmissions(adapter, ticketKey, binding);
|
|
770
|
+
const response = await getReadiness(adapter.client, binding.attempt_id, {
|
|
771
|
+
repo_name: deps.repoName,
|
|
772
|
+
boundary: "publish",
|
|
773
|
+
});
|
|
774
|
+
if (!response.ok) {
|
|
775
|
+
return {
|
|
776
|
+
...base,
|
|
777
|
+
managed: true,
|
|
778
|
+
pending_submissions: pass.pending,
|
|
779
|
+
unavailable_reason: `${response.error}: ${response.message}`,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const readiness = response.body.readiness;
|
|
783
|
+
const publication = readiness?.publication ?? null;
|
|
784
|
+
return {
|
|
785
|
+
...base,
|
|
786
|
+
managed: true,
|
|
787
|
+
outcome: readiness?.outcome ?? null,
|
|
788
|
+
boundary: readiness?.boundary,
|
|
789
|
+
blockers: Array.isArray(readiness?.blockers) ? readiness.blockers : [],
|
|
790
|
+
identities: readiness?.identities ?? {},
|
|
791
|
+
preserved_branch: readiness?.preserved_branch ?? binding.source_branch,
|
|
792
|
+
preserved_head_sha: readiness?.preserved_head_sha ?? null,
|
|
793
|
+
publication,
|
|
794
|
+
checkpoint_only: publication?.action === "checkpoint_only",
|
|
795
|
+
delivery_complete: readiness?.delivery_complete === true,
|
|
796
|
+
pending_submissions: pass.pending,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
573
799
|
async function continuePipelineExecution(deps, persistence, row, recipe, autoApprove, forceExecuteAtIndex, approvalAgentResult) {
|
|
574
800
|
let pipelineRunId = row.pipeline_run_id;
|
|
575
801
|
let stepIndex = row.current_step_index;
|
|
@@ -618,6 +844,19 @@ async function continuePipelineExecution(deps, persistence, row, recipe, autoApp
|
|
|
618
844
|
};
|
|
619
845
|
}
|
|
620
846
|
if (step.type === "agent_task") {
|
|
847
|
+
const phase = phaseForInstructionFile(step.instruction_file);
|
|
848
|
+
const planMetadata = recordedPlanMetadata(results);
|
|
849
|
+
// BAPI-1426: enroll before the first routed phase (the production task)
|
|
850
|
+
// starts, and persist the binding with the pause below so a resumed run or
|
|
851
|
+
// a fresh process reuses the same attempt. Before post-PR routing, refresh
|
|
852
|
+
// the server-accepted ledger: it alone decides post-PR ownership.
|
|
853
|
+
let accepted;
|
|
854
|
+
if (phase && planMetadata) {
|
|
855
|
+
const binding = await ensureOrchestratedBinding(deps, recipe, results);
|
|
856
|
+
if (phase === "post_pr_gap_close") {
|
|
857
|
+
accepted = await refreshAcceptedLedger(deps, recipe, binding);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
621
860
|
try {
|
|
622
861
|
await persistence.patchRun(pipelineRunId, {
|
|
623
862
|
current_step_index: stepIndex,
|
|
@@ -639,10 +878,8 @@ async function continuePipelineExecution(deps, persistence, row, recipe, autoApp
|
|
|
639
878
|
// hand-off did not survive — the recipe pauses, and the session that resumes
|
|
640
879
|
// may be a different one.
|
|
641
880
|
let instruction = step.instruction ?? "";
|
|
642
|
-
const phase = phaseForInstructionFile(step.instruction_file);
|
|
643
|
-
const planMetadata = recordedPlanMetadata(results);
|
|
644
881
|
if (phase && planMetadata) {
|
|
645
|
-
instruction = injectPhaseRoutingContext(instruction, buildPhaseRoutingContext(planMetadata, phase, recordedLedger(results), recordedCheckpoint(results)));
|
|
882
|
+
instruction = injectPhaseRoutingContext(instruction, buildPhaseRoutingContext(planMetadata, phase, recordedLedger(results), recordedCheckpoint(results), accepted));
|
|
646
883
|
}
|
|
647
884
|
return {
|
|
648
885
|
status: "needs_agent_task",
|
|
@@ -752,12 +989,18 @@ async function continuePipelineExecution(deps, persistence, row, recipe, autoApp
|
|
|
752
989
|
step_index: step.step,
|
|
753
990
|
});
|
|
754
991
|
}
|
|
992
|
+
// BAPI-1426: recipe completion is not ticket delivery. A routed recipe reports
|
|
993
|
+
// the server's delivery decision separately; a checkpoint-only outcome is a
|
|
994
|
+
// supported exit and leaves the recipe status `completed`.
|
|
755
995
|
return {
|
|
756
996
|
status: "completed",
|
|
757
997
|
pipeline_run_id: pipelineRunId,
|
|
758
998
|
pipeline: recipe.pipeline,
|
|
759
999
|
total_steps: totalSteps,
|
|
760
1000
|
results,
|
|
1001
|
+
...(recipeHasRoutedPhases(recipe)
|
|
1002
|
+
? { delivery: await deliveryOutcomeForCompletion(deps, recipe, results) }
|
|
1003
|
+
: {}),
|
|
761
1004
|
};
|
|
762
1005
|
}
|
|
763
1006
|
function handlePersistenceErrorForEnvelope(err, extras) {
|