@bridge_gpt/mcp-server 0.2.66 → 0.2.67

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 (100) hide show
  1. package/README.md +252 -77
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +43 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/commands.generated.js +11 -11
  8. package/build/conductor/bridge-api-client.js +143 -1
  9. package/build/conductor/bring-up-facts.js +1 -1
  10. package/build/conductor/doctor.js +3 -3
  11. package/build/conductor/epic-implementer-cli.js +22 -6
  12. package/build/conductor/errors.js +4 -3
  13. package/build/conductor/guided-repair.js +6 -2
  14. package/build/conductor/hook-runtime-qualification.js +1 -1
  15. package/build/conductor/merge-execution-contract.js +27 -8
  16. package/build/conductor/merge-execution-eligibility.js +3 -2
  17. package/build/conductor/pr-ci-producer.js +1 -1
  18. package/build/conductor/readiness-cli.js +18 -2
  19. package/build/conductor/readiness-sections.js +41 -6
  20. package/build/conductor/readiness.js +181 -29
  21. package/build/conductor/recovery-cli.js +200 -8
  22. package/build/conductor/recovery-operations.js +69 -3
  23. package/build/conductor/repair-contract.js +54 -3
  24. package/build/conductor/repository-workflow-remedy.js +125 -0
  25. package/build/conductor/tools.js +2 -2
  26. package/build/conductor-bundle-artifacts.js +8 -0
  27. package/build/docs.generated.js +8 -3
  28. package/build/drive-epic-key.js +6 -5
  29. package/build/drive-epic.js +85 -54
  30. package/build/epic-coherence-input.js +2 -1
  31. package/build/epic-implementer-bin.js +22 -20
  32. package/build/epic-implementer-bundle-cli.js +2 -2
  33. package/build/executor/cli.js +3 -2
  34. package/build/executor/completion-contract.js +48 -5
  35. package/build/executor/completion-supervision.js +4 -0
  36. package/build/executor/env.js +19 -0
  37. package/build/executor/heartbeat.js +1 -1
  38. package/build/executor/http-client.js +20 -0
  39. package/build/executor/job-errors.js +25 -0
  40. package/build/executor/job-runner.js +225 -41
  41. package/build/executor/observation.js +6 -1
  42. package/build/executor/preflight.js +51 -14
  43. package/build/executor/required-work-ledger.js +6 -1
  44. package/build/executor/resume-pre-spawn.js +2 -0
  45. package/build/executor/runner.js +8 -2
  46. package/build/executor/service-unit.js +7 -1
  47. package/build/executor/worker-finalization.js +82 -29
  48. package/build/executor/worker-guard-hook.js +137 -33
  49. package/build/executor/worktree.js +82 -1
  50. package/build/index.js +403 -429
  51. package/build/install-bridge-conductor.js +8 -6
  52. package/build/install-bridge.js +3 -3
  53. package/build/phase-context-git-probe.js +68 -0
  54. package/build/phase-result-artifacts.js +173 -5
  55. package/build/pipelines.generated.js +18 -24
  56. package/build/plane/alembic-head.js +12 -3
  57. package/build/plane/base-worktree-guard.js +147 -0
  58. package/build/plane/cli.js +217 -17
  59. package/build/plane/code-snapshot.js +468 -0
  60. package/build/plane/defaults.js +48 -3
  61. package/build/plane/endpoint.js +41 -0
  62. package/build/plane/manifest.js +120 -1
  63. package/build/plane/member-roster.js +94 -19
  64. package/build/plane/preflight.js +30 -2
  65. package/build/plane/process-cwd.js +67 -0
  66. package/build/plane/restart.js +542 -0
  67. package/build/plane/shutdown.js +31 -0
  68. package/build/plane/status.js +143 -8
  69. package/build/plane/supervisor.js +586 -179
  70. package/build/plane/types.js +54 -2
  71. package/build/printed-remedy-commands.js +37 -0
  72. package/build/readiness-check.js +1 -1
  73. package/build/readme.generated.js +1 -1
  74. package/build/review-generation.js +22 -0
  75. package/build/setup-epic.js +44 -19
  76. package/build/sfcc/config.js +9 -0
  77. package/build/sfcc/credentials.js +100 -38
  78. package/build/sfcc/hostname.js +67 -0
  79. package/build/sfcc/permissions.js +17 -13
  80. package/build/sfcc/register.js +1 -1
  81. package/build/sfcc/tool-wrapper.js +39 -43
  82. package/build/sfcc/write-grants.js +17 -11
  83. package/build/sfcc/write-result.js +4 -3
  84. package/build/sfcc/writes-site-preference.js +20 -8
  85. package/build/start-tickets.js +179 -31
  86. package/build/tool-error-envelope.js +61 -0
  87. package/build/version.generated.js +3 -3
  88. package/build/visual-diff.js +22 -1
  89. package/build/worker-guard-hook-bin.js +3 -6
  90. package/build/worker-guard-hook-main.js +47 -0
  91. package/build/worktree-core.js +130 -4
  92. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  93. package/docs/EPIC-IMPLEMENTER.md +662 -0
  94. package/docs/install/mcp-tool-integrations.md +24 -15
  95. package/docs/install/sfcc-integration.md +12 -4
  96. package/package.json +1 -1
  97. package/pipelines/plan-epic.json +0 -5
  98. package/pipelines/review-ticket.json +1 -1
  99. package/docs/CONDUCTOR.md +0 -605
  100. package/pipelines/idea-to-pr.json +0 -49
@@ -18,12 +18,12 @@
18
18
  * callers: `recovery-cli.ts` and `plane/shutdown.ts`.
19
19
  */
20
20
  import { randomUUID } from "crypto";
21
- import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, fetchChildContinuationStatus, postContinueParkedChild, resumeCiTicket, REVIEW_HEAD_REFUSAL_CODES, } from "./bridge-api-client.js";
22
- import { clearGuidedReference, guardForBinding, GUIDED_PAYLOAD_CONFLICT_MESSAGE, GUIDED_REFERENCE_STORE_FAILED_MESSAGE, prepareGuidedRequest, } from "./guided-repair.js";
21
+ import { adoptCurrentHeadAndUnparkTicket, completeMergeChild, ConductorBridgeApiError, dispatchRemediation, fetchEpicRunState, mergeChild, patchPolicy, RERUN_CI_ROUTE_UNAVAILABLE_CODE, RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE, rerunCi, resumeRun, safeDiagnosticMessage, scopeCatchUp, scopeReparse, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, fetchChildContinuationStatus, postContinueParkedChild, resumeCiTicket, REVIEW_HEAD_REFUSAL_CODES, } from "./bridge-api-client.js";
22
+ import { clearGuidedReference, guardForBinding, GUIDED_PAYLOAD_CONFLICT_MESSAGE, GUIDED_REFERENCE_STORE_FAILED_MESSAGE, guidedPayloadDigest, prepareGuidedRequest, } from "./guided-repair.js";
23
23
  import { DEFAULT_GATE_NAME } from "./git-ci-types.js";
24
24
  import { canonicalSha256, clearContinuationReference, clearRecoveryReferenceFile, computeContinuationFingerprint, readRecoveryReferenceFile, writeRecoveryReferenceFile, isFullSha, newContinuationRequestId, readContinuationReference, writeContinuationReference, } from "./epic-child-continuation.js";
25
25
  import { resolveLocalMergeCredential, resolveLocalMergeMethod, runApprovedLocalMerge, } from "./local-merge.js";
26
- import { RESUME_RUN_UNCONFIRMED_MESSAGE } from "./repair-contract.js";
26
+ import { RERUN_CI_UNCONFIRMED_REASON, RESUME_RUN_UNCONFIRMED_MESSAGE } from "./repair-contract.js";
27
27
  /**
28
28
  * Bounded total attempts for a ticket-recovery CAS retry loop. Each attempt
29
29
  * begins with a fresh, authoritative state read — so this bounds full
@@ -501,6 +501,72 @@ export async function guidedRepairOnce(options, deps = {}) {
501
501
  ? { kind: "committed", result, requestId, referenceReused }
502
502
  : { kind: "no_op", result, requestId, referenceReused };
503
503
  }
504
+ /** The bounded unconfirmed text for a `rerun-ci` whose release write was lost. */
505
+ export const RERUN_CI_COMPENSATION_UNCONFIRMED_MESSAGE = "the server could not confirm whether the workflow runs it did not enqueue are still " +
506
+ "recorded for this head. Re-read the run explanation before choosing another action";
507
+ /**
508
+ * Send ONE guided `rerun-ci`, pinned to its displayed binding, through the shared
509
+ * guided-repair reference (stored BEFORE the first POST; a lost response re-sends
510
+ * the SAME request). The server decides eligibility and every per-check status;
511
+ * this layer only classifies the answer.
512
+ */
513
+ export async function rerunCiRepair(access, options, deps = {}) {
514
+ const call = deps.rerunCi ?? rerunCi;
515
+ const outcome = await guidedRepairOnce({
516
+ binding: options.binding,
517
+ payloadDigest: guidedPayloadDigest({ ticket_key: options.ticketKey }),
518
+ send: (guard) => call(access, { epicRunId: options.epicRunId, ticketKey: options.ticketKey, guard }),
519
+ unconfirmedMessage: "the outcome could not be confirmed: the server may or may not have requested the " +
520
+ "re-runs. Re-run the SAME command to re-send the same pinned request, or inspect " +
521
+ "the run read-only",
522
+ }, deps);
523
+ switch (outcome.kind) {
524
+ case "committed":
525
+ case "no_op":
526
+ return {
527
+ kind: "applied",
528
+ result: outcome.result,
529
+ requestId: outcome.requestId,
530
+ referenceReused: outcome.referenceReused,
531
+ };
532
+ case "stale":
533
+ return {
534
+ kind: "stale",
535
+ code: outcome.code,
536
+ message: outcome.message,
537
+ requestId: outcome.requestId,
538
+ };
539
+ case "refused":
540
+ if (outcome.code === RERUN_CI_ROUTE_UNAVAILABLE_CODE) {
541
+ return { kind: "unsupported", message: RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE };
542
+ }
543
+ if (outcome.code === "ROW_VERSION_CONFLICT" &&
544
+ outcome.message.includes(RERUN_CI_UNCONFIRMED_REASON)) {
545
+ return {
546
+ kind: "unconfirmed",
547
+ reason: "compensation_conflict",
548
+ message: RERUN_CI_COMPENSATION_UNCONFIRMED_MESSAGE,
549
+ requestId: outcome.requestId,
550
+ };
551
+ }
552
+ return {
553
+ kind: "refused",
554
+ code: outcome.code,
555
+ status: outcome.status,
556
+ message: outcome.message,
557
+ requestId: outcome.requestId,
558
+ };
559
+ case "unconfirmed":
560
+ return {
561
+ kind: "unconfirmed",
562
+ reason: "transport",
563
+ message: outcome.message,
564
+ requestId: outcome.requestId,
565
+ };
566
+ default:
567
+ return { kind: "unavailable", message: outcome.message };
568
+ }
569
+ }
504
570
  const AUTHORITATIVE_OUTCOMES = new Set([
505
571
  "committed",
506
572
  "already_continued",
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The shared contract for the seven operator repair verbs: BAPI-1154's
2
+ * The shared contract for the eight operator repair verbs: BAPI-1154's
3
3
  * `scope-reparse`, `dispatch-remediation`, and `merge-child`, BAPI-1176's
4
- * `scope-catch-up`, `patch-policy`, and `resume-run`, and BAPI-1326's
5
- * ticket-addressed `resume-ci`.
4
+ * `scope-catch-up`, `patch-policy`, and `resume-run`, BAPI-1326's
5
+ * ticket-addressed `resume-ci`, and BAPI-1388's ticket-addressed `rerun-ci`.
6
6
  *
7
7
  * CLI-only, exactly like the BAPI-872 recovery verbs: none of these names is ever
8
8
  * registered as an MCP tool (`index-static.test.ts` pins that). This module is the
@@ -22,6 +22,7 @@ export const VERB_SCOPE_CATCH_UP = "scope-catch-up";
22
22
  export const VERB_PATCH_POLICY = "patch-policy";
23
23
  export const VERB_RESUME_RUN = "resume-run";
24
24
  export const VERB_RESUME_CI = "resume-ci";
25
+ export const VERB_RERUN_CI = "rerun-ci";
25
26
  /** Every repair verb, in the order an operator meets them. */
26
27
  export const OPERATOR_REPAIR_VERBS = [
27
28
  VERB_SCOPE_REPARSE,
@@ -31,6 +32,7 @@ export const OPERATOR_REPAIR_VERBS = [
31
32
  VERB_PATCH_POLICY,
32
33
  VERB_RESUME_RUN,
33
34
  VERB_RESUME_CI,
35
+ VERB_RERUN_CI,
34
36
  ];
35
37
  /**
36
38
  * The ONE cap on driver context, in UTF-8 BYTES — the same number the server
@@ -156,3 +158,52 @@ export const CI_RESUME_RECEIPT_RETENTION = "latest_only";
156
158
  export function isCiResumeRefusalCode(code) {
157
159
  return (typeof code === "string" && CI_RESUME_REFUSAL_CODES.includes(code));
158
160
  }
161
+ // ---------------------------------------------------------------------------
162
+ // BAPI-1388 — `rerun-ci`
163
+ // ---------------------------------------------------------------------------
164
+ /**
165
+ * The closed `rerun-ci` refusal subset — mirrors `RERUN_CI_REFUSAL_CODES` in
166
+ * `operator_repair_contract.py`. It mints no new code: every member already
167
+ * exists in {@link OPERATOR_REPAIR_REFUSAL_CODES}.
168
+ */
169
+ export const RERUN_CI_REFUSAL_CODES = [
170
+ "TICKET_NOT_IN_RUN",
171
+ "GUIDED_REPAIR_DISABLED",
172
+ "REPAIR_SUBJECT_STALE",
173
+ "RUN_NOT_DISPATCHABLE",
174
+ "TICKET_NOT_REMEDIABLE",
175
+ "REMEDIATION_JOB_IN_FLIGHT",
176
+ "REMEDIATION_LANE_BUSY",
177
+ "REMEDIATION_HELD",
178
+ "REMEDIATION_NO_PULL_REQUEST",
179
+ "ROW_VERSION_CONFLICT",
180
+ "DISPATCH_ENQUEUE_FAILED",
181
+ ];
182
+ /** True when `code` is one of the closed `rerun-ci` refusal codes. */
183
+ export function isRerunCiRefusalCode(code) {
184
+ return (typeof code === "string" && RERUN_CI_REFUSAL_CODES.includes(code));
185
+ }
186
+ /**
187
+ * The bounded reason a `rerun-ci` `ROW_VERSION_CONFLICT` carries when releasing a
188
+ * reservation lost its guarded write. Whether the unenqueued workflow runs are
189
+ * still recorded is UNCONFIRMED; the CLI exits 3.
190
+ */
191
+ export const RERUN_CI_UNCONFIRMED_REASON = "rerun_ci_unconfirmed";
192
+ /**
193
+ * The closed per-check outcome vocabulary. None of them says a check is green: a
194
+ * provider re-run is an action, not a verdict.
195
+ */
196
+ export const RERUN_CI_CHECK_STATUSES = [
197
+ "enqueued",
198
+ "deduplicated",
199
+ "deferred",
200
+ "not_re_runnable",
201
+ ];
202
+ /** Why one failed check is not re-run: a closed token. */
203
+ export const RERUN_CI_NOT_RUNNABLE_REASONS = [
204
+ "not_github_actions",
205
+ "no_workflow_run_id",
206
+ "claude_review_excluded",
207
+ ];
208
+ /** The closed overall outcomes of an applied `rerun-ci`. */
209
+ export const RERUN_CI_OUTCOMES = ["enqueued", "partial", "nothing_to_enqueue"];
@@ -0,0 +1,125 @@
1
+ /** The two fixed workflow files unattended auto-merge reads. Named once here. */
2
+ export const REVIEW_WORKFLOW_PATH = ".github/workflows/claude-review.yml";
3
+ export const CI_WORKFLOW_PATH = ".github/workflows/epic-implementer-ci.yml";
4
+ /** The migration guard's stable job id and its exact checker invocation. */
5
+ export const MIGRATION_GUARD_JOB_ID = "migration-guard";
6
+ export const MIGRATION_GUARD_CHECKER_PATH = "scripts/check_alembic_migration_graph.py";
7
+ export const MIGRATION_GUARD_INVOCATION = `python ${MIGRATION_GUARD_CHECKER_PATH}`;
8
+ const RETRY = "Run the readiness check again, or pass --attended. " +
9
+ "If it keeps failing, contact Bridge operations.";
10
+ const TEMPLATES = {
11
+ review_workflow_file: `${REVIEW_WORKFLOW_PATH} is missing from the default branch. Add it, or pass --attended.`,
12
+ review_run_id_marker: `${REVIEW_WORKFLOW_PATH} does not emit the run-id: marker in its verdict comment. ` +
13
+ "Add it, or pass --attended.",
14
+ review_reviewed_sha_marker: `${REVIEW_WORKFLOW_PATH} does not emit the Reviewed-SHA: marker in its verdict comment. ` +
15
+ "Add it, or pass --attended.",
16
+ review_workflow_unverifiable: `Bridge could not read ${REVIEW_WORKFLOW_PATH} on the default branch. ${RETRY}`,
17
+ ci_workflow_file: `${CI_WORKFLOW_PATH} is missing from the default branch. Add it, or pass --attended.`,
18
+ migration_guard_job: `${CI_WORKFLOW_PATH} has no job with id ${MIGRATION_GUARD_JOB_ID} running ${MIGRATION_GUARD_INVOCATION}. ` +
19
+ "It is required because this repository has alembic.ini on its default branch, " +
20
+ `and an equivalent checker at ${MIGRATION_GUARD_CHECKER_PATH} is required too. ` +
21
+ "Add that job, or pass --attended.",
22
+ migration_chain_unverifiable: "Bridge could not confirm whether this repository has alembic.ini on its default branch, " +
23
+ `so it could not decide if the ${MIGRATION_GUARD_JOB_ID} job is required. ${RETRY}`,
24
+ ci_workflow_unverifiable: `Bridge could not read ${CI_WORKFLOW_PATH} on the default branch. ${RETRY}`,
25
+ // An older server never reported whether the guard is conditional, so its
26
+ // guard has always been required. This states that requirement and NOTHING
27
+ // about alembic.ini: an older server observed no such file.
28
+ migration_guard_job_legacy: `${CI_WORKFLOW_PATH} has no job with id ${MIGRATION_GUARD_JOB_ID} running ${MIGRATION_GUARD_INVOCATION}. ` +
29
+ "Add that job, or pass --attended.",
30
+ };
31
+ /** Shown when the report is missing, incomplete, or not in a shape we can name. */
32
+ const UNVERIFIABLE_FALLBACK = "Bridge could not verify the review and CI workflows on the default branch. " + RETRY;
33
+ function classifyReview(w) {
34
+ if (w === null || w === undefined || typeof w !== "object")
35
+ return "malformed";
36
+ if (w.probe_succeeded !== true)
37
+ return ["review_workflow_unverifiable"];
38
+ if (w.workflow_present === false)
39
+ return ["review_workflow_file"];
40
+ if (w.workflow_present !== true ||
41
+ typeof w.emits_run_id !== "boolean" ||
42
+ typeof w.emits_reviewed_sha !== "boolean") {
43
+ return "malformed";
44
+ }
45
+ const missing = [];
46
+ if (!w.emits_run_id)
47
+ missing.push("review_run_id_marker");
48
+ if (!w.emits_reviewed_sha)
49
+ missing.push("review_reviewed_sha_marker");
50
+ return missing;
51
+ }
52
+ function classifyCi(c) {
53
+ if (c === null || c === undefined || typeof c !== "object")
54
+ return "malformed";
55
+ if (c.probe_succeeded !== true)
56
+ return ["ci_workflow_unverifiable"];
57
+ if (c.workflow_present === false)
58
+ return ["ci_workflow_file"];
59
+ if (c.workflow_present !== true || typeof c.migration_guard_present !== "boolean") {
60
+ return "malformed";
61
+ }
62
+ if (c.migration_guard_present)
63
+ return [];
64
+ const required = c.migration_guard_required;
65
+ const chainProbed = c.migration_chain_probe_succeeded;
66
+ // Older server: neither field reported. The guard was always required there,
67
+ // and the absent fields must never read as "not required".
68
+ if (required === undefined && chainProbed === undefined) {
69
+ return ["migration_guard_job_legacy"];
70
+ }
71
+ if (typeof required !== "boolean" || typeof chainProbed !== "boolean")
72
+ return "malformed";
73
+ if (chainProbed && !required)
74
+ return []; // confirmed guard-free
75
+ if (chainProbed && required)
76
+ return ["migration_guard_job"];
77
+ return ["migration_chain_unverifiable"];
78
+ }
79
+ /**
80
+ * Name the first failing workflow section of a parsed readiness report, its
81
+ * missing facts in stable order, and one fixed-template message.
82
+ *
83
+ * `section: null` with the fallback text means the report is missing or malformed
84
+ * — "could not verify", never "fine". An empty `missing` with `section: null`
85
+ * cannot occur: a report with nothing failing yields the fallback only when it is
86
+ * unreadable, and otherwise the empty result below.
87
+ */
88
+ export function describeRepositoryWorkflowRemedy(readiness) {
89
+ const review = classifyReview(readiness?.review_workflow);
90
+ if (review === "malformed") {
91
+ return { section: null, missing: [], text: UNVERIFIABLE_FALLBACK };
92
+ }
93
+ if (review.length > 0)
94
+ return build("review_workflow", review);
95
+ const ci = classifyCi(readiness?.conductor_ci_workflow);
96
+ if (ci === "malformed") {
97
+ return { section: null, missing: [], text: UNVERIFIABLE_FALLBACK };
98
+ }
99
+ if (ci.length > 0)
100
+ return build("conductor_ci_workflow", ci);
101
+ return { section: null, missing: [], text: "" };
102
+ }
103
+ function build(section, missing) {
104
+ return { section, missing, text: missing.map((token) => TEMPLATES[token]).join(" ") };
105
+ }
106
+ /**
107
+ * The remedy sentence for a repository whose workflows are not confirmed. Never
108
+ * empty: a report with nothing to name (the server said unconfirmed but this
109
+ * client can name no item) gets the unverifiable fallback rather than silence.
110
+ */
111
+ export function repositoryWorkflowRemedy(readiness) {
112
+ const described = describeRepositoryWorkflowRemedy(readiness);
113
+ return described.text === "" ? UNVERIFIABLE_FALLBACK : described.text;
114
+ }
115
+ /**
116
+ * The remedy for the CI workflow section alone — what the readiness CI row shows
117
+ * when the migration guard is absent. Same templates, same words as
118
+ * {@link repositoryWorkflowRemedy}, so the row and the refusals cannot drift.
119
+ */
120
+ export function ciWorkflowRemedy(c) {
121
+ const tokens = classifyCi(c);
122
+ if (tokens === "malformed" || tokens.length === 0)
123
+ return UNVERIFIABLE_FALLBACK;
124
+ return tokens.map((token) => TEMPLATES[token]).join(" ");
125
+ }
@@ -82,9 +82,9 @@ function registerExplainRunTool(registerTool) {
82
82
  // is the field list: a caller cannot discover the answer shape from a
83
83
  // tool that only says "explain a run", and the whole point of the tool is
84
84
  // that the answer is in one place.
85
- description: "Explain why a Conductor epic run, or one ticket in it, is stuck. One snapshot: " +
85
+ description: "Explain why an Epic Implementer run, or one ticket in it, is stuck. One snapshot: " +
86
86
  "current gate, latest observation and age, outstanding jobs with queue position, " +
87
- "lease owner/expiry, blocker reasons, expected-vs-observed head SHA, committed and " +
87
+ "lease owner/expiry, blocker reasons, expected/observed head SHA, committed and " +
88
88
  "pending disposition, usage ceiling. Pass ticket_key to narrow. Read-only; 404 " +
89
89
  "returns status 'unknown'.",
90
90
  inputSchema: {
@@ -27,6 +27,14 @@ import { validateTicketKey } from "./ticket-key-utils.js";
27
27
  const EPIC_SLUG_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
28
28
  /** Placeholder node identity written by `decompose-epic.md` before tickets exist. */
29
29
  const PLACEHOLDER_RE = /TBD-\d+/;
30
+ /**
31
+ * A whole plan-node key that is still an authoring placeholder (`TBD-<digits>`),
32
+ * case-insensitive. Anchored on purpose and stateless (no `g` or `y` flag, so
33
+ * `.test()` carries no `lastIndex`). This is NOT {@link PLACEHOLDER_RE}: that one
34
+ * is an unanchored scan of a whole serialized plan, and
35
+ * {@link findResidualPlaceholder} keeps using it unchanged.
36
+ */
37
+ export const PLACEHOLDER_NODE_KEY_RE = /^TBD-\d+$/i;
30
38
  /**
31
39
  * The conductor's hard spec bound, mirroring `_SPEC_REVIEW_TICKET_SPEC_MAX_CHARS`
32
40
  * (64,000) in `api/library/epic_implementer/reconciler.py`. Since PR #848 the