@bridge_gpt/mcp-server 0.2.60 → 0.2.65

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.
Files changed (89) hide show
  1. package/README.md +58 -11
  2. package/build/agent-launchers/claude-executor-adapter.js +134 -3
  3. package/build/agent-launchers/executor-adapter.js +54 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/commands.generated.js +8 -7
  7. package/build/conductor/bridge-api-client.js +678 -14
  8. package/build/conductor/bring-up-facts.js +3 -1
  9. package/build/conductor/doctor.js +23 -14
  10. package/build/conductor/epic-child-continuation.js +324 -0
  11. package/build/conductor/epic-implementer-cli.js +142 -17
  12. package/build/conductor/git-hooks.js +529 -118
  13. package/build/conductor/hook-runtime-qualification.js +340 -0
  14. package/build/conductor/merge-execution-contract.js +30 -5
  15. package/build/conductor/merge-execution-eligibility.js +10 -9
  16. package/build/conductor/readiness-sections.js +104 -20
  17. package/build/conductor/readiness.js +129 -25
  18. package/build/conductor/recovery-cli.js +354 -13
  19. package/build/conductor/recovery-operations.js +251 -5
  20. package/build/conductor/repair-contract.js +8 -0
  21. package/build/conductor-bundle-artifacts.js +1 -0
  22. package/build/credential-store.js +194 -0
  23. package/build/docs.generated.js +2 -1
  24. package/build/doctor.js +161 -7
  25. package/build/drive-epic-key.js +12 -0
  26. package/build/drive-epic.js +1318 -111
  27. package/build/epic-coherence-input-cli.js +137 -0
  28. package/build/epic-coherence-input.js +920 -0
  29. package/build/epic-coherence-input.test-fixtures.js +175 -0
  30. package/build/epic-implementer-bin.js +35 -29
  31. package/build/epic-implementer-bundle-cli.js +14 -1
  32. package/build/epic-integration-pr.js +61 -15
  33. package/build/epic-launch-profile.js +464 -0
  34. package/build/epic-run-status.js +5 -0
  35. package/build/epic-shared/bridge-client.js +46 -4
  36. package/build/executor/cli.js +48 -0
  37. package/build/executor/completion-contract.js +423 -0
  38. package/build/executor/completion-supervision.js +286 -0
  39. package/build/executor/credentials.js +19 -1
  40. package/build/executor/deps.js +65 -1
  41. package/build/executor/env.js +6 -0
  42. package/build/executor/heartbeat.js +13 -3
  43. package/build/executor/http-client.js +117 -1
  44. package/build/executor/integration-job.js +759 -0
  45. package/build/executor/job-errors.js +48 -0
  46. package/build/executor/job-runner.js +1386 -35
  47. package/build/executor/job-types.js +10 -2
  48. package/build/executor/observation.js +114 -7
  49. package/build/executor/preflight.js +99 -0
  50. package/build/executor/process-containment.js +756 -0
  51. package/build/executor/process.js +235 -5
  52. package/build/executor/prompt-spec.js +262 -1
  53. package/build/executor/provider-checks.js +69 -0
  54. package/build/executor/rebase-finalization.js +351 -0
  55. package/build/executor/reconcile-overlap-job.js +77 -84
  56. package/build/executor/required-verification.js +313 -0
  57. package/build/executor/required-work-ledger.js +1121 -0
  58. package/build/executor/results.js +142 -0
  59. package/build/executor/resume-pre-spawn.js +87 -4
  60. package/build/executor/runner.js +246 -1
  61. package/build/executor/test-declaration.js +20 -0
  62. package/build/executor/types.js +55 -0
  63. package/build/executor/worker-command.js +49 -13
  64. package/build/executor/worker-finalization.js +88 -11
  65. package/build/executor/worker-guard-hook.js +29 -4
  66. package/build/executor/worktree.js +13 -0
  67. package/build/executor-launch.js +658 -0
  68. package/build/index.js +772 -954
  69. package/build/install-bridge-conductor.js +622 -40
  70. package/build/install-bridge.js +92 -16
  71. package/build/merge-pull-request.js +1 -1
  72. package/build/merge-workflow-config.js +54 -0
  73. package/build/merge-workflow-drift.js +35 -4
  74. package/build/merge-workflow.js +386 -2
  75. package/build/pipelines.generated.js +9 -8
  76. package/build/plan-discovery.js +137 -0
  77. package/build/plan-epic-implementer-eligibility.js +2 -2
  78. package/build/plane/cli.js +37 -3
  79. package/build/plane/member-logs.js +3 -0
  80. package/build/plane/member-roster.js +7 -1
  81. package/build/plane/shutdown.js +59 -1
  82. package/build/plane/supervisor.js +148 -0
  83. package/build/readme.generated.js +1 -1
  84. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  85. package/build/setup-epic.js +130 -48
  86. package/build/version.generated.js +3 -3
  87. package/build/worker-guard-hook-bin.js +2 -2
  88. package/docs/CONDUCTOR.md +116 -2
  89. package/package.json +2 -1
@@ -96,10 +96,11 @@ import { mapConductorDoctorReportToReadinessChecks, } from "./doctor.js";
96
96
  import { mapInstallStatusChecksToReadinessChecks, } from "../install-doctor.js";
97
97
  import { mapPlanePreflightToReadinessChecks, } from "../plane/preflight.js";
98
98
  import { runConductorInstallDoctor, } from "./install-doctor.js";
99
- import { BRING_UP_SECTION_LABELS } from "./readiness-sections.js";
99
+ import { BRING_UP_SECTION_LABELS, presentRequiredChecks, } from "./readiness-sections.js";
100
100
  import { classifyBringUpFacts } from "./bring-up-facts.js";
101
101
  import { classifyMergeExecutionEligibility, MERGE_EXECUTION_INELIGIBLE_REMEDIATION, MERGE_EXECUTION_UNVERIFIABLE_REMEDIATION, } from "./merge-execution-eligibility.js";
102
102
  import { createReadinessCheck, createReadinessCheckSafely, summarizeReadinessChecks, validateReadinessChecks, } from "../readiness-check.js";
103
+ import { ADMISSION_EVIDENCE_SCHEMA_VERSION, } from "./bridge-api-client.js";
103
104
  /** This report's own schema version. See the module docstring. */
104
105
  export const CONDUCTOR_READINESS_GATE_SCHEMA_VERSION = 1;
105
106
  /** Fixed statement carried in every report, so no consumer can mistake it for a gate. */
@@ -140,7 +141,7 @@ export const SERVER_READINESS_DESCRIPTORS = [
140
141
  { id: "executor-readiness", label: "Executor server observation" },
141
142
  { id: "review-workflow", label: "Review workflow protocol" },
142
143
  { id: "conductor-ci-workflow", label: "Conductor CI migration guard" },
143
- // BAPI-1102 — the two ONE-TIME unattended prerequisites plus the aggregate the
144
+ // BAPI-1102 — the two unattended repository prerequisites plus the aggregate the
144
145
  // third unattended refusal reads. Noun phrases, like every row above: these
145
146
  // render as list items under a source heading, not as sentences.
146
147
  { id: "unattended-consent", label: "Repository unattended consent" },
@@ -170,7 +171,6 @@ const SERVER_FAILURE_COPY = {
170
171
  const SERVER_REMEDIATIONS = {
171
172
  supervisorSetup: "run `install conductor` (or `setup-epic`) for this repository to write the project-default supervisor posture.",
172
173
  supervisorConfig: "run `install conductor` for this repository to write the project-default supervisor configuration.",
173
- requiredChecks: "configure a non-empty required-check list in done_gate_config; an empty list makes the done gate pass unconditionally.",
174
174
  mergeApproval: "set the merge-approval requirement explicitly in the supervisor configuration rather than leaving it unset.",
175
175
  reviewPolicyMissing: "set the repository review-policy default so the merge path and the code_review gate read the same signal.",
176
176
  reviewPolicyDivergent: "align the repository review-policy default with the done gate's review_state condition, or confirm the difference is intended.",
@@ -189,8 +189,10 @@ const SERVER_REMEDIATIONS = {
189
189
  olderServer: "upgrade the Bridge API deploy so it reports this fact; its state is unknown here, not healthy.",
190
190
  // BAPI-1102. Each names the ONE action that clears it, and none of them
191
191
  // mentions a webhook URL, a destination, or any configuration value.
192
- unattendedConsent: "set `unattended_epic_implementer_allowed` for this repository (docs/claude/account-settings-operator-runbook.md), or run setup with --attended.",
193
- unattendedNotifyDefault: "supply a --policy-file declaring notify.webhook_url, or run setup with --attended; an unattended run must have a working escalation channel, and notify.local_sink is not one.",
192
+ // BAPI-1273: consent defaults ON; the repository owner switches it on the
193
+ // admin panel's Setup page. No MCP or CLI surface ever writes it.
194
+ unattendedConsent: "turn on 'Allow unattended Epic Implementer runs' on the repository's Setup page in the Bridge admin panel, or run setup with --attended. If it is already on, contact Bridge support.",
195
+ unattendedNotifyDefault: "supply a --policy-file declaring notify.webhook_url, or add an email address to the account so escalation alerts can be emailed, or run setup with --attended; an unattended run must have a working escalation channel, and notify.local_sink is not one.",
194
196
  repositoryWorkflowReadiness: "run `install-bridge conductor` so the review and conductor-CI workflows are confirmed on the default branch, or run setup with --attended.",
195
197
  // BAPI-1135. Both are IMPORTED from the shared classifier rather than written
196
198
  // out here, so the remedy command has exactly one spelling across the refusal,
@@ -252,18 +254,20 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
252
254
  });
253
255
  // Counts and booleans are safe facts and stay in `detail`; they never become
254
256
  // checks of their own, because there is nothing separate to remediate.
255
- outcomes.push(s.required_checks_empty
256
- ? {
257
- id: "supervisor-required-checks",
258
- status: "warn",
259
- detail: "the required-check list is EMPTY — the done gate would pass unconditionally",
260
- remediation: SERVER_REMEDIATIONS.requiredChecks,
261
- }
262
- : {
263
- id: "supervisor-required-checks",
264
- status: "pass",
265
- detail: `${s.required_checks_count} required check(s) configured`,
266
- });
257
+ //
258
+ // BAPI-1254: a nonempty count is not proof. The shared presentation — the same
259
+ // one the install-time section renders — passes ONLY a server `verified`
260
+ // verdict; a confirmed mismatch, could-not-verify, an empty list, and an older
261
+ // server's silence all fail, with distinct fixed copy.
262
+ const requiredChecks = presentRequiredChecks(s);
263
+ outcomes.push({
264
+ id: "supervisor-required-checks",
265
+ status: requiredChecks.satisfied ? "pass" : "fail",
266
+ detail: requiredChecks.detail,
267
+ ...(requiredChecks.remediation === undefined
268
+ ? {}
269
+ : { remediation: requiredChecks.remediation }),
270
+ });
267
271
  // Auto-merge OFF is a posture, not a gap: an operator may deliberately merge
268
272
  // by hand. Reported, never remediated.
269
273
  outcomes.push({
@@ -309,7 +313,7 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
309
313
  outcomes.push(ciWorkflowOutcome(readiness));
310
314
  outcomes.push(...unattendedOutcomes(readiness));
311
315
  const byId = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
312
- return SERVER_READINESS_DESCRIPTORS.map(({ id, label }) => {
316
+ const described = SERVER_READINESS_DESCRIPTORS.map(({ id, label }) => {
313
317
  const outcome = byId.get(id);
314
318
  if (!outcome) {
315
319
  return createReadinessCheck({
@@ -332,6 +336,97 @@ export function mapServerReadinessToChecks(readiness, failure = "unreachable") {
332
336
  : { remediation: outcome.remediation ?? SERVER_REMEDIATIONS.olderServer }),
333
337
  });
334
338
  });
339
+ // BAPI-1260 — appended ONLY when the server reported the block, so every
340
+ // pre-BAPI-1260 report keeps exactly the descriptor set above.
341
+ return [...described, ...admissionEvidenceChecks(readiness)];
342
+ }
343
+ /** Human labels for the known BAPI-1260 evidence predicates. */
344
+ const ADMISSION_EVIDENCE_LABELS = {
345
+ unattended_consent: "Launch evidence: unattended consent",
346
+ notify_webhook: "Launch evidence: notification destination",
347
+ repository_readiness: "Launch evidence: repository workflows",
348
+ required_checks: "Launch evidence: required CI checks",
349
+ index_provenance: "Launch evidence: canonical index provenance",
350
+ durable_actor: "Launch evidence: durable actor handshake",
351
+ migration_head: "Launch evidence: database migration head",
352
+ };
353
+ /** Who acts, in fixed prose. Never a value from the payload. */
354
+ const ADMISSION_OWNER_REMEDIATION = {
355
+ operator: "the operator re-runs guided setup (`install conductor --launch-profile <name>`) and re-observes.",
356
+ repository_admin: "a repository administrator resolves it (unattended consent is the 'Allow unattended Epic Implementer runs' toggle on the repository's Setup page in the Bridge admin panel); setup never grants it.",
357
+ bridge_operations: "escalate to Bridge operations; this is not something an operator command can change.",
358
+ };
359
+ /**
360
+ * Project the server's typed launch evidence (BAPI-1260) into `server.*`
361
+ * checks, read-only.
362
+ *
363
+ * The CLIENT decides nothing here: `confirmed` renders pass, `not_applicable`
364
+ * renders skip, and `denied` and `unable_to_verify` both render fail with
365
+ * DIFFERENT detail — a known denial and an inability to verify call for
366
+ * different actions, and neither authorizes a launch. The server is the only
367
+ * admission authority; this row is what it said, not a verdict of our own.
368
+ */
369
+ export function admissionEvidenceChecks(readiness) {
370
+ const evidence = readiness.admission_evidence;
371
+ if (evidence === undefined)
372
+ return [];
373
+ if (evidence === null || evidence.schema_version !== ADMISSION_EVIDENCE_SCHEMA_VERSION) {
374
+ return [
375
+ createReadinessCheckSafely({
376
+ id: "server.admission-evidence",
377
+ source: "server",
378
+ label: "Launch evidence",
379
+ status: "fail",
380
+ detail: evidence === null
381
+ ? "the server reported no launch evidence, so no mandatory prerequisite is proven"
382
+ : "the server's launch-evidence schema is not one this CLI renders; treat every prerequisite as unverified",
383
+ remediation: "upgrade the CLI or the Bridge API deploy so both speak the same evidence schema.",
384
+ }),
385
+ ];
386
+ }
387
+ return evidence.records.map((record) => admissionEvidenceCheck(record));
388
+ }
389
+ function admissionEvidenceCheck(record) {
390
+ const slug = record.predicate.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
391
+ const label = ADMISSION_EVIDENCE_LABELS[record.predicate] ?? "Launch evidence";
392
+ const reason = record.reason ? ` (${record.reason})` : "";
393
+ const remediation = ADMISSION_OWNER_REMEDIATION[record.next_action_owner ?? "operator"] ?? ADMISSION_OWNER_REMEDIATION.operator;
394
+ switch (record.verdict) {
395
+ case "confirmed":
396
+ return createReadinessCheckSafely({
397
+ id: `server.admission-${slug}`,
398
+ source: "server",
399
+ label,
400
+ status: "pass",
401
+ detail: `confirmed by ${record.source}; fresh for ${record.max_age_seconds}s after observation`,
402
+ });
403
+ case "not_applicable":
404
+ return createReadinessCheckSafely({
405
+ id: `server.admission-${slug}`,
406
+ source: "server",
407
+ label,
408
+ status: "skip",
409
+ detail: `not applicable${reason}`,
410
+ });
411
+ case "denied":
412
+ return createReadinessCheckSafely({
413
+ id: `server.admission-${slug}`,
414
+ source: "server",
415
+ label,
416
+ status: "fail",
417
+ detail: `DENIED by ${record.source}${reason} — dependent activation, dispatch, and privileged operations are held`,
418
+ remediation,
419
+ });
420
+ default:
421
+ return createReadinessCheckSafely({
422
+ id: `server.admission-${slug}`,
423
+ source: "server",
424
+ label,
425
+ status: "fail",
426
+ detail: `could not be verified${reason} — unknown is not safe, so dependent work is held`,
427
+ remediation,
428
+ });
429
+ }
335
430
  }
336
431
  /**
337
432
  * The three BAPI-1153 bring-up safety rows.
@@ -392,7 +487,7 @@ function unattendedOutcomes(readiness) {
392
487
  : {
393
488
  id: "unattended-consent",
394
489
  status: "warn",
395
- detail: "unattended_epic_implementer_allowed is NOT set — an unattended run will be refused before it is created",
490
+ detail: "unattended runs are off for this repository — an unattended run will be refused before it is created",
396
491
  remediation: SERVER_REMEDIATIONS.unattendedConsent,
397
492
  },
398
493
  u.notify_webhook_default_declared
@@ -405,12 +500,21 @@ function unattendedOutcomes(readiness) {
405
500
  ? "a notify webhook default is declared and verified"
406
501
  : "a notify webhook default is declared (verified at approval, not here)",
407
502
  }
408
- : {
409
- id: "unattended-notify-default",
410
- status: "warn",
411
- detail: "no repository notify webhook default is declared — an unattended run needs an escalation channel, and no column supplies a repository default today, so the run must declare notify.webhook_url itself",
412
- remediation: SERVER_REMEDIATIONS.unattendedNotifyDefault,
413
- },
503
+ : u.escalation_email_available === true
504
+ ? {
505
+ // BAPI-1274 — with no webhook declared, escalation alerts go to the
506
+ // account's email address. Presence only: the address is never
507
+ // reported by the server and is never rendered here.
508
+ id: "unattended-notify-default",
509
+ status: "pass",
510
+ detail: "No webhook declared; escalation alerts will be emailed to the account's email address",
511
+ }
512
+ : {
513
+ id: "unattended-notify-default",
514
+ status: "warn",
515
+ detail: "no repository notify webhook default is declared — an unattended run needs an escalation channel, and no column supplies a repository default today, so the run must declare notify.webhook_url itself or the account needs an email address for escalation alerts",
516
+ remediation: SERVER_REMEDIATIONS.unattendedNotifyDefault,
517
+ },
414
518
  u.repository_readiness_confirmed
415
519
  ? {
416
520
  id: "repository-workflow-readiness",
@@ -1,13 +1,14 @@
1
1
  /**
2
- * `conductor` operator recovery and repair verbs — ten CLI-only commands.
2
+ * `conductor` operator recovery and repair verbs — twelve CLI-only commands.
3
3
  *
4
4
  * The four BAPI-872 recovery verbs: `stop-run`, `abandon-run`, `unpark`,
5
5
  * `adopt-current-head-and-unpark`. The six run-addressed repair verbs: BAPI-1154's
6
6
  * `scope-reparse`, `dispatch-remediation`, `merge-child`, and BAPI-1176's
7
- * `scope-catch-up`, `patch-policy`, `resume-run`.
7
+ * `scope-catch-up`, `patch-policy`, `resume-run`. BAPI-1300's parked-child
8
+ * continuation pair: `continue-child` and the read-only `continuation-receipt`.
8
9
  *
9
10
  * CLI-only by design — this is an explicit ticket non-goal, not an oversight:
10
- * none of these ten command names is ever registered as an MCP tool, tool
11
+ * none of these twelve command names is ever registered as an MCP tool, tool
11
12
  * description, or generated tool-schema input (`index-static.test.ts` guards
12
13
  * this). They exist so an operator (or `plane down`, via
13
14
  * `recovery-operations.ts`) can recover a stuck run or ticket WITHOUT
@@ -23,7 +24,7 @@
23
24
  */
24
25
  import { readFile } from "node:fs/promises";
25
26
  import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
26
- import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES } from "./repair-contract.js";
27
+ import { DRIVER_CONTEXT_MAX_BYTES, POLICY_PATCH_MAX_BYTES, RESUME_RUN_UNCONFIRMED_MESSAGE, } from "./repair-contract.js";
27
28
  // ---------------------------------------------------------------------------
28
29
  // Flag parsing
29
30
  // ---------------------------------------------------------------------------
@@ -43,6 +44,12 @@ const REJECTED_VERSION_FLAGS = new Set([
43
44
  "--expected_row_version",
44
45
  "--expected-generation",
45
46
  "--expected_generation",
47
+ // BAPI-1300: the continuation's park-episode binding is read once and stored
48
+ // by the client itself; an operator never supplies it.
49
+ "--park-generation",
50
+ "--park_generation",
51
+ "--source-park-generation",
52
+ "--source_park_generation",
46
53
  ]);
47
54
  /**
48
55
  * Tokenize `--flag value` / `--flag=value` / boolean flags for the recovery
@@ -266,16 +273,52 @@ const ADOPT_CURRENT_HEAD_USAGE = [
266
273
  "off its anchored commit: adopt the CURRENT PR head and unpark in one step.",
267
274
  "Retries a bounded number of times on its own if the ticket changes",
268
275
  "concurrently — never accepts a version counter.",
276
+ "",
277
+ "For a ticket parked as unexplained_review_head_drift this is \"Adopt current",
278
+ "head and resume CI\": the server reads the provider's current head, accepts it,",
279
+ "keeps every repair allowance already spent (no repair attempt is granted), and",
280
+ "resumes the ticket at CI. No worker is started and nothing is marked passed —",
281
+ "CI and review must pass again on the accepted head. A repeat of the same",
282
+ "request reports the recorded result instead of adopting twice.",
269
283
  ].join("\n");
270
- function renderTicketRecoveryResult(result, json) {
284
+ /** BAPI-1310 (E1): the fixed allowance sentence the adoption result carries. */
285
+ const REVIEW_HEAD_ALLOWANCE_LINE = "Allowance effect: repair allowances preserved; acceptance does not grant another repair.";
286
+ /** BAPI-1310 (E1): the warning shown when run control still holds the run. */
287
+ export const REVIEW_HEAD_HELD_WARNING = "Head accepted; run remains held by run control";
288
+ /** Render the E1 adoption result (committed or replayed) to stdout. */
289
+ function renderReviewHeadRecovery(ticketKey, epicRunId, recovery) {
290
+ const outcome = recovery.result === "replayed"
291
+ ? "replayed — this operation already committed; nothing changed now"
292
+ : "committed";
293
+ console.log(`Ticket ${ticketKey} (run ${epicRunId}): Adopt current head and resume CI — ${outcome}.`);
294
+ console.log(` Previous expected head: ${recovery.previous_expected_head_sha}`);
295
+ console.log(` Committed accepted head: ${recovery.accepted_head_sha}`);
296
+ console.log(` ${REVIEW_HEAD_ALLOWANCE_LINE}`);
297
+ console.log(` Resumed at: ${recovery.resumed_gate}`);
298
+ console.log(` Current gate: ${recovery.current_gate ?? "unavailable"}`);
299
+ console.log(" CI and review must pass again on the accepted head; no worker was started and nothing was marked passed.");
300
+ if (recovery.held_by_run_control) {
301
+ console.log(` WARNING: ${REVIEW_HEAD_HELD_WARNING}.`);
302
+ }
303
+ }
304
+ export function renderTicketRecoveryResult(result, json) {
271
305
  if (json) {
272
306
  console.log(JSON.stringify(result));
273
307
  return result.ok ? 0 : 1;
274
308
  }
275
309
  if (result.ok) {
310
+ if (result.reviewHeadRecovery) {
311
+ renderReviewHeadRecovery(result.ticketKey, result.epicRunId, result.reviewHeadRecovery);
312
+ return 0;
313
+ }
276
314
  console.log(`Ticket ${result.ticketKey} (run ${result.epicRunId}): unparked — status: ${result.status}.`);
277
315
  return 0;
278
316
  }
317
+ if (result.kind === "refused") {
318
+ console.error(`Ticket ${result.ticketKey} (run ${result.epicRunId}): refused (${result.errorCode}) — ` +
319
+ `${result.message}. The ticket remains parked; nothing was changed.`);
320
+ return 1;
321
+ }
279
322
  if (result.kind === "ticket-not-found") {
280
323
  console.error(`Ticket ${result.ticketKey}: not found in run ${result.epicRunId}.`);
281
324
  return 1;
@@ -440,7 +483,7 @@ function renderRepairResult(outcome, input, json, extraLines = () => []) {
440
483
  }
441
484
  console.log(JSON.stringify(outcome.kind === "refused"
442
485
  ? { ok: false, kind: "refused", code: outcome.code, message: outcome.message }
443
- : { ok: false, kind: "unavailable", message: outcome.message }));
486
+ : { ok: false, kind: "unavailable", message: input.unavailableMessage ?? outcome.message }));
444
487
  return 1;
445
488
  }
446
489
  if (!outcome.ok) {
@@ -448,6 +491,10 @@ function renderRepairResult(outcome, input, json, extraLines = () => []) {
448
491
  console.error(`Error: ${input.verb} refused on run ${input.epicRunId} (${input.targetLabel}) — ` +
449
492
  `${outcome.code}: ${outcome.message}`);
450
493
  }
494
+ else if (input.unavailableMessage) {
495
+ console.error(`Error: ${input.verb} on run ${input.epicRunId} (${input.targetLabel}) — ` +
496
+ `${input.unavailableMessage}.`);
497
+ }
451
498
  else {
452
499
  console.error(`Error: ${input.verb} could not complete on run ${input.epicRunId} ` +
453
500
  `(${input.targetLabel}) — ${outcome.message}.`);
@@ -788,11 +835,16 @@ export async function runPatchPolicyCommand(argv, deps = {}) {
788
835
  const RESUME_RUN_USAGE = [
789
836
  "Usage: epic-implementer resume-run --epic-run-id <id> [--clear-quarantine] [--json]",
790
837
  "",
791
- "Clear a paused run's control record and return it to active, after the server",
792
- "re-checks the run's activation requirements. A quarantined run resumes only with",
793
- "--clear-quarantine; a stopped run never resumes (start a new run). A run that is",
794
- "already active with no control record is a successful no-op. The server retries",
795
- "a concurrent control change itself — never supply a generation.",
838
+ "Return a held run to active, after the server re-checks the run's activation",
839
+ "requirements. A paused run's control record is cleared. A run blocked by an",
840
+ "eligible integration block has that block lifted (there is no control record",
841
+ "to clear); the server decides eligibility. A quarantined run resumes only with",
842
+ "--clear-quarantine; a stopped run never resumes (start a new run). A run that",
843
+ "is already active and unheld is a successful no-op (already current). The",
844
+ "server retries a concurrent control change itself — never supply a generation.",
845
+ "",
846
+ "If the response is lost or cannot be read, the resume may or may not have",
847
+ "committed: inspect the run read-only before any separately authorized retry.",
796
848
  ].join("\n");
797
849
  export async function runResumeRunCommand(argv) {
798
850
  try {
@@ -812,8 +864,9 @@ export async function runResumeRunCommand(argv) {
812
864
  return renderRepairResult(outcome, {
813
865
  verb: "resume-run",
814
866
  epicRunId: parsed.epicRunId,
815
- targetLabel: parsed.clearQuarantine ? "run control (clear quarantine)" : "run control",
816
- stateLabel: "run control",
867
+ targetLabel: parsed.clearQuarantine ? "run (clear quarantine)" : "run",
868
+ stateLabel: "run state",
869
+ unavailableMessage: RESUME_RUN_UNCONFIRMED_MESSAGE,
817
870
  }, parsed.json);
818
871
  }
819
872
  catch (error) {
@@ -821,3 +874,291 @@ export async function runResumeRunCommand(argv) {
821
874
  return 1;
822
875
  }
823
876
  }
877
+ // ---------------------------------------------------------------------------
878
+ // continue-child / continuation-receipt (BAPI-1300)
879
+ // ---------------------------------------------------------------------------
880
+ const CONTINUE_CHILD_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--reason"]);
881
+ const CONTINUATION_RECEIPT_VALUE_FLAGS = new Set(["--epic-run-id", "--ticket-key", "--request-id"]);
882
+ const MAX_REASON_CHARS = 500;
883
+ const CONTINUE_CHILD_USAGE = [
884
+ "Usage: epic-implementer continue-child --epic-run-id <id> --ticket-key <key> [--reason <text>] [--json]",
885
+ "",
886
+ "Continue a parked child's CURRENT park episode without resetting it: the same",
887
+ "attempt resumes on its retained work with only the allowance it has left (no",
888
+ "fresh budget, no counter or clock reset — use unpark for that). The server",
889
+ "commits one job and one durable receipt together, at most once per episode.",
890
+ "",
891
+ "If the response is lost, the outcome is reported UNCONFIRMED, never refused or",
892
+ "committed. Re-running continue-child reuses the SAME request (it never",
893
+ "approves a new head or episode); continuation-receipt looks the receipt up.",
894
+ "Never accepts a version counter or a park generation.",
895
+ ].join("\n");
896
+ const CONTINUATION_RECEIPT_USAGE = [
897
+ "Usage: epic-implementer continuation-receipt --epic-run-id <id> --ticket-key <key> [--request-id <id>] [--json]",
898
+ "",
899
+ "Read-only. Show the child's current park episode and, for the pending local",
900
+ "continuation request (or --request-id), whether its receipt proves a commit.",
901
+ "A missing receipt is not a refusal. Looking history up authorizes nothing.",
902
+ ].join("\n");
903
+ function parseContinuationArgs(argv, valueFlags) {
904
+ const { values, bools } = tokenizeRecoveryFlags(argv, valueFlags, RECOVERY_BOOL_FLAGS);
905
+ if (bools.has("--help")) {
906
+ return { epicRunId: "", ticketKey: "", json: bools.has("--json"), help: true };
907
+ }
908
+ const epicRunId = requireFlag(values, "--epic-run-id");
909
+ const ticketKey = requireFlag(values, "--ticket-key");
910
+ const reason = values.get("--reason")?.trim();
911
+ if (reason !== undefined && reason.length > MAX_REASON_CHARS) {
912
+ throw new ConductorValidationError(`Flag "--reason" must be at most ${MAX_REASON_CHARS} characters.`);
913
+ }
914
+ const requestId = values.get("--request-id")?.trim();
915
+ if (requestId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$/.test(requestId)) {
916
+ throw new ConductorValidationError('Flag "--request-id" is not a valid request id.');
917
+ }
918
+ return {
919
+ epicRunId,
920
+ ticketKey,
921
+ ...(reason ? { reason } : {}),
922
+ ...(requestId ? { requestId } : {}),
923
+ json: bools.has("--json"),
924
+ help: false,
925
+ };
926
+ }
927
+ /** Exported for tests: parse `continue-child` arguments. */
928
+ export function parseContinueChildArgs(argv) {
929
+ return parseContinuationArgs(argv, CONTINUE_CHILD_VALUE_FLAGS);
930
+ }
931
+ /** Exported for tests: parse `continuation-receipt` arguments. */
932
+ export function parseContinuationReceiptArgs(argv) {
933
+ return parseContinuationArgs(argv, CONTINUATION_RECEIPT_VALUE_FLAGS);
934
+ }
935
+ function receiptSummary(receipt) {
936
+ return {
937
+ receipt_id: receipt.receipt_id,
938
+ request_id: receipt.request_id,
939
+ gate: receipt.gate,
940
+ job_type: receipt.job_type,
941
+ continuation_job_id: receipt.continuation_job_id,
942
+ original_job_id: receipt.original_job_id,
943
+ source_park_generation: receipt.source_park_generation,
944
+ resulting_park_generation: receipt.resulting_park_generation,
945
+ committed_at: receipt.committed_at,
946
+ runtime_outcome: receipt.runtime_outcome,
947
+ };
948
+ }
949
+ function currentSummary(current) {
950
+ if (current === null)
951
+ return null;
952
+ return {
953
+ status: current.status,
954
+ parked: current.parked,
955
+ parked_gate: current.parked_gate,
956
+ park_generation: current.park_generation,
957
+ parked_age_seconds: current.parked_age_seconds,
958
+ needs_human_reason: current.needs_human_reason,
959
+ unresolved_obligations: current.unresolved_obligations,
960
+ continuations_used: current.continuations_used,
961
+ continuation_limit: current.continuation_limit,
962
+ continuation_available: current.continuation_available,
963
+ continuation_unavailable_reason: current.continuation_unavailable_reason,
964
+ };
965
+ }
966
+ function currentLine(current) {
967
+ if (!current.parked) {
968
+ return `Current: status ${current.status} (not parked).`;
969
+ }
970
+ const age = current.parked_age_seconds === null ? "" : `, parked ${current.parked_age_seconds}s`;
971
+ const available = current.continuation_available
972
+ ? "continuation available"
973
+ : `continuation unavailable (${current.continuation_unavailable_reason ?? "unknown"})`;
974
+ return (`Current: parked at gate ${current.parked_gate} (episode ${current.park_generation}${age}); ` +
975
+ `${available}.`);
976
+ }
977
+ /** Exported for tests: render a `continue-child` result. Returns the exit code. */
978
+ export function renderContinueChildResult(result, target, json) {
979
+ const label = `Ticket ${target.ticketKey} (run ${target.epicRunId})`;
980
+ if (result.kind === "decided") {
981
+ const r = result.result;
982
+ const ok = r.outcome === "committed" || r.outcome === "already_continued";
983
+ if (json) {
984
+ console.log(JSON.stringify({
985
+ ok,
986
+ kind: r.outcome,
987
+ reason_code: r.reason_code,
988
+ explanation: r.explanation,
989
+ request_id: result.requestId,
990
+ replayed: r.replayed,
991
+ retryable: r.retryable,
992
+ reference_reused: result.reusedReference,
993
+ receipt: r.receipt === null ? null : receiptSummary(r.receipt),
994
+ current: currentSummary(r.current),
995
+ }));
996
+ return ok ? 0 : 1;
997
+ }
998
+ const receipt = r.receipt;
999
+ switch (r.outcome) {
1000
+ case "committed":
1001
+ console.log(`${label}: continuation COMMITTED${r.replayed ? " (replayed — already committed earlier)" : ""} — ` +
1002
+ `job #${receipt?.continuation_job_id} (${receipt?.job_type}) dispatched for park episode ` +
1003
+ `${receipt?.source_park_generation}; receipt ${receipt?.receipt_id}. This proves the dispatch, ` +
1004
+ "not that the work has finished.");
1005
+ return 0;
1006
+ case "already_continued":
1007
+ console.log(`${label}: this park episode was ALREADY CONTINUED by another request — job ` +
1008
+ `#${receipt?.continuation_job_id}, receipt ${receipt?.receipt_id}. Nothing was dispatched ` +
1009
+ "for this request.");
1010
+ return 0;
1011
+ case "stale_historical_episode":
1012
+ console.error(`${label}: STALE HISTORICAL EPISODE — receipt ${receipt?.receipt_id} (job ` +
1013
+ `#${receipt?.continuation_job_id}) belongs to park episode ${receipt?.source_park_generation}, ` +
1014
+ "which is history. The child has parked again and was NOT resumed by it.");
1015
+ if (r.current)
1016
+ console.error(currentLine(r.current));
1017
+ return 1;
1018
+ case "unsupported":
1019
+ console.error(`${label}: continuation UNSUPPORTED (${r.reason_code}) — ${r.explanation}`);
1020
+ return 1;
1021
+ default:
1022
+ console.error(`${label}: continuation REFUSED (${r.reason_code}) — ${r.explanation}`);
1023
+ if (r.retryable)
1024
+ console.error("This refusal is retryable; nothing was written.");
1025
+ return 1;
1026
+ }
1027
+ }
1028
+ if (result.kind === "refused_http") {
1029
+ if (json) {
1030
+ console.log(JSON.stringify({
1031
+ ok: false,
1032
+ kind: "refused",
1033
+ status: result.status,
1034
+ code: result.errorCode,
1035
+ request_id: result.requestId,
1036
+ }));
1037
+ return 1;
1038
+ }
1039
+ console.error(`${label}: request REJECTED before anything was written (status ${result.status}` +
1040
+ `${result.errorCode ? `, ${result.errorCode}` : ""}).`);
1041
+ return 1;
1042
+ }
1043
+ if (result.kind === "not_continuable") {
1044
+ if (json) {
1045
+ console.log(JSON.stringify({ ok: false, kind: "not_continuable", current: currentSummary(result.current) }));
1046
+ return 1;
1047
+ }
1048
+ console.error(`${label}: not continuable right now. Nothing was sent.`);
1049
+ console.error(currentLine(result.current));
1050
+ return 1;
1051
+ }
1052
+ if (result.kind === "unconfirmed") {
1053
+ if (json) {
1054
+ console.log(JSON.stringify({
1055
+ ok: false,
1056
+ kind: "unconfirmed",
1057
+ transport: result.transport,
1058
+ request_id: result.requestId,
1059
+ reference_reused: result.reusedReference,
1060
+ }));
1061
+ return 1;
1062
+ }
1063
+ console.error(`${label}: continuation UNCONFIRMED after ${result.transport} — the request may or may not have ` +
1064
+ `committed, and no receipt proves it yet (request ${result.requestId}).`);
1065
+ console.error(`Look it up: epic-implementer continuation-receipt --epic-run-id ${target.epicRunId} ` +
1066
+ `--ticket-key ${target.ticketKey}. Re-running continue-child reuses this same request; it ` +
1067
+ "never starts a new one.");
1068
+ return 1;
1069
+ }
1070
+ if (json) {
1071
+ console.log(JSON.stringify({ ok: false, kind: "unavailable", message: result.message }));
1072
+ return 1;
1073
+ }
1074
+ console.error(`${label}: could not continue — ${result.message}. Nothing was sent.`);
1075
+ return 1;
1076
+ }
1077
+ /** Exported for tests: render a `continuation-receipt` lookup. */
1078
+ export function renderContinuationReceiptResult(result, target, json) {
1079
+ const label = `Ticket ${target.ticketKey} (run ${target.epicRunId})`;
1080
+ if (result.kind === "unavailable") {
1081
+ if (json) {
1082
+ console.log(JSON.stringify({ ok: false, kind: "unavailable", message: result.message }));
1083
+ }
1084
+ else {
1085
+ console.error(`${label}: could not read — ${result.message}.`);
1086
+ }
1087
+ return 1;
1088
+ }
1089
+ const status = result.status;
1090
+ if (json) {
1091
+ console.log(JSON.stringify({
1092
+ ok: true,
1093
+ kind: result.kind,
1094
+ request_id: result.requestId,
1095
+ outcome: status.outcome,
1096
+ receipt: status.receipt === null ? null : receiptSummary(status.receipt),
1097
+ current: currentSummary(status.current),
1098
+ }));
1099
+ return 0;
1100
+ }
1101
+ if (result.kind === "found" && status.receipt !== null) {
1102
+ const r = status.receipt;
1103
+ console.log(`${label}: receipt ${r.receipt_id} — continuation COMMITTED at ${r.committed_at}: job ` +
1104
+ `#${r.continuation_job_id} (${r.job_type}, runtime ${r.runtime_outcome ?? "unknown"}) for park ` +
1105
+ `episode ${r.source_park_generation}.`);
1106
+ if (status.outcome === "stale_historical_episode") {
1107
+ console.log("That episode is history: the child has parked again since.");
1108
+ }
1109
+ }
1110
+ else if (result.requestId !== null) {
1111
+ console.log(`${label}: no receipt for request ${result.requestId} — its commit is NOT proven (this is not a refusal).`);
1112
+ }
1113
+ else {
1114
+ console.log(`${label}: no pending local continuation request.`);
1115
+ }
1116
+ console.log(currentLine(status.current));
1117
+ return 0;
1118
+ }
1119
+ export async function runContinueChildCommand(argv) {
1120
+ try {
1121
+ const parsed = parseContinueChildArgs(argv);
1122
+ if (parsed.help) {
1123
+ console.log(CONTINUE_CHILD_USAGE);
1124
+ return 0;
1125
+ }
1126
+ const access = await resolveRecoveryAccess(parsed.json);
1127
+ if (!access)
1128
+ return 1;
1129
+ const { continueChildOnce } = await import("./recovery-operations.js");
1130
+ const result = await continueChildOnce(access, {
1131
+ epicRunId: parsed.epicRunId,
1132
+ ticketKey: parsed.ticketKey,
1133
+ ...(parsed.reason ? { reason: parsed.reason } : {}),
1134
+ });
1135
+ return renderContinueChildResult(result, parsed, parsed.json);
1136
+ }
1137
+ catch (error) {
1138
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
1139
+ return 1;
1140
+ }
1141
+ }
1142
+ export async function runContinuationReceiptCommand(argv) {
1143
+ try {
1144
+ const parsed = parseContinuationReceiptArgs(argv);
1145
+ if (parsed.help) {
1146
+ console.log(CONTINUATION_RECEIPT_USAGE);
1147
+ return 0;
1148
+ }
1149
+ const access = await resolveRecoveryAccess(parsed.json);
1150
+ if (!access)
1151
+ return 1;
1152
+ const { lookupContinuationReceipt } = await import("./recovery-operations.js");
1153
+ const result = await lookupContinuationReceipt(access, {
1154
+ epicRunId: parsed.epicRunId,
1155
+ ticketKey: parsed.ticketKey,
1156
+ ...(parsed.requestId ? { requestId: parsed.requestId } : {}),
1157
+ });
1158
+ return renderContinuationReceiptResult(result, parsed, parsed.json);
1159
+ }
1160
+ catch (error) {
1161
+ console.error(`Error: ${toConductorErrorEnvelope(error).message}`);
1162
+ return 1;
1163
+ }
1164
+ }