@bridge_gpt/mcp-server 0.2.69 → 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 +140 -292
- 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 +23 -4
- package/build/claude-review-workflow-drift.js +3 -3
- package/build/codex-bridge-profile.js +1 -1
- package/build/commands.generated.js +6 -6
- 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/credential-store.js +356 -13
- package/build/docs.generated.js +26 -13
- package/build/doctor.js +94 -32
- 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 +57 -41
- 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 +66 -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 +72 -0
- 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 +1067 -536
- 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 +376 -60
- 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/setup-epic.js +400 -30
- package/build/shared-resources.generated.js +5 -0
- package/build/start-tickets.js +28 -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/upgrade-cli.js +13 -0
- 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
|
@@ -15,19 +15,47 @@
|
|
|
15
15
|
* the per-task lifecycle, keyed by the FULL `tool_use_id`, says whether the work
|
|
16
16
|
* the worker started actually finished.
|
|
17
17
|
*
|
|
18
|
+
* A task ID may identify sequential runs, each with its own tool_use_id.
|
|
19
|
+
* Lifecycle-terminal failure permits a new run but need not resolve execution.
|
|
20
|
+
*
|
|
18
21
|
* Two places deliberately differ from the probe reducer (`reducer-v2.mjs`), on
|
|
19
22
|
* the user-approved ticket's terms:
|
|
20
|
-
* - A REQUIRED task that ended `failed
|
|
21
|
-
* ran its own `TaskStop`)
|
|
22
|
-
*
|
|
23
|
+
* - A REQUIRED task that ended `failed` or `stopped` stays UNRESOLVED. A worker
|
|
24
|
+
* withdrawal (the worker ran its own `TaskStop`) and a background command
|
|
25
|
+
* that failed inside the startup window (`failed_to_start`) resolve
|
|
26
|
+
* EXECUTION only: the work is no longer running, but nothing here says it
|
|
27
|
+
* succeeded or that anything was verified. Declared verification still
|
|
28
|
+
* depends only on verification facts.
|
|
29
|
+
* - BAPI-1455: a required COMMAND (`bash_task`, `bash_bg`, `subagent_bash`)
|
|
30
|
+
* that ended `failed` or `stopped` is resolved when the worker received
|
|
31
|
+
* that command's OWN terminal result — today only its foreground
|
|
32
|
+
* tool_result, bound by the exact tool_use_id. The recorded state stays the
|
|
33
|
+
* real one and the envelope carries `result_delivered: true` beside it.
|
|
34
|
+
* Delegates, monitors, denials, launch acknowledgements, and background
|
|
35
|
+
* output reads never count: no background result shape has been measured
|
|
36
|
+
* that binds to its task by structure rather than text, so background
|
|
37
|
+
* commands stay unresolved exactly as before.
|
|
23
38
|
* - A denial links to an obligation only on the exact OPERATION and TARGET (for
|
|
24
39
|
* example the prepared branch on `origin`), never on the effect class alone.
|
|
25
40
|
*
|
|
26
|
-
* Purity: no I/O, no real timers (the supervisor takes injected timer deps
|
|
41
|
+
* Purity: no I/O, no real timers (the supervisor takes injected timer deps; the
|
|
42
|
+
* ledger itself only reads an injected clock and starts no timers), no
|
|
27
43
|
* retained prompt, command, argument, URL, or path. Tool inputs and denial
|
|
28
44
|
* targets are kept only as SHA-256 digests.
|
|
29
45
|
*/
|
|
30
|
-
import { MAX_DENIALS, MAX_EVENT_BYTES, MAX_TASKS,
|
|
46
|
+
import { DELIVERY_RESOLVABLE_KINDS, DELIVERY_RESOLVABLE_STATES, MAX_DENIALS, MAX_EVENT_BYTES, MAX_TASKS, boundEnvelope, boundedIdentifier, deriveCompletionCause, digestStructured, isTaskResolved, runtimeMatchesSupported, successRefusalReason, COMPLETION_CONTRACT_VERSION, COMPLETION_CONTRACT_VERSION_V2, } from "./completion-contract.js";
|
|
47
|
+
/**
|
|
48
|
+
* A background command that fails this soon after its launch acknowledgement is
|
|
49
|
+
* classified `failed_to_start` and resolves execution of a required task.
|
|
50
|
+
*
|
|
51
|
+
* Accepted risk: a background verification command that genuinely fails inside
|
|
52
|
+
* the window also resolves execution. The envelope keeps the visible
|
|
53
|
+
* `failed_to_start` state and does not establish verification. Required CI and
|
|
54
|
+
* merge admission stay fail-closed, and the existing CI-failure and
|
|
55
|
+
* observation-timeout notices remain the alert paths. A check outside required
|
|
56
|
+
* CI is an accepted gap whose exposure can last one CI cycle.
|
|
57
|
+
*/
|
|
58
|
+
export const FAILED_TO_START_WINDOW_MS = 30_000;
|
|
31
59
|
// ---------------------------------------------------------------------------
|
|
32
60
|
// Bounds and small pure helpers
|
|
33
61
|
// ---------------------------------------------------------------------------
|
|
@@ -309,12 +337,15 @@ const TERMINAL_STATES = new Set([
|
|
|
309
337
|
"failed",
|
|
310
338
|
"stopped",
|
|
311
339
|
"withdrawn",
|
|
340
|
+
"failed_to_start",
|
|
312
341
|
"conflict",
|
|
313
342
|
]);
|
|
343
|
+
const FAILED_TO_START_KINDS = new Set(["bash_bg", "bash_task", "subagent_bash"]);
|
|
314
344
|
const KNOWN_TASK_STATUSES = new Set(["pending", "running", "completed", "failed", "stopped", "killed"]);
|
|
315
345
|
export function createRequiredWorkLedger(init) {
|
|
316
346
|
const binding = { ...init.binding };
|
|
317
347
|
const lifecycleSupported = init.lifecycleSupported === true;
|
|
348
|
+
const clock = init.now ?? Date.now;
|
|
318
349
|
const optionalWatchers = (init.optionalWatchers ?? []).slice(0, 16).map((w) => ({ ...w }));
|
|
319
350
|
// An obligation whose target cannot be normalized links nothing: a denial of
|
|
320
351
|
// it then reads `unlinked`/`ambiguous`, and unmet finalization still refuses
|
|
@@ -326,7 +357,8 @@ export function createRequiredWorkLedger(init) {
|
|
|
326
357
|
const obligations = new Map();
|
|
327
358
|
const evicted = new Map();
|
|
328
359
|
const toolUses = new Map();
|
|
329
|
-
const workerStops = new Set();
|
|
360
|
+
const workerStops = new Set(); // bound obligation keys
|
|
361
|
+
const deferredWorkerStops = new Set(); // consumed at first association
|
|
330
362
|
const denials = [];
|
|
331
363
|
let latestSnapshot = null;
|
|
332
364
|
let materialSeq = 0;
|
|
@@ -352,7 +384,7 @@ export function createRequiredWorkLedger(init) {
|
|
|
352
384
|
continue;
|
|
353
385
|
if (latestSnapshot?.includes(o.taskId ?? "\u0000"))
|
|
354
386
|
continue;
|
|
355
|
-
evicted.set(o.key, { state: o.state, taskId: o.taskId, kind: o.kind, inputDigest: o.inputDigest, order: o.order });
|
|
387
|
+
evicted.set(o.key, { key: o.key, state: o.state, taskId: o.taskId, kind: o.kind, inputDigest: o.inputDigest, order: o.order });
|
|
356
388
|
obligations.delete(o.key);
|
|
357
389
|
return true;
|
|
358
390
|
}
|
|
@@ -372,18 +404,48 @@ export function createRequiredWorkLedger(init) {
|
|
|
372
404
|
key,
|
|
373
405
|
state: "pending",
|
|
374
406
|
order: ++orderSeq,
|
|
407
|
+
launchedAt: null,
|
|
408
|
+
resultDelivered: false,
|
|
409
|
+
failedByResult: false,
|
|
375
410
|
...spec,
|
|
376
411
|
required: spec.required ?? !isOptional(spec.kind, spec.inputDigest),
|
|
377
412
|
};
|
|
378
413
|
obligations.set(key, o);
|
|
414
|
+
if (o.taskId !== null)
|
|
415
|
+
bindDeferredStop(o);
|
|
379
416
|
return o;
|
|
380
417
|
};
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
418
|
+
const bindDeferredStop = (o) => {
|
|
419
|
+
if (o.taskId !== null && deferredWorkerStops.delete(o.taskId))
|
|
420
|
+
workerStops.add(o.key);
|
|
421
|
+
};
|
|
422
|
+
/** Record the clock at the FIRST launch event only; a later event never moves it. */
|
|
423
|
+
const recordLaunch = (o) => {
|
|
424
|
+
if (o.launchedAt !== null || o.state !== "pending")
|
|
425
|
+
return;
|
|
426
|
+
o.launchedAt = clock();
|
|
427
|
+
};
|
|
428
|
+
/** `failed_to_start` only for a first failure of background work inside the startup window. */
|
|
429
|
+
const classifyFailure = (o) => {
|
|
430
|
+
if (o.state !== "pending" || !o.background || !FAILED_TO_START_KINDS.has(o.kind) || o.launchedAt === null)
|
|
431
|
+
return "failed";
|
|
432
|
+
const elapsed = clock() - o.launchedAt;
|
|
433
|
+
return Number.isFinite(elapsed) && elapsed >= 0 && elapsed <= FAILED_TO_START_WINDOW_MS ? "failed_to_start" : "failed";
|
|
434
|
+
};
|
|
435
|
+
/** Prefer a live run, then the newest lifecycle-terminal run, including evictions. */
|
|
436
|
+
const findCurrentRunByTaskId = (taskId) => {
|
|
437
|
+
let current;
|
|
438
|
+
for (const o of [...obligations.values(), ...evicted.values()]) {
|
|
439
|
+
if (o.taskId !== taskId)
|
|
440
|
+
continue;
|
|
441
|
+
if (!current ||
|
|
442
|
+
(!TERMINAL_STATES.has(o.state) && TERMINAL_STATES.has(current.state)) ||
|
|
443
|
+
(TERMINAL_STATES.has(o.state) === TERMINAL_STATES.has(current.state) && o.order > current.order))
|
|
444
|
+
current = o;
|
|
445
|
+
}
|
|
446
|
+
return current;
|
|
386
447
|
};
|
|
448
|
+
const hasOtherLiveClaim = (taskId, key) => [...obligations.values(), ...evicted.values()].some((o) => o.taskId === taskId && o.key !== key && !o.key.startsWith("t:") && !TERMINAL_STATES.has(o.state));
|
|
387
449
|
/** Apply a terminal state. Identical duplicates are idempotent; a contradiction is `conflict`. */
|
|
388
450
|
const applyTerminal = (o, next) => {
|
|
389
451
|
if (!TERMINAL_STATES.has(o.state)) {
|
|
@@ -394,7 +456,11 @@ export function createRequiredWorkLedger(init) {
|
|
|
394
456
|
return o.state === "conflict" ? "conflict" : "applied";
|
|
395
457
|
// A stop (CLI teardown or worker TaskStop) after the task already ended is
|
|
396
458
|
// measured noise (reducer-v2 R3), not a contradiction of the recorded end.
|
|
397
|
-
if ((next === "stopped" || next === "withdrawn") && (o.state === "succeeded" || o.state === "failed"))
|
|
459
|
+
if ((next === "stopped" || next === "withdrawn") && (o.state === "succeeded" || o.state === "failed" || o.state === "failed_to_start"))
|
|
460
|
+
return "applied";
|
|
461
|
+
// A repeated failure notice keeps the first classification: a duplicate
|
|
462
|
+
// after the window must not contradict `failed_to_start`.
|
|
463
|
+
if (next === "failed" && o.state === "failed_to_start")
|
|
398
464
|
return "applied";
|
|
399
465
|
// A killed foreground call reports `stopped` and then its own error
|
|
400
466
|
// tool_result (measured C5k, "Exit code 137"): one ending, not two.
|
|
@@ -410,21 +476,40 @@ export function createRequiredWorkLedger(init) {
|
|
|
410
476
|
insufficient.add("event_conflict");
|
|
411
477
|
return "conflict";
|
|
412
478
|
};
|
|
479
|
+
/**
|
|
480
|
+
* BAPI-1455 — apply a task's own foreground result and record that it was
|
|
481
|
+
* delivered. The recorded state stays the real outcome; a later lifecycle
|
|
482
|
+
* `stopped` may still refine a result-derived `failed` (a killed call).
|
|
483
|
+
*/
|
|
484
|
+
const applyForegroundResult = (o, next) => {
|
|
485
|
+
const wasPending = !TERMINAL_STATES.has(o.state);
|
|
486
|
+
const r = applyTerminal(o, next);
|
|
487
|
+
o.resultDelivered = true;
|
|
488
|
+
if (wasPending && o.state === "failed")
|
|
489
|
+
o.failedByResult = true;
|
|
490
|
+
return r;
|
|
491
|
+
};
|
|
413
492
|
/** Fold a placeholder (snapshot-only or orphan terminal keyed by task id) into the real obligation. */
|
|
414
493
|
const mergePlaceholder = (real, taskId) => {
|
|
415
494
|
const ph = obligations.get(`t:${taskId}`);
|
|
416
495
|
if (!ph || ph === real)
|
|
417
496
|
return "applied";
|
|
418
497
|
obligations.delete(ph.key);
|
|
498
|
+
if (workerStops.delete(ph.key))
|
|
499
|
+
workerStops.add(real.key);
|
|
500
|
+
if (ph.resultDelivered)
|
|
501
|
+
real.resultDelivered = true;
|
|
502
|
+
if (real.launchedAt === null && ph.launchedAt !== null && real.state === "pending")
|
|
503
|
+
real.launchedAt = ph.launchedAt;
|
|
419
504
|
if (TERMINAL_STATES.has(ph.state))
|
|
420
505
|
return applyTerminal(real, ph.state);
|
|
421
506
|
return "applied";
|
|
422
507
|
};
|
|
423
|
-
const terminalOnEvicted = (key, next) => {
|
|
508
|
+
const terminalOnEvicted = (key, taskId, next) => {
|
|
424
509
|
const ev = evicted.get(key);
|
|
425
510
|
if (!ev)
|
|
426
511
|
return "ignored";
|
|
427
|
-
if (ev.state === next || next === "stopped" || next === "withdrawn")
|
|
512
|
+
if (ev.taskId === taskId && (ev.state === next || next === "stopped" || next === "withdrawn"))
|
|
428
513
|
return "applied";
|
|
429
514
|
insufficient.add("event_conflict");
|
|
430
515
|
return "conflict";
|
|
@@ -492,6 +577,7 @@ export function createRequiredWorkLedger(init) {
|
|
|
492
577
|
}
|
|
493
578
|
const input = b["input"];
|
|
494
579
|
const inputDigest = obligationInputDigest(input);
|
|
580
|
+
const bg = isRecord(input) && input["run_in_background"] === true;
|
|
495
581
|
// Bounded index; the input itself is dropped here, only digests remain.
|
|
496
582
|
if (!toolUses.has(id)) {
|
|
497
583
|
if (toolUses.size >= MAX_TOOL_USE_INDEX) {
|
|
@@ -499,9 +585,14 @@ export function createRequiredWorkLedger(init) {
|
|
|
499
585
|
if (oldest !== undefined)
|
|
500
586
|
toolUses.delete(oldest);
|
|
501
587
|
}
|
|
502
|
-
toolUses.set(id, {
|
|
588
|
+
toolUses.set(id, {
|
|
589
|
+
name,
|
|
590
|
+
inputDigest,
|
|
591
|
+
denial: resolveDenialOperation(name, input),
|
|
592
|
+
background: bg,
|
|
593
|
+
foregroundResult: null,
|
|
594
|
+
});
|
|
503
595
|
}
|
|
504
|
-
const bg = isRecord(input) && input["run_in_background"] === true;
|
|
505
596
|
if (name === "Agent" || name === "Task") {
|
|
506
597
|
track(id, { toolUseId: id, taskId: null, kind: bg ? "delegate_bg" : "delegate", inputDigest, background: bg, placeholder: false });
|
|
507
598
|
}
|
|
@@ -513,10 +604,16 @@ export function createRequiredWorkLedger(init) {
|
|
|
513
604
|
}
|
|
514
605
|
else if (name === "TaskStop") {
|
|
515
606
|
// R13: the worker's OWN stop request. It turns a later stop into
|
|
516
|
-
// `withdrawn` for the record
|
|
607
|
+
// `withdrawn` for the record. A withdrawal resolves execution of a
|
|
608
|
+
// required task (the work is no longer running); it is not success.
|
|
517
609
|
const tid = isRecord(input) ? boundedIdentifier(input["task_id"]) : null;
|
|
518
|
-
if (tid && workerStops.size < MAX_WORKER_STOPS)
|
|
519
|
-
|
|
610
|
+
if (tid && workerStops.size + deferredWorkerStops.size < MAX_WORKER_STOPS) {
|
|
611
|
+
const current = findCurrentRunByTaskId(tid);
|
|
612
|
+
if (current)
|
|
613
|
+
workerStops.add(current.key);
|
|
614
|
+
else
|
|
615
|
+
deferredWorkerStops.add(tid);
|
|
616
|
+
}
|
|
520
617
|
}
|
|
521
618
|
// Synchronous foreground Bash and every other tool are exploratory: not
|
|
522
619
|
// obligations. Their denials are still recorded from the tool_result.
|
|
@@ -546,9 +643,25 @@ export function createRequiredWorkLedger(init) {
|
|
|
546
643
|
continue;
|
|
547
644
|
}
|
|
548
645
|
const o = obligations.get(id);
|
|
549
|
-
if (!o
|
|
550
|
-
|
|
551
|
-
|
|
646
|
+
if (!o) {
|
|
647
|
+
// BAPI-1455: a foreground Bash result can precede the lifecycle start
|
|
648
|
+
// that turns it into an obligation. Keep only the closed outcome.
|
|
649
|
+
const entry = toolUses.get(id);
|
|
650
|
+
if (entry && entry.name === "Bash" && !entry.background && entry.foregroundResult === null) {
|
|
651
|
+
entry.foregroundResult = isError ? "failed" : "succeeded";
|
|
652
|
+
}
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (o.background) {
|
|
656
|
+
// A launch acknowledgement, not a result: non-terminal. It is the
|
|
657
|
+
// first named launch event for the startup window. No result text is read.
|
|
658
|
+
if (!isError)
|
|
659
|
+
recordLaunch(o);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
// A foreground result is the task's own terminal result, delivered to the
|
|
663
|
+
// worker under its exact tool_use_id (BAPI-1455): whatever its exit code.
|
|
664
|
+
const r = applyForegroundResult(o, isError ? "failed" : "succeeded");
|
|
552
665
|
if (r === "conflict")
|
|
553
666
|
outcome = "conflict";
|
|
554
667
|
}
|
|
@@ -572,6 +685,7 @@ export function createRequiredWorkLedger(init) {
|
|
|
572
685
|
track(`t:${taskId}`, { toolUseId: taskId, taskId, kind, inputDigest: null, background: bg, placeholder: true, required: true });
|
|
573
686
|
return "applied";
|
|
574
687
|
}
|
|
688
|
+
const repeatedStart = obligations.get(toolUseId)?.taskId === taskId;
|
|
575
689
|
const entry = toolUses.get(toolUseId);
|
|
576
690
|
const o = track(toolUseId, {
|
|
577
691
|
toolUseId,
|
|
@@ -587,8 +701,28 @@ export function createRequiredWorkLedger(init) {
|
|
|
587
701
|
insufficient.add("event_conflict");
|
|
588
702
|
return "conflict";
|
|
589
703
|
}
|
|
704
|
+
if (!repeatedStart && hasOtherLiveClaim(taskId, o.key)) {
|
|
705
|
+
o.taskId = taskId;
|
|
706
|
+
o.state = "conflict";
|
|
707
|
+
insufficient.add("event_conflict");
|
|
708
|
+
return "conflict";
|
|
709
|
+
}
|
|
590
710
|
o.taskId = taskId;
|
|
591
|
-
|
|
711
|
+
bindDeferredStop(o);
|
|
712
|
+
// An orphan terminal recorded before this start had to guess a generic
|
|
713
|
+
// background `task`; the trusted start now names the real kind.
|
|
714
|
+
if (o.kind === "task") {
|
|
715
|
+
o.kind = kind;
|
|
716
|
+
o.background = bg;
|
|
717
|
+
}
|
|
718
|
+
recordLaunch(o);
|
|
719
|
+
const merged = mergePlaceholder(o, taskId);
|
|
720
|
+
if (merged === "conflict")
|
|
721
|
+
return merged;
|
|
722
|
+
// BAPI-1455: the command's own foreground result arrived before this start.
|
|
723
|
+
if (!bg && entry?.foregroundResult)
|
|
724
|
+
return applyForegroundResult(o, entry.foregroundResult);
|
|
725
|
+
return merged;
|
|
592
726
|
};
|
|
593
727
|
const handleTaskTerminal = (e, status) => {
|
|
594
728
|
const taskId = boundedIdentifier(e["task_id"]);
|
|
@@ -603,8 +737,6 @@ export function createRequiredWorkLedger(init) {
|
|
|
603
737
|
let next = status === "completed" ? "succeeded" : status === "failed" ? "failed" : status === "stopped" || status === "killed" ? "stopped" : null;
|
|
604
738
|
if (next === null)
|
|
605
739
|
return "applied"; // pending/running: non-terminal progress.
|
|
606
|
-
if (next === "stopped" && workerStops.has(taskId))
|
|
607
|
-
next = "withdrawn";
|
|
608
740
|
const rawToolUse = e["tool_use_id"];
|
|
609
741
|
const toolUseId = rawToolUse === undefined || rawToolUse === null ? null : boundedIdentifier(rawToolUse);
|
|
610
742
|
if (rawToolUse !== undefined && rawToolUse !== null && !toolUseId) {
|
|
@@ -613,42 +745,66 @@ export function createRequiredWorkLedger(init) {
|
|
|
613
745
|
}
|
|
614
746
|
let o;
|
|
615
747
|
if (toolUseId) {
|
|
616
|
-
|
|
617
|
-
if (
|
|
748
|
+
// Exact keyed notices are authoritative for their own run, even after resume.
|
|
749
|
+
if (evicted.has(toolUseId))
|
|
750
|
+
return terminalOnEvicted(toolUseId, taskId, next);
|
|
751
|
+
o = obligations.get(toolUseId);
|
|
752
|
+
if (o?.taskId !== null && o?.taskId !== undefined && o.taskId !== taskId) {
|
|
618
753
|
insufficient.add("event_conflict");
|
|
619
754
|
return "conflict";
|
|
620
755
|
}
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
toolUseId
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
756
|
+
if (!o || o.taskId === null) {
|
|
757
|
+
const byTask = findCurrentRunByTaskId(taskId);
|
|
758
|
+
// An unexplained terminal cannot introduce a resumed start. Association
|
|
759
|
+
// of an existing anonymous tool use still requires no competing live claim.
|
|
760
|
+
if ((o && hasOtherLiveClaim(taskId, toolUseId)) ||
|
|
761
|
+
(!o && byTask && !byTask.key.startsWith("t:") && byTask.key !== toolUseId)) {
|
|
762
|
+
insufficient.add("event_conflict");
|
|
763
|
+
return "conflict";
|
|
764
|
+
}
|
|
765
|
+
o = o ?? track(toolUseId, {
|
|
766
|
+
toolUseId, taskId, kind: "task",
|
|
767
|
+
inputDigest: toolUses.get(toolUseId)?.inputDigest ?? null,
|
|
768
|
+
background: true, placeholder: false,
|
|
769
|
+
});
|
|
770
|
+
if (!o)
|
|
771
|
+
return "applied";
|
|
635
772
|
o.taskId = taskId;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
773
|
+
bindDeferredStop(o);
|
|
774
|
+
const merged = mergePlaceholder(o, taskId);
|
|
775
|
+
if (merged === "conflict")
|
|
776
|
+
return merged;
|
|
777
|
+
}
|
|
639
778
|
}
|
|
640
779
|
else {
|
|
641
|
-
|
|
780
|
+
// A late unkeyed original terminal is inherently indistinguishable from
|
|
781
|
+
// a current-run update; no new correlation protocol is inferred here.
|
|
782
|
+
const current = findCurrentRunByTaskId(taskId);
|
|
783
|
+
if (current && evicted.has(current.key))
|
|
784
|
+
return terminalOnEvicted(current.key, taskId, next);
|
|
785
|
+
o = current ? obligations.get(current.key) : undefined;
|
|
642
786
|
if (!o) {
|
|
643
|
-
for (const [k, ev] of evicted)
|
|
644
|
-
if (ev.taskId === taskId)
|
|
645
|
-
return terminalOnEvicted(k, next);
|
|
646
787
|
o = track(`t:${taskId}`, { toolUseId: taskId, taskId, kind: "task", inputDigest: null, background: true, placeholder: true, required: true });
|
|
647
788
|
if (!o)
|
|
648
789
|
return "applied";
|
|
649
790
|
}
|
|
650
791
|
}
|
|
651
|
-
|
|
792
|
+
if (next === "stopped" && workerStops.has(o.key))
|
|
793
|
+
next = "withdrawn";
|
|
794
|
+
// BAPI-1455: a killed foreground call can deliver its error result before
|
|
795
|
+
// the lifecycle `stopped`. The lifecycle names the real end, so a
|
|
796
|
+
// result-derived `failed` is refined to `stopped` (one ending, not two).
|
|
797
|
+
if (next === "stopped" && o.state === "failed" && o.failedByResult) {
|
|
798
|
+
o.state = "stopped";
|
|
799
|
+
o.failedByResult = false;
|
|
800
|
+
return "applied";
|
|
801
|
+
}
|
|
802
|
+
const applied = applyTerminal(o, next === "failed" ? classifyFailure(o) : next);
|
|
803
|
+
// A lifecycle failure confirms a result-derived one: later teardown noise
|
|
804
|
+
// must not refine it.
|
|
805
|
+
if (o.state === "failed")
|
|
806
|
+
o.failedByResult = false;
|
|
807
|
+
return applied;
|
|
652
808
|
};
|
|
653
809
|
const handleSnapshot = (e) => {
|
|
654
810
|
const tasks = e["tasks"];
|
|
@@ -673,13 +829,7 @@ export function createRequiredWorkLedger(init) {
|
|
|
673
829
|
// A task the snapshot lists but no start/tool use explains yet is an
|
|
674
830
|
// UNKNOWN obligation, required until a lifecycle event resolves it.
|
|
675
831
|
for (const tid of ids) {
|
|
676
|
-
if (
|
|
677
|
-
continue;
|
|
678
|
-
let wasEvicted = false;
|
|
679
|
-
for (const ev of evicted.values())
|
|
680
|
-
if (ev.taskId === tid)
|
|
681
|
-
wasEvicted = true;
|
|
682
|
-
if (wasEvicted)
|
|
832
|
+
if (findCurrentRunByTaskId(tid))
|
|
683
833
|
continue;
|
|
684
834
|
track(`t:${tid}`, { toolUseId: tid, taskId: tid, kind: "task", inputDigest: null, background: true, placeholder: true, required: true });
|
|
685
835
|
}
|
|
@@ -779,19 +929,14 @@ export function createRequiredWorkLedger(init) {
|
|
|
779
929
|
const required = [];
|
|
780
930
|
const optional = [];
|
|
781
931
|
for (const tid of latestSnapshot ?? []) {
|
|
782
|
-
const o =
|
|
932
|
+
const o = findCurrentRunByTaskId(tid);
|
|
783
933
|
if (!o) {
|
|
784
|
-
|
|
785
|
-
for (const ev of evicted.values())
|
|
786
|
-
if (ev.taskId === tid)
|
|
787
|
-
evictedResolved = true;
|
|
788
|
-
if (!evictedResolved)
|
|
789
|
-
required.push(tid);
|
|
934
|
+
required.push(tid);
|
|
790
935
|
continue;
|
|
791
936
|
}
|
|
792
937
|
if (!isLive(o))
|
|
793
938
|
continue;
|
|
794
|
-
(o.required ? required : optional).push(tid);
|
|
939
|
+
(obligations.get(o.key)?.required !== false ? required : optional).push(tid);
|
|
795
940
|
}
|
|
796
941
|
return { required, optional };
|
|
797
942
|
};
|
|
@@ -814,7 +959,7 @@ export function createRequiredWorkLedger(init) {
|
|
|
814
959
|
return all.map((o) => {
|
|
815
960
|
// Only a terminal non-success is replaceable: live work is not superseded,
|
|
816
961
|
// and a `conflict` is contradictory evidence, not a failure to retry.
|
|
817
|
-
const replaceable = o.state === "failed" || o.state === "stopped" || o.state === "withdrawn";
|
|
962
|
+
const replaceable = o.state === "failed" || o.state === "failed_to_start" || o.state === "stopped" || o.state === "withdrawn";
|
|
818
963
|
return { o, state: replaceable && succeededLater(o) ? "replaced" : o.state };
|
|
819
964
|
});
|
|
820
965
|
};
|
|
@@ -869,7 +1014,7 @@ export function createRequiredWorkLedger(init) {
|
|
|
869
1014
|
// A snapshot id whose obligation is already listed (by its tool_use_id or as
|
|
870
1015
|
// a placeholder) is the same work: list it once, not under both handles.
|
|
871
1016
|
for (const tid of snap.required)
|
|
872
|
-
if (!liveRequired.includes(tid) && !
|
|
1017
|
+
if (!liveRequired.includes(tid) && !findCurrentRunByTaskId(tid))
|
|
873
1018
|
liveRequired.push(tid);
|
|
874
1019
|
for (const tid of snap.optional)
|
|
875
1020
|
if (!liveOptional.includes(tid))
|
|
@@ -886,11 +1031,11 @@ export function createRequiredWorkLedger(init) {
|
|
|
886
1031
|
onlyBashBg = false;
|
|
887
1032
|
}
|
|
888
1033
|
for (const tid of snap.required) {
|
|
889
|
-
const o =
|
|
1034
|
+
const o = findCurrentRunByTaskId(tid);
|
|
890
1035
|
if (!o || o.kind !== "bash_bg")
|
|
891
1036
|
onlyBashBg = false;
|
|
892
|
-
else if (!bashBgIds.includes(o.taskId ?? o.
|
|
893
|
-
bashBgIds.push(o.taskId ?? o.
|
|
1037
|
+
else if (!bashBgIds.includes(o.taskId ?? o.key))
|
|
1038
|
+
bashBgIds.push(o.taskId ?? o.key);
|
|
894
1039
|
}
|
|
895
1040
|
return {
|
|
896
1041
|
readyToClose,
|
|
@@ -901,6 +1046,12 @@ export function createRequiredWorkLedger(init) {
|
|
|
901
1046
|
turnOpen,
|
|
902
1047
|
finalResultNoOpenTurn: resultsSeen > 0 && latestIsResult && !turnOpen,
|
|
903
1048
|
liveRequiredBashBg: onlyBashBg ? bashBgIds : [],
|
|
1049
|
+
liveRequiredAfterFinal: resultsSeen > 0 && latestIsResult && !turnOpen
|
|
1050
|
+
? [...new Set([
|
|
1051
|
+
...[...obligations.values()].filter((o) => isLive(o) && o.required).map((o) => o.taskId ?? o.toolUseId),
|
|
1052
|
+
...snap.required,
|
|
1053
|
+
])]
|
|
1054
|
+
: [],
|
|
904
1055
|
};
|
|
905
1056
|
};
|
|
906
1057
|
// One predicate for "a required task is still pending", shared by the envelope
|
|
@@ -935,7 +1086,10 @@ export function createRequiredWorkLedger(init) {
|
|
|
935
1086
|
if (snap.required.length > 0 || unexplainedPlaceholder)
|
|
936
1087
|
reasons.add("stale_task_snapshot");
|
|
937
1088
|
const sufficient = reasons.size === 0;
|
|
938
|
-
|
|
1089
|
+
// BAPI-1455: a command whose own terminal result was delivered is resolved,
|
|
1090
|
+
// whatever its exit code; its recorded state is kept as it is.
|
|
1091
|
+
const delivered = (o, state) => o.resultDelivered && DELIVERY_RESOLVABLE_KINDS.has(o.kind) && DELIVERY_RESOLVABLE_STATES.has(state);
|
|
1092
|
+
const unresolvedRequired = effective.some(({ o, state }) => o.required && !isTaskResolved({ kind: o.kind, state, result_delivered: delivered(o, state) }));
|
|
939
1093
|
const anyConflict = effective.some(({ state }) => state === "conflict");
|
|
940
1094
|
let execution = unresolvedRequired || anyConflict || snap.required.length > 0 || unexplainedPlaceholder ? "incomplete" : "complete";
|
|
941
1095
|
if (!sufficient && execution === "complete")
|
|
@@ -948,6 +1102,7 @@ export function createRequiredWorkLedger(init) {
|
|
|
948
1102
|
state: state === "pending" ? "unknown" : state,
|
|
949
1103
|
required: o.required,
|
|
950
1104
|
input_digest: o.inputDigest,
|
|
1105
|
+
...(delivered(o, state) ? { result_delivered: true } : {}),
|
|
951
1106
|
}));
|
|
952
1107
|
const satisfied = new Set((facts.satisfiedObligations ?? [])
|
|
953
1108
|
.map(normalizeObligation)
|
|
@@ -979,7 +1134,7 @@ export function createRequiredWorkLedger(init) {
|
|
|
979
1134
|
};
|
|
980
1135
|
});
|
|
981
1136
|
const candidate = {
|
|
982
|
-
evidence_version: facts.verificationFacts ? COMPLETION_CONTRACT_VERSION_V2 : COMPLETION_CONTRACT_VERSION,
|
|
1137
|
+
evidence_version: facts.verificationFacts?.deferred_obligation_identities !== undefined ? 3 : facts.verificationFacts ? COMPLETION_CONTRACT_VERSION_V2 : COMPLETION_CONTRACT_VERSION,
|
|
983
1138
|
runtime: { ...facts.runtime },
|
|
984
1139
|
binding: { ...binding, final_head_sha: facts.finalHeadSha },
|
|
985
1140
|
session_id: sessionId,
|
|
@@ -1031,9 +1186,9 @@ export function createRequiredWorkLedger(init) {
|
|
|
1031
1186
|
export const CLOSURE_DEBOUNCE_MS = 1500;
|
|
1032
1187
|
/**
|
|
1033
1188
|
* The absolute post-result window (BAPI-1395). Once a final result is in, no
|
|
1034
|
-
* turn is open, and
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1189
|
+
* turn is open, and any required work remains, stdin closes within this window.
|
|
1190
|
+
* A new turn cancels it; a later final result opens a fresh window. Background
|
|
1191
|
+
* activity never extends it, and the absolute deadline always takes precedence.
|
|
1037
1192
|
*/
|
|
1038
1193
|
export const COMPLETION_WINDOW_MS = 30 * 60 * 1000;
|
|
1039
1194
|
/**
|
|
@@ -1046,13 +1201,13 @@ export const COMPLETION_WINDOW_MS = 30 * 60 * 1000;
|
|
|
1046
1201
|
* caller drains to EOF and the ledger recomputes, so a late turn still
|
|
1047
1202
|
* invalidates the apparent closure (`late_activity_after_close`).
|
|
1048
1203
|
*
|
|
1049
|
-
* The nudge: at most one, only when a turn ended while ONLY server-declared
|
|
1204
|
+
* The optional-watcher nudge: at most one, only when a turn ended while ONLY server-declared
|
|
1050
1205
|
* optional watchers are live. It is never sent while required work is live and
|
|
1051
1206
|
* never asks for a broad stop — cancelling required work is not the executor's
|
|
1052
1207
|
* call.
|
|
1053
1208
|
*
|
|
1054
1209
|
* The completion window (BAPI-1395): when the final result is in and only
|
|
1055
|
-
* required
|
|
1210
|
+
* any required work is live, one fixed end `min(opened + window, deadline)`
|
|
1056
1211
|
* is set. A new worker turn cancels it; background activity does not. The one
|
|
1057
1212
|
* nudge (shared with the optional-watcher nudge, so a job gets at most one)
|
|
1058
1213
|
* asks the worker to stop only its own tasks that are merely waiting. Closing
|
|
@@ -1127,7 +1282,7 @@ export function createClosureSupervisor(opts) {
|
|
|
1127
1282
|
onUndelivered();
|
|
1128
1283
|
});
|
|
1129
1284
|
};
|
|
1130
|
-
const openWindow = (
|
|
1285
|
+
const openWindow = (requiredIds) => {
|
|
1131
1286
|
windowOpened = true;
|
|
1132
1287
|
const now = deps.now();
|
|
1133
1288
|
const windowEnd = Math.min(now + windowMs, opts.deadlineAtMs);
|
|
@@ -1141,7 +1296,7 @@ export function createClosureSupervisor(opts) {
|
|
|
1141
1296
|
}, Math.max(0, windowEnd - now));
|
|
1142
1297
|
}
|
|
1143
1298
|
if (!nudged && opts.buildWindowNudge) {
|
|
1144
|
-
sendNudge(opts.buildWindowNudge([...
|
|
1299
|
+
sendNudge(opts.buildWindowNudge([...requiredIds]), () => {
|
|
1145
1300
|
nudgeAwaitingReply = false;
|
|
1146
1301
|
});
|
|
1147
1302
|
}
|
|
@@ -1165,8 +1320,8 @@ export function createClosureSupervisor(opts) {
|
|
|
1165
1320
|
nudgeAwaitingReply = false;
|
|
1166
1321
|
return;
|
|
1167
1322
|
}
|
|
1168
|
-
if (!windowOpened && s.finalResultNoOpenTurn && s.
|
|
1169
|
-
openWindow(s.
|
|
1323
|
+
if (!windowOpened && s.finalResultNoOpenTurn && s.liveRequiredAfterFinal.length > 0) {
|
|
1324
|
+
openWindow(s.liveRequiredAfterFinal);
|
|
1170
1325
|
return;
|
|
1171
1326
|
}
|
|
1172
1327
|
if (!s.readyToClose)
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import { createWorktreeForTicket } from "../worktree-core.js";
|
|
17
17
|
import { ContinuationLineageMismatch, ContinuationWorktreeMissing, ExecutorNamedError, WorktreeLostBeforePush, } from "./job-errors.js";
|
|
18
18
|
import { assertWorktreeOnBranch, listGitWorktrees, findWorktreeByBranch, remoteBranchExists as remoteBranchExistsDefault, } from "./worktree-inspection.js";
|
|
19
|
+
import { ciFixContextError } from "./prompt-spec.js";
|
|
20
|
+
import { fastForwardRetainedWorktreeToBoundHead } from "./worktree.js";
|
|
19
21
|
/** The canonical WIP checkpoint commit message (executor-run, deterministic). */
|
|
20
22
|
export const RESUME_WIP_COMMIT_MESSAGE = "wip: auto-checkpoint before resume";
|
|
21
23
|
function asString(value) {
|
|
@@ -189,11 +191,34 @@ export async function verifyContinuationHeadLineage(worktreePath, boundHeadSha,
|
|
|
189
191
|
throw new ExecutorNamedError(ContinuationLineageMismatch, `continuation worktree HEAD does not descend from the authorized head ${boundHeadSha.slice(0, 12)}`);
|
|
190
192
|
}
|
|
191
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* BAPI-1514: the bound head a REFRESHED ci_fix continuation's evidence
|
|
196
|
+
* describes, or null for every other job. Non-null only when the job is a
|
|
197
|
+
* `ci_fix` continuation whose `ci_resolved_inputs.evidence_head_sha` is a full
|
|
198
|
+
* 40-hex SHA equal to both `ci_resolved_inputs.head_sha` and the binding's
|
|
199
|
+
* bound head. A legacy payload (no `evidence_head_sha`) or any mismatch is null,
|
|
200
|
+
* which keeps the retained-work protocol byte for byte (pure).
|
|
201
|
+
*/
|
|
202
|
+
export function readRefreshedCiFixEvidenceHead(job, binding) {
|
|
203
|
+
if (job.job_type !== "ci_fix" || binding.boundHeadSha === null)
|
|
204
|
+
return null;
|
|
205
|
+
const payload = job.payload && typeof job.payload === "object" ? job.payload : null;
|
|
206
|
+
const inputs = payload?.ci_resolved_inputs;
|
|
207
|
+
if (!inputs || typeof inputs !== "object" || Array.isArray(inputs))
|
|
208
|
+
return null;
|
|
209
|
+
const raw = inputs;
|
|
210
|
+
const evidence = typeof raw.evidence_head_sha === "string" ? raw.evidence_head_sha.trim().toLowerCase() : "";
|
|
211
|
+
const head = typeof raw.head_sha === "string" ? raw.head_sha.trim().toLowerCase() : "";
|
|
212
|
+
if (!FULL_SHA.test(evidence) || evidence !== head || evidence !== binding.boundHeadSha)
|
|
213
|
+
return null;
|
|
214
|
+
return evidence;
|
|
215
|
+
}
|
|
192
216
|
/**
|
|
193
217
|
* Locate and verify a continuation's RETAINED worktree, then checkpoint it.
|
|
194
218
|
*
|
|
195
219
|
* Order: resolve the existing worktree (never recreate) -> verify its branch ->
|
|
196
|
-
*
|
|
220
|
+
* (BAPI-1514, refreshed ci_fix only) fast-forward a clean worktree to the bound
|
|
221
|
+
* head -> verify its head lineage -> WIP-checkpoint dirty state. A missing worktree is a
|
|
197
222
|
* refusal ({@link ContinuationWorktreeMissing}), not a recreation from origin:
|
|
198
223
|
* recreation cannot restore unpushed commits, and continuing without them would
|
|
199
224
|
* silently discard exactly the work the continuation exists to carry. The
|
|
@@ -211,6 +236,18 @@ export async function prepareRetainedContinuationWorktree(job, binding, seams) {
|
|
|
211
236
|
throw new ExecutorNamedError(ContinuationWorktreeMissing, `continuation for '${expectedBranch}' found no retained worktree; it is never recreated from origin because unpushed work cannot be recovered that way`);
|
|
212
237
|
}
|
|
213
238
|
await assertWorktreeOnBranch(seams.runCommand, worktreePath, expectedBranch);
|
|
239
|
+
// BAPI-1514: a refreshed ci_fix continuation describes its bound head. Move a
|
|
240
|
+
// clean retained worktree forward to it before the lineage proof; every
|
|
241
|
+
// outcome that did not reach the bound head falls through to that proof,
|
|
242
|
+
// which refuses unchanged. Only a moved remote tip refuses here, as the
|
|
243
|
+
// existing stale-head refusal, because the evidence no longer describes it.
|
|
244
|
+
const refreshedHead = readRefreshedCiFixEvidenceHead(job, binding);
|
|
245
|
+
if (refreshedHead !== null) {
|
|
246
|
+
const fastForward = seams.fastForwardRetainedWorktree ?? fastForwardRetainedWorktreeToBoundHead;
|
|
247
|
+
const advanced = await fastForward(worktreePath, expectedBranch, refreshedHead, seams.runCommand);
|
|
248
|
+
if (advanced.kind === "remote_moved")
|
|
249
|
+
throw ciFixContextError(job, "stale_head");
|
|
250
|
+
}
|
|
214
251
|
await verifyContinuationHeadLineage(worktreePath, binding.boundHeadSha, seams);
|
|
215
252
|
const checkpoint = await autoCheckpointDirtyState(worktreePath, seams, CONTINUATION_WIP_COMMIT_MESSAGE);
|
|
216
253
|
return { worktreePath, branch: expectedBranch, checkpointed: checkpoint.committed };
|