immune-brain 4.5.0 → 4.6.0

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 (38) hide show
  1. package/README.md +3 -2
  2. package/package.json +1 -1
  3. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  4. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +0 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +47 -2
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +23 -18
  7. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -3
  8. package/plugins/immune-brain/dist/claude/mcp-server.mjs +679 -762
  9. package/plugins/immune-brain/dist/imm-loop.md +11 -4
  10. package/plugins/immune-brain/dist/imm-planner.md +37 -12
  11. package/plugins/immune-brain/runtime/assurance/coordinator.ts +55 -2
  12. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
  13. package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
  14. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +73 -27
  15. package/plugins/immune-brain/runtime/claude/mcp_server.ts +3 -4
  16. package/plugins/immune-brain/runtime/claude/review_host.ts +15 -0
  17. package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
  18. package/plugins/immune-brain/runtime/kernel/application.ts +2 -5
  19. package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
  20. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
  21. package/plugins/immune-brain/runtime/kernel/canary_application.ts +1 -2
  22. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
  23. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
  24. package/plugins/immune-brain/runtime/kernel/enrollment.ts +21 -12
  25. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
  26. package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
  27. package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
  28. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  29. package/plugins/immune-brain/runtime/unattended/batch_git.ts +9 -1
  30. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
  31. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +120 -31
  32. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
  33. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +6 -7
  34. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +72 -119
  35. package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
  36. package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
  37. package/plugins/immune-brain/runtime/workspace_scope.ts +44 -8
  38. package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
@@ -208,27 +208,20 @@ export interface GhExecution {
208
208
  }
209
209
 
210
210
  export interface GhTransport {
211
- run(args: string[], options?: { cwd?: string; stdin?: string; budget?: OperationBudget }): Promise<GhExecution>;
211
+ run(args: string[], options?: { cwd?: string; stdin?: string; signal?: { readonly aborted: boolean } }): Promise<GhExecution>;
212
212
  }
213
213
 
214
214
  /**
215
- * One shared deadline and cancellation boundary for a whole operation. A
216
- * per-call timeout alone cannot bound a multi-step publication: several calls
217
- * that each finish inside their own limit still add up past the operation's
218
- * budget. The entry point creates the budget once and passes it to every
219
- * transport call, so a spent budget refuses the next remote write instead of
220
- * starting one more.
215
+ * External cancellation for a multi-step operation. There is no cumulative
216
+ * deadline: one publication call is a finite step sequence that stops at its
217
+ * first failed call, and each call keeps its own per-call timeout. Only the
218
+ * caller's abort signal is forwarded to every call.
221
219
  */
222
- export interface OperationBudget {
223
- /** Wall-clock time by which the whole operation must have finished. */
224
- deadline_ms: number;
220
+ export interface OperationCancellation {
225
221
  /** Optional external cancellation signal (user cancel / host abort). */
226
222
  signal?: { readonly aborted: boolean };
227
223
  }
228
224
 
229
- /** Default whole-operation publication budget: two minutes. */
230
- export const PUBLICATION_BUDGET_MS = 120_000;
231
-
232
225
  interface RepositoryInfo {
233
226
  id: number;
234
227
  name_with_owner: string;
@@ -492,22 +485,18 @@ export function createGhTransport(binary = "gh"): GhTransport {
492
485
  return new Promise((complete) => {
493
486
  let stdout: Buffer = Buffer.alloc(0);
494
487
  let stderr: Buffer = Buffer.alloc(0);
495
- // A spent or cancelled operation budget never starts another remote call.
496
- const remaining = options.budget === undefined
497
- ? GH_TIMEOUT_MS
498
- : Math.min(GH_TIMEOUT_MS, options.budget.deadline_ms - Date.now());
499
- if (options.budget && (options.budget.signal?.aborted || remaining <= 0)) {
488
+ // A cancelled operation never starts another remote call.
489
+ if (options.signal?.aborted) {
500
490
  complete({
501
491
  exit_code: 1,
502
492
  stdout: "",
503
- stderr: options.budget.signal?.aborted
504
- ? "operation cancelled by the caller"
505
- : "publication deadline exceeded before the next remote call",
493
+ stderr: "operation cancelled by the caller",
506
494
  timed_out: true,
507
495
  output_exceeded: false,
508
496
  });
509
497
  return;
510
498
  }
499
+ const remaining = GH_TIMEOUT_MS;
511
500
  let timedOut = false;
512
501
  let outputExceeded = false;
513
502
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -984,7 +973,7 @@ function createInitiativeBody(
984
973
  KIND_INITIATIVE_MARKER,
985
974
  marker("repo-id", repository.id),
986
975
  marker("initiative-id", operation.initiative_id),
987
- ].join("\n")}\n\n${provenance}## How to use this Issue\n\n- Edit planning prose and Slice ordering directly after creation.\n- Keep each Slice marker attached to exactly one stable Slice entry.\n- The tracker never rewrites or closes this Parent after creation; the tracker never changes or closes it automatically.\n\n## Problem\n\n${publicText(projection.problem ?? "The Initiative addresses the bounded delivery described below.", "projection.problem")}\n\n## Result\n\n${publicText(projection.result ?? operation.goal, "projection.result")}\n\n## Initiative design\n\n${publicText(projection.design ?? "Each Child preserves the shared Initiative decisions and boundaries recorded here.", "projection.design")}\n\n## Decisions\n\n${listText(projection.decisions, "- No additional Initiative decisions recorded.")}\n\n## Testing strategy\n\n${publicText(projection.testing_strategy ?? "Each Child closes from its focused acceptance verification.", "projection.testing_strategy")}\n\n## Out of scope\n\n${listText(projection.out_of_scope, "- Unrelated work outside this Initiative.")}\n\n## Slices\n\n${operation.slices.length + historicalSlices.length === 0 ? "No Slices recorded yet." : [...historicalSlices, ...operation.slices.map((slice) => `- [ ] ${marker("slice-id", slice.id)} **${slice.id}**: ${slice.result ?? slice.goal}${slice.blocked_by?.length ? ` (blocked by: ${slice.blocked_by.join(", ")})` : ""}`)].join("\n")}\n\n${ISSUE_FOOTER}\n`;
976
+ ].join("\n")}\n\n${provenance}## How to use this Issue\n\n- Edit planning prose and Slice ordering directly after creation.\n- Keep each Slice marker attached to exactly one stable Slice entry.\n- The tracker never rewrites this Parent after creation; the tracker closes it as completed once every Slice Child is completed, and never closes it otherwise.\n\n## Problem\n\n${publicText(projection.problem ?? "The Initiative addresses the bounded delivery described below.", "projection.problem")}\n\n## Result\n\n${publicText(projection.result ?? operation.goal, "projection.result")}\n\n## Initiative design\n\n${publicText(projection.design ?? "Each Child preserves the shared Initiative decisions and boundaries recorded here.", "projection.design")}\n\n## Decisions\n\n${listText(projection.decisions, "- No additional Initiative decisions recorded.")}\n\n## Testing strategy\n\n${publicText(projection.testing_strategy ?? "Each Child closes from its focused acceptance verification.", "projection.testing_strategy")}\n\n## Out of scope\n\n${listText(projection.out_of_scope, "- Unrelated work outside this Initiative.")}\n\n## Slices\n\n${operation.slices.length + historicalSlices.length === 0 ? "No Slices recorded yet." : [...historicalSlices, ...operation.slices.map((slice) => `- [ ] ${marker("slice-id", slice.id)} **${slice.id}**: ${slice.result ?? slice.goal}${slice.blocked_by?.length ? ` (blocked by: ${slice.blocked_by.join(", ")})` : ""}`)].join("\n")}\n\n${ISSUE_FOOTER}\n`;
988
977
  }
989
978
 
990
979
  async function createInitiative(
@@ -1076,12 +1065,8 @@ interface AmendmentExecutionContext {
1076
1065
  pendingContent: Map<string, { title: string; body: string }>;
1077
1066
  /** Approved final Parent title/body. */
1078
1067
  parent: { title: string; body: string };
1079
- /** Approved bound Parent issue number. */
1080
- parentIssueNumber: number;
1081
1068
  /** Historical Slice lines derived from the approved Parent baseline (exact bytes). */
1082
1069
  historicalSlices: string[];
1083
- /** Historical dependency database IDs and terminal state_reason snapshotted before any write. */
1084
- historicalRelations: Map<string, { blocked_by: number[]; state_reason: string | null }>;
1085
1070
  }
1086
1071
 
1087
1072
  /**
@@ -1154,9 +1139,7 @@ function approvedAmendmentContent(
1154
1139
  title: initiativeIssueTitle(parent.initiative_id, parent.projection),
1155
1140
  body: parentBody,
1156
1141
  },
1157
- parentIssueNumber: parentIssue.number,
1158
1142
  historicalSlices,
1159
- historicalRelations: new Map(),
1160
1143
  };
1161
1144
  }
1162
1145
 
@@ -1198,66 +1181,12 @@ function baselineHistoricalSlices(parentBaselineBody: string, amendedSliceIds: S
1198
1181
  return lines;
1199
1182
  }
1200
1183
 
1201
- /** Edit an open Initiative Parent's title/body to the approved amendment content. */
1202
- async function amendInitiativeParent(
1203
- root: string,
1204
- gh: GhTransport,
1205
- operation: Extract<TrackerOperation, { op: "create-initiative" }>,
1206
- source: RepositorySnapshot,
1207
- binding: InitiativeAmendmentBinding,
1208
- context: AmendmentExecutionContext,
1209
- ): Promise<GithubTrackerResult> {
1210
- const lookup = (issues: GithubIssue[]) => initiativeLookup(issues, source.repository.id, operation.initiative_id);
1211
- const body = context.parent.body;
1212
- const oversized = bodyLimitFailure(operation.op, body);
1213
- if (oversized) return oversized;
1214
- const title = context.parent.title;
1215
- // Re-read the Parent immediately before writing: earlier topology and
1216
- // historical-dependency reads may have raced a concurrent user edit.
1217
- const reread = await snapshot(root, gh, operation.op);
1218
- if ("contract" in reread) return reread;
1219
- const found = lookup(reread.issues);
1220
- if (found.kind !== "found")
1221
- return result(operation.op, "permanent_failure", "an amendment requires the Initiative Parent to already exist");
1222
- if (found.issue.state !== "open")
1223
- return result(operation.op, "ambiguous_remote_state", "an amendment requires the Initiative Parent to remain open", found.issue);
1224
- if (binding.issue_number !== found.issue.number)
1225
- return result(operation.op, "ambiguous_remote_state", `amendment Parent is bound to Issue #${binding.issue_number} but observed Issue #${found.issue.number}`, found.issue);
1226
- if (found.issue.body === body && found.issue.title === title && !labelMutationArgs(found.issue.labels, []).length)
1227
- return result(operation.op, "already_current", "Initiative Issue already carries the requested amended content", found.issue);
1228
- // Content that already matches the approved final bytes still converges labels;
1229
- // anything else must still be the approved amendment baseline.
1230
- if ((found.issue.body !== body || found.issue.title !== title)
1231
- && (found.issue.body !== binding.body || found.issue.title !== binding.title))
1232
- return result(operation.op, "ambiguous_remote_state", "Initiative Parent changed since the approved amendment baseline", found.issue);
1233
- // The Parent carries no Task state labels: any managed label observed on it is
1234
- // drift and is removed by the same edit that writes the amended content.
1235
- const labelArgs = labelMutationArgs(found.issue.labels, []);
1236
- const edited = await gh.run([
1237
- "issue", "edit", String(found.issue.number), "--repo", source.repository.name_with_owner,
1238
- "--title", title,
1239
- "--body-file", "-",
1240
- ...labelArgs,
1241
- ], { cwd: root, stdin: body });
1242
- if (edited.exit_code !== 0 || edited.output_exceeded) return ghFailure(operation.op, edited, "Initiative amendment edit failed");
1243
- const refreshed = await snapshot(root, gh, operation.op);
1244
- if ("contract" in refreshed) return refreshed;
1245
- const confirmed = lookup(refreshed.issues);
1246
- if (confirmed.kind !== "found") return result(operation.op, "ambiguous_remote_state", "Initiative Parent became ambiguous after amendment", found.issue);
1247
- if (MANAGED_ISSUE_LABELS.some((label) => confirmed.issue.labels.includes(label)))
1248
- return result(operation.op, "retryable_failure", "Initiative amendment did not converge to an unlabeled Parent", confirmed.issue);
1249
- return confirmed.issue.body === body && confirmed.issue.title === title
1250
- ? result(operation.op, "updated", "Initiative Issue updated with approved amendment content", confirmed.issue)
1251
- : result(operation.op, "retryable_failure", "Initiative amendment did not converge to the requested title and body", confirmed.issue);
1252
- }
1253
1184
 
1254
1185
  async function upsertTask(
1255
1186
  root: string,
1256
1187
  gh: GhTransport,
1257
1188
  operation: Extract<TrackerOperation, { op: "upsert-task" }>,
1258
1189
  source: RepositorySnapshot,
1259
- pendingBinding: InitiativeAmendmentBinding | undefined | null = null,
1260
- amendmentContext: AmendmentExecutionContext | undefined = undefined,
1261
1190
  ): Promise<GithubTrackerResult> {
1262
1191
  const labelFailure = await labelAvailabilityFailure(root, gh, source.repository, desiredTaskLabels(operation));
1263
1192
  if (labelFailure) return labelFailure;
@@ -1275,8 +1204,6 @@ async function upsertTask(
1275
1204
  const lookup = (issues: GithubIssue[]) => taskLookup(issues, source.repository.id, operation.task_id);
1276
1205
  const found = lookup(source.issues);
1277
1206
  if (found.kind === "ambiguous") return result(operation.op, "ambiguous_remote_state", found.message);
1278
- if (found.kind === "missing" && pendingBinding !== null && pendingBinding !== undefined)
1279
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is bound to Issue #${pendingBinding.issue_number} but that Issue no longer holds its Task marker; amend the binding or restore the marker instead of recreating`, parent.issue);
1280
1207
  const blockerIds = operation.projection?.blocked_by ?? [];
1281
1208
  const blockers: GithubIssue[] = [];
1282
1209
  for (const blockerId of blockerIds) {
@@ -1297,40 +1224,6 @@ async function upsertTask(
1297
1224
  let createdChild = false;
1298
1225
  let labelsConverged = false;
1299
1226
  if (found.kind === "missing") {
1300
- // Pre-create re-read (amendment path): a concurrent writer may have already
1301
- // created this unbound Task between the initial snapshot and our create — the
1302
- // same resumable-creation contract applies before we issue any write, so we
1303
- // fail closed on divergent content instead of issuing a duplicate create.
1304
- if (amendmentContext !== undefined) {
1305
- const reRead = await snapshot(root, gh, operation.op);
1306
- if ("contract" in reRead) return reRead;
1307
- // The re-read snapshot must also still hold the amendment's Parent exactly
1308
- // as approved: a Parent closed or edited after the initial snapshot fails
1309
- // closed here, before any Child create (avoiding an avoidable remote write
1310
- // that the post-create attachment guard would otherwise reject).
1311
- const reReadParent = initiativeLookup(reRead.issues, reRead.repository.id, operation.initiative_id);
1312
- if (reReadParent.kind !== "found")
1313
- return result(operation.op, "ambiguous_remote_state", reReadParent.kind === "ambiguous" ? reReadParent.message : "amendment Parent is not observable before creating a new Child", parent.issue);
1314
- if (reReadParent.issue.number !== amendmentContext.parentIssueNumber)
1315
- return result(operation.op, "ambiguous_remote_state", `amendment Parent is bound to Issue #${amendmentContext.parentIssueNumber} but observed Issue #${reReadParent.issue.number} before creating a new Child`, reReadParent.issue);
1316
- if (reReadParent.issue.state !== "open")
1317
- return result(operation.op, "ambiguous_remote_state", "amendment Parent is no longer open before creating a new Child", reReadParent.issue);
1318
- if (amendmentContext.parent.title !== reReadParent.issue.title || amendmentContext.parent.body !== reReadParent.issue.body)
1319
- return result(operation.op, "ambiguous_remote_state", "amendment Parent content changed before creating a new Child", reReadParent.issue);
1320
- if (sliceCount(reReadParent.issue.body, operation.slice_id) !== 1)
1321
- return result(operation.op, "ambiguous_remote_state", `Parent Issue #${reReadParent.issue.number} lost its exact Slice marker ${operation.slice_id} before creating a new Child`, reReadParent.issue);
1322
- const raced = lookup(reRead.issues);
1323
- if (raced.kind === "ambiguous") return result(operation.op, "ambiguous_remote_state", raced.message);
1324
- if (raced.kind === "found") {
1325
- const approved = amendmentContext.pendingContent.get(operation.task_id);
1326
- const resumable = approved !== undefined
1327
- && raced.issue.state === "open"
1328
- && carriesApprovedContent(raced.issue.title, raced.issue.body, approved);
1329
- if (!resumable)
1330
- return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} is unbound but Issue #${raced.issue.number} already exists with divergent content; bind it to amend`, raced.issue);
1331
- return updatePendingChild(root, gh, reRead, operation, raced.issue, undefined, blockers, approved, amendmentContext);
1332
- }
1333
- }
1334
1227
  const mutation = await gh.run([
1335
1228
  "issue", "create", "--repo", source.repository.name_with_owner,
1336
1229
  "--title", title,
@@ -1348,59 +1241,12 @@ async function upsertTask(
1348
1241
  }
1349
1242
  if (created.issue.body !== body || created.issue.title !== title)
1350
1243
  return result(operation.op, "retryable_failure", "Task Issue did not converge to the requested title and body", created.issue);
1351
- if (amendmentContext !== undefined) {
1352
- // Amendment unbound new Child: creation converged, but the Child must
1353
- // still be open — a concurrent close between create and read fails closed
1354
- // instead of attaching and wiring dependencies onto closed work.
1355
- if (created.issue.state !== "open")
1356
- return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} (Issue #${created.issue.number}) is not open after creation`, created.issue);
1357
- // Route through the amendment convergence path so attachment (R4
1358
- // re-attach) and every dependency write carry the same pre-write
1359
- // revalidation as bound pending Children.
1360
- return updatePendingChild(root, gh, source, operation, created.issue, undefined, blockers, amendmentContext.pendingContent.get(operation.task_id), amendmentContext);
1361
- }
1362
1244
  child = created.issue;
1363
1245
  createdChild = true;
1364
1246
  } else {
1365
1247
  const owned = ownedTaskLookup(source.issues, source.repository.id, operation.task_id, operation.initiative_id, operation.slice_id);
1366
1248
  if (owned.kind !== "found") return result(operation.op, "ambiguous_remote_state", owned.kind === "ambiguous" ? owned.message : "Task Issue ownership changed during publication", found.issue);
1367
1249
  child = owned.issue;
1368
- if (pendingBinding !== null) {
1369
- if (child.state !== "open")
1370
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is closed and cannot be amended as pending work`, found.issue);
1371
- // The bound pending Child must still be the Issue its binding pins (review-5):
1372
- // a replacement Issue that took over the markers fails closed instead of
1373
- // being edited or recreated.
1374
- if (pendingBinding !== undefined && pendingBinding.issue_number !== child.number)
1375
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is bound to Issue #${pendingBinding.issue_number} but observed Issue #${child.number}`, found.issue);
1376
- const approvedFinal = amendmentContext?.pendingContent.get(operation.task_id);
1377
- // A Child left open with a validated terminal suffix (failed terminal close)
1378
- // counts as approved-final when its suffix-free bytes match the approved bytes.
1379
- const isFinal = approvedFinal !== undefined && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
1380
- if (!isFinal) {
1381
- // Suffix-aware baseline equality: a Child whose remote bytes carry a validated
1382
- // terminal suffix over the binding baseline (failed terminal close) still
1383
- // matches its binding, because the suffix is terminal evidence, not drift.
1384
- const baselineMatches = pendingBinding !== undefined
1385
- && pendingBinding.title === found.issue.title
1386
- && carriesApprovedContent(found.issue.title, found.issue.body, pendingBinding);
1387
- if (!baselineMatches)
1388
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} changed since the approved amendment baseline`, found.issue);
1389
- }
1390
- return updatePendingChild(root, gh, source, operation, child, pendingBinding?.issue_number, blockers, approvedFinal, amendmentContext);
1391
- }
1392
- if (amendmentContext !== undefined) {
1393
- // Amendment unbound Child observed before any write: it is only valid
1394
- // as the exact approved-final creation of this same batch (resumable
1395
- // creation); anything else fails closed.
1396
- const approvedFinal = amendmentContext.pendingContent.get(operation.task_id);
1397
- const resumable = approvedFinal !== undefined
1398
- && child.state === "open"
1399
- && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
1400
- if (!resumable)
1401
- return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} is unbound but Issue #${child.number} already exists with divergent content; bind it to amend`, found.issue);
1402
- return updatePendingChild(root, gh, source, operation, child, undefined, blockers, approvedFinal, amendmentContext);
1403
- }
1404
1250
  if (!sameTrackedBody(found.issue.body, body) || found.issue.title !== title)
1405
1251
  return result(operation.op, "permanent_failure", "Task Issue already exists with a different title or Agent Brief; edit the GitHub source or retry the original projection before changing native relations", found.issue);
1406
1252
  // The published surface records the approved Intent identity. A Child that
@@ -1513,7 +1359,7 @@ async function closeTerminalIssue(
1513
1359
  : result(operation.op, "retryable_failure", "terminal Issue closure did not converge", found.issue);
1514
1360
  }
1515
1361
 
1516
- async function markTerminal(
1362
+ async function markChildTerminal(
1517
1363
  root: string,
1518
1364
  gh: GhTransport,
1519
1365
  operation: Extract<TrackerOperation, { op: "mark-terminal" }>,
@@ -1559,6 +1405,86 @@ async function markTerminal(
1559
1405
  return closeTerminalIssue(root, gh, operation, refreshed, reread.issue, lookup);
1560
1406
  }
1561
1407
 
1408
+ /**
1409
+ * The Parent decision reads only Issue-scoped endpoints (the Parent and its
1410
+ * Sub-issue list), never a repository listing. A failed read or close is a
1411
+ * tracker observation whose single retry action is the same `mark-terminal`;
1412
+ * the Child's terminal closure is already confirmed and stays unchanged.
1413
+ */
1414
+ async function closeParentWhenComplete(
1415
+ root: string,
1416
+ gh: GhTransport,
1417
+ operation: Extract<TrackerOperation, { op: "mark-terminal" }>,
1418
+ source: RepositorySnapshot,
1419
+ childResult: GithubTrackerResult,
1420
+ ): Promise<GithubTrackerResult> {
1421
+ const child = taskLookup(source.issues, source.repository.id, operation.task_id);
1422
+ if (child.kind !== "found") return childResult;
1423
+ const initiativeId = ownershipMarkerValue(child.issue.body, "initiative-id");
1424
+ if (!initiativeId) return childResult;
1425
+ const parentLookup = initiativeLookup(source.issues, source.repository.id, initiativeId);
1426
+ if (parentLookup.kind !== "found") return childResult;
1427
+ const parentNumber = parentLookup.issue.number;
1428
+ const retry = (message: string, status: TrackerStatus = "retryable_failure") =>
1429
+ result(operation.op, status, `Parent #${parentNumber}: ${message}; the Task closure is unchanged, retry the same mark-terminal`, child.issue);
1430
+ const endpoint = `repos/${source.repository.name_with_owner}/issues/${parentNumber}`;
1431
+ const readParent = async (): Promise<GithubIssue | GithubTrackerResult> => {
1432
+ const read = await gh.run(["api", endpoint], { cwd: root });
1433
+ if (read.exit_code !== 0 || read.output_exceeded) {
1434
+ const failed = ghFailure(operation.op, read, "cannot read the Initiative Parent");
1435
+ return retry(failed.message, failed.status);
1436
+ }
1437
+ try {
1438
+ const [issue] = parseIssues(`[${read.stdout}]`);
1439
+ return issue ?? retry("gh returned a malformed Issue", "permanent_failure");
1440
+ } catch (error) {
1441
+ return retry(error instanceof Error ? error.message : String(error), "permanent_failure");
1442
+ }
1443
+ };
1444
+ const parent = await readParent();
1445
+ if ("contract" in parent) return parent;
1446
+ if (parent.state === "closed") return childResult;
1447
+ const listed = await gh.run(["api", "--paginate", "--slurp", `${endpoint}/sub_issues?per_page=100`], { cwd: root });
1448
+ if (listed.exit_code !== 0 || listed.output_exceeded) {
1449
+ const failed = ghFailure(operation.op, listed, "cannot read the Initiative Sub-issues");
1450
+ return retry(failed.message, failed.status);
1451
+ }
1452
+ let children: GithubIssue[];
1453
+ try {
1454
+ children = parseIssues(listed.stdout);
1455
+ } catch (error) {
1456
+ return retry(error instanceof Error ? error.message : String(error), "permanent_failure");
1457
+ }
1458
+ const sliceChildren = children.filter((candidate) =>
1459
+ candidate.body.includes(marker("initiative-id", initiativeId))
1460
+ && /<!-- immune-brain:slice-id=[^>]+ -->/.test(candidate.body));
1461
+ if (sliceChildren.length === 0
1462
+ || !sliceChildren.every((candidate) => candidate.state === "closed" && candidate.state_reason === "completed"))
1463
+ return childResult;
1464
+ const close = await gh.run(["issue", "close", String(parentNumber), "--repo", source.repository.name_with_owner, "--reason", "completed"], { cwd: root });
1465
+ const confirmed = await readParent();
1466
+ if ("contract" in confirmed) return confirmed;
1467
+ if (confirmed.state === "closed" && confirmed.state_reason === "completed")
1468
+ return result(operation.op, "updated", `${childResult.message}; Initiative Parent #${parentNumber} closed as completed`, child.issue);
1469
+ if (close.exit_code !== 0 || close.output_exceeded) {
1470
+ const failed = ghFailure(operation.op, close, "Initiative Parent closure failed");
1471
+ return retry(failed.message, failed.status);
1472
+ }
1473
+ return retry("closure did not converge");
1474
+ }
1475
+
1476
+ async function markTerminal(
1477
+ root: string,
1478
+ gh: GhTransport,
1479
+ operation: Extract<TrackerOperation, { op: "mark-terminal" }>,
1480
+ source: RepositorySnapshot,
1481
+ ): Promise<GithubTrackerResult> {
1482
+ const terminal = await markChildTerminal(root, gh, operation, source);
1483
+ if (operation.phase !== "done" || (terminal.status !== "updated" && terminal.status !== "already_current") || !terminal.association_found)
1484
+ return terminal;
1485
+ return closeParentWhenComplete(root, gh, operation, source, terminal);
1486
+ }
1487
+
1562
1488
  function normalizedList(value: unknown, name: string, max = 2_000): string[] | undefined {
1563
1489
  if (value === undefined) return undefined;
1564
1490
  if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
@@ -1672,19 +1598,6 @@ function validateOperation(operation: TrackerOperation): TrackerOperation {
1672
1598
  };
1673
1599
  }
1674
1600
 
1675
- /** Run one amendment pending Task: create unbound new Children, amend bound drift. */
1676
- async function runAmendmentTaskOperation(
1677
- root: string,
1678
- gh: GhTransport,
1679
- operation: Extract<TrackerOperation, { op: "upsert-task" }>,
1680
- pendingBinding: InitiativeAmendmentBinding | undefined | null,
1681
- context: AmendmentExecutionContext | undefined,
1682
- ): Promise<GithubTrackerResult> {
1683
- const absoluteRoot = resolve(root);
1684
- const source = await snapshot(absoluteRoot, gh, operation.op);
1685
- if ("contract" in source) return source;
1686
- return upsertTask(absoluteRoot, gh, operation, source, pendingBinding, context);
1687
- }
1688
1601
 
1689
1602
  export async function runGithubTrackerOperation(
1690
1603
  root: string,
@@ -1925,35 +1838,6 @@ function publicationIntentDrift(
1925
1838
  return null;
1926
1839
  }
1927
1840
 
1928
- function publicationIssueDrift(expected: {
1929
- issue_number?: number;
1930
- issue_url?: string;
1931
- node_id?: string;
1932
- }, actual: GithubIssue, title: string, body: string, label: string, options?: { allowTerminalSuffix?: boolean; taskId?: string; expectedIntentHash?: string }): string | null {
1933
- if (expected.issue_number !== actual.number || expected.issue_url !== actual.url || expected.node_id !== String(actual.id))
1934
- return `${label} identity changed during Initiative publication`;
1935
- if (actual.state !== "open") return `${label} is no longer open`;
1936
- // The published Intent identity is verified on the final readback as well:
1937
- // a Child whose hash changed only after the earlier per-Child check would
1938
- // otherwise be reported as a confirmed success.
1939
- if (options?.taskId)
1940
- return publishedIntentMismatch(actual.body, options.taskId, options.expectedIntentHash) ?? driftOnContent(actual, title, body, label, options);
1941
- // The published-identity marker is additive and compared separately, so a Child
1942
- // published before it existed is still the same tracked content here.
1943
- return driftOnContent(actual, title, body, label, options);
1944
- }
1945
-
1946
- function driftOnContent(actual: GithubIssue, title: string, body: string, label: string, options?: { allowTerminalSuffix?: boolean }): string | null {
1947
- if (actual.title !== title || !sameTrackedBody(actual.body, body)) {
1948
- // Only a pending amendment Child left open with a validated terminal suffix
1949
- // (failed terminal close) matches its suffix-free expected content. The
1950
- // Parent and the strict non-amendment path have no terminal-suffix
1951
- // lifecycle: byte-exact comparison, any extra suffix is real drift.
1952
- if (options?.allowTerminalSuffix && carriesApprovedContent(actual.title, actual.body, { title, body })) return null;
1953
- return `${label} content changed during Initiative publication`;
1954
- }
1955
- return null;
1956
- }
1957
1841
 
1958
1842
  /** One bound Issue's expected remote identity and content. */
1959
1843
  function validateAmendmentBinding(binding: unknown, label: string): InitiativeAmendmentBinding {
@@ -2224,268 +2108,639 @@ function carriesApprovedContent(title: string, body: string, approved: { title:
2224
2108
  return typeof stripped === "string" && stripped.trimEnd() === approved.body.trimEnd();
2225
2109
  }
2226
2110
 
2227
- async function updatePendingChild(
2111
+ /**
2112
+ * The Issue number at the end of a `gh issue create` response URL, or null.
2113
+ * Creation takes its number from the create response: no read-back follows a
2114
+ * write, so a response that cannot be parsed leaves the outcome uncertain
2115
+ * rather than asking the repository what landed.
2116
+ */
2117
+ function parseCreatedIssueNumber(stdout: string): number | null {
2118
+ const match = stdout.trim().match(/\/issues\/(\d+)\/?$/);
2119
+ if (!match) return null;
2120
+ const number = Number(match[1]);
2121
+ return Number.isSafeInteger(number) && number > 0 ? number : null;
2122
+ }
2123
+
2124
+ /**
2125
+ * Resolve one Issue's database id. Assumption A1 fallback: `gh issue create`
2126
+ * returns only the Issue URL, so the id the relation endpoints require is read
2127
+ * once per created Issue immediately after its create. The id is never
2128
+ * re-resolved for an Issue the start listing already carries.
2129
+ */
2130
+ async function resolveIssueId(
2228
2131
  root: string,
2229
2132
  gh: GhTransport,
2230
- source: RepositorySnapshot,
2231
- op: Extract<TrackerOperation, { op: "upsert-task" }>,
2232
- child: GithubIssue,
2233
- boundNumber: number | undefined,
2234
- desiredBlockers: GithubIssue[],
2235
- approvedFinal: { title: string; body: string } | undefined,
2236
- amendmentContext: AmendmentExecutionContext | undefined = undefined,
2237
- ): Promise<GithubTrackerResult> { // eslint-disable-line @typescript-eslint/no-unused-vars -- boundNumber retained for call-site symmetry
2238
- const parent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
2239
- const body = childBody(source.repository, op, parent.kind === "found" ? parent.issue : child);
2240
- const oversized = bodyLimitFailure(op.op, body, MAX_TERMINAL_SUFFIX_BYTES);
2241
- if (oversized) return oversized;
2242
- const title = taskIssueTitle(op, sliceOrdinalFromChecklist(parent.kind === "found" ? parent.issue.body : child.body, op.slice_id, op.projection?.slice_ordinal ?? 1));
2243
- // Parent content expectation for pre-write revalidation: on the amendment
2244
- // path the expectation is always the fixed approved final Parent bytes —
2245
- // never re-adopt freshly observed content as the baseline, so an edit that
2246
- // lands between the Parent write and this Child write fails closed. On the
2247
- // non-amendment path (no amendmentContext) no Parent content expectation is
2248
- // enforced here.
2249
- const parentApprovedContent = amendmentContext?.parent;
2250
- const parentBoundNumber = amendmentContext?.parentIssueNumber;
2251
- const baseBody = child.body;
2252
- const suffixEvent = issueTerminalEventId(baseBody);
2253
- if (suffixEvent === "multiple") return result(op.op, "ambiguous_remote_state", "pending Task has multiple terminal markers", child);
2254
- if (suffixEvent === "malformed") return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
2255
- // An open Child left by a failed terminal close retains its validated terminal
2256
- // suffix; the approved-final bytes keep that suffix so the original-input batch
2257
- // retry converges instead of failing closed on its own partial write.
2258
- let finalBody = suffixEvent !== null ? `${body.trimEnd()}${terminalSuffix(suffixEvent)}` : body;
2259
- // The approved-final bytes are suffix-free by construction; a Child carrying a
2260
- // validated terminal suffix matches when its suffix-free bytes are exact.
2261
- const approvedMatches = approvedFinal !== undefined && carriesApprovedContent(title, body, approvedFinal);
2262
- if (!approvedMatches)
2263
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a terminal suffix that diverges from the approved amendment content`, child);
2264
- // R4 re-attach: a Child that already carries the exact approved-final content
2265
- // but lost its native Sub-issue attachment (a detached intermediate state from
2266
- // a prior partial write) is re-attached instead of failing closed, keeping the
2267
- // original batch retryable. The Child must be bound to this amendment's
2268
- // Parent (its marker Initiative) and carry no other parent edge — a foreign
2269
- // attachment is ambiguous remote state, never re-attached.
2270
- const approvedNow = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
2271
- if (approvedNow) {
2272
- const currentParent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
2273
- if (currentParent.kind !== "found")
2274
- return result(op.op, "ambiguous_remote_state", "pending Task Parent is not observable before attachment convergence", child);
2275
- // Re-read and revalidate the Child immediately before the attachment
2276
- // mutation: the earlier snapshot may have raced a concurrent edit or
2277
- // close. exact identity, open state and approved-final bytes must hold,
2278
- // or the re-attach fails closed with zero relation writes. The attachment
2279
- // check is skipped here: the re-attach itself converges it and verifies
2280
- // the relation after the write.
2281
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, undefined, true, parentApprovedContent, parentBoundNumber);
2282
- if ("contract" in revalidated) return revalidated;
2283
- const targetParentNumber = parentBoundNumber ?? currentParent.issue.number;
2284
- const attachment = await confirmAttachment(root, gh, op.op, source.repository, targetParentNumber, child.number);
2285
- if (!("attached" in attachment)) return attachment;
2286
- if (!attachment.attached) {
2287
- const attached = await attachSubIssue(root, gh, op.op, source.repository, targetParentNumber, child);
2288
- if (!("attached" in attached)) return attached;
2133
+ repository: RepositoryInfo,
2134
+ number: number,
2135
+ ): Promise<GithubTrackerResult | number> {
2136
+ const read = await gh.run(
2137
+ ["api", "--jq", ".id", `repos/${repository.name_with_owner}/issues/${number}`],
2138
+ { cwd: root },
2139
+ );
2140
+ if (read.exit_code !== 0 || read.output_exceeded)
2141
+ return ghFailure("upsert-task", read, `cannot resolve the database id of Issue #${number}`);
2142
+ const id = Number(read.stdout.trim());
2143
+ return Number.isSafeInteger(id) && id > 0
2144
+ ? id
2145
+ : result("upsert-task", "retryable_failure", `Issue #${number} identity could not be resolved`);
2146
+ }
2147
+
2148
+ /**
2149
+ * Direct publication: S1 of docs/specs/tracker-direct-publication.spec.md.
2150
+ *
2151
+ * Mirrors the `to-spec`/`to-tickets` write flow. One start-of-run read of the
2152
+ * repository identity and the Issue listing (plus the label list, resolved by
2153
+ * the caller), then one create per absent Issue in dependency order, one native
2154
+ * Sub-issue attach per Child, and one `blocked_by` write per dependency edge --
2155
+ * every Issue number and id taken from its create response. Nothing is re-read
2156
+ * after a write, so a half-landed batch is recovered by rerunning the same
2157
+ * approved manifest: the next start listing adopts whatever landed and only the
2158
+ * missing relations are written. The reads that are not the start listing -- the
2159
+ * Parent's native Sub-issue list, each adopted Child's `blocked_by` set, and one
2160
+ * id resolution per created Issue -- are targeted at Issues the batch itself
2161
+ * owns and all happen before the relation write they inform.
2162
+ */
2163
+ async function publishInitiativeDirect(
2164
+ root: string,
2165
+ gh: GhTransport,
2166
+ prepared: ReturnType<typeof preflightPublication>,
2167
+ initial: RepositorySnapshot,
2168
+ ): Promise<GithubInitiativePublicationResult> {
2169
+ const plannedTaskIds = prepared.order.map((operation) => operation.task_id);
2170
+ const initiativeId = prepared.initiative.initiative_id;
2171
+ const repository = initial.repository;
2172
+ const desiredParentTitle = initiativeIssueTitle(initiativeId, prepared.initiative.projection);
2173
+ const desiredParentBody = createInitiativeBody(repository, prepared.initiative);
2174
+ let initiativeResult: GithubTrackerResult | undefined;
2175
+ const taskResults: GithubInitiativePublicationResult["tasks"] = [];
2176
+ const failed = (failure: GithubTrackerResult): GithubInitiativePublicationResult =>
2177
+ publicationResultWithPlan(plannedTaskIds, failure.status, failure.message, initiativeResult, taskResults);
2178
+
2179
+ const parentLookup = initiativeLookup(initial.issues, repository.id, initiativeId);
2180
+ if (parentLookup.kind === "ambiguous")
2181
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", parentLookup.message);
2182
+ const existingParent = parentLookup.kind === "found" ? parentLookup.issue : null;
2183
+ // A closed Parent is start-of-run drift: it is state the listing already
2184
+ // reveals, so it fails closed here rather than being adopted as current.
2185
+ if (existingParent && existingParent.state !== "open")
2186
+ return publicationResultWithPlan(
2187
+ plannedTaskIds,
2188
+ "ambiguous_remote_state",
2189
+ "Initiative Parent is no longer open",
2190
+ result("create-initiative", "ambiguous_remote_state", "Initiative Parent is no longer open", existingParent),
2191
+ );
2192
+ // Whole-batch validation before the first write: every already-published
2193
+ // Child of this batch must still be the one this Intent approves, including
2194
+ // its published Intent identity. This runs before the Parent content check
2195
+ // because the Parent body embeds the Child goal prose, so a stale Intent must
2196
+ // be reported as the identity mismatch it is, not as Parent drift.
2197
+ const batchDrift = publicationBatchDrift(
2198
+ initial,
2199
+ initiativeId,
2200
+ prepared.order as Extract<TrackerOperation, { op: "upsert-task" }>[],
2201
+ );
2202
+ if (batchDrift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", batchDrift);
2203
+ // The Parent is never rewritten: an existing Parent that no longer carries
2204
+ // the exact approved bytes is a permanent, caller-fixable drift.
2205
+ if (existingParent && (existingParent.title !== desiredParentTitle || existingParent.body !== desiredParentBody))
2206
+ return publicationResultWithPlan(
2207
+ plannedTaskIds,
2208
+ "permanent_failure",
2209
+ "Initiative Issue already exists and the tracker never rewrites it; edit the GitHub source directly for later planning changes",
2210
+ result("create-initiative", "permanent_failure", "Initiative Issue already exists and the tracker never rewrites it", existingParent),
2211
+ );
2212
+
2213
+ const existingChildren = new Map<string, GithubIssue>();
2214
+ for (const operation of prepared.order) {
2215
+ const owned = ownedTaskLookup(initial.issues, repository.id, operation.task_id, initiativeId, operation.slice_id);
2216
+ if (owned.kind === "ambiguous")
2217
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", owned.message);
2218
+ if (owned.kind === "found") existingChildren.set(operation.task_id, owned.issue);
2219
+ }
2220
+ // An owned Child whose Parent is gone cannot be adopted under a new Parent:
2221
+ // the Child still carries the old Parent link, so recreating the Parent would
2222
+ // silently re-home it. This is caller-fixable remote drift, not a create path.
2223
+ if (!existingParent && existingChildren.size > 0)
2224
+ return publicationResultWithPlan(
2225
+ plannedTaskIds,
2226
+ "ambiguous_remote_state",
2227
+ "an owned Child of this Initiative exists but its Parent Issue is missing; the tracker never recreates a Parent over its existing Children",
2228
+ );
2229
+ // A closed Child is start-of-run drift for the same reason as a closed Parent.
2230
+ for (const operation of prepared.order) {
2231
+ const issue = existingChildren.get(operation.task_id);
2232
+ if (issue && issue.state !== "open")
2233
+ return publicationResultWithPlan(
2234
+ plannedTaskIds,
2235
+ "ambiguous_remote_state",
2236
+ `Task ${operation.task_id} is no longer open`,
2237
+ result("upsert-task", "ambiguous_remote_state", `Task ${operation.task_id} is no longer open`, issue),
2238
+ );
2239
+ }
2240
+ if (existingParent) {
2241
+ for (const operation of prepared.order) {
2242
+ const issue = existingChildren.get(operation.task_id);
2243
+ if (!issue) continue;
2244
+ const title = taskIssueTitle(operation, sliceOrdinalFromChecklist(desiredParentBody, operation.slice_id, operation.projection?.slice_ordinal ?? 1));
2245
+ const body = childBody(repository, operation, existingParent);
2246
+ if (issue.title !== title || !sameTrackedBody(issue.body, body))
2247
+ return publicationResultWithPlan(
2248
+ plannedTaskIds,
2249
+ "ambiguous_remote_state",
2250
+ `Task ${operation.task_id} published content no longer matches the approved Intent publication`,
2251
+ result("upsert-task", "ambiguous_remote_state", `Task ${operation.task_id} published content drifted`, issue),
2252
+ );
2289
2253
  }
2290
2254
  }
2291
- // The authoritative remote observation for the write decision is the
2292
- // revalidated pre-write read, not the earlier snapshot: a terminal suffix
2293
- // that landed after the snapshot must be preserved exactly, never
2294
- // overwritten by bytes computed from stale data.
2295
- let observed = child;
2296
- const desiredLabels = desiredTaskLabels(op);
2297
- if (child.title !== title || child.body !== finalBody || labelMutationArgs(child.labels, desiredLabels).length) {
2298
- // Re-read the Child immediately before writing: earlier blocker ownership
2299
- // reads may have raced a concurrent user edit. The bound issue_number must
2300
- // still hold and the remote content must still be baseline-or-approved-final.
2301
- // revalidatePendingChildBeforeWrite additionally rechecks native Parent
2302
- // attachment and exact Parent/Slice ownership so a detached or moved Child
2303
- // never receives a content rewrite.
2304
- const revalidated = await revalidatePendingChildBeforeWrite(
2305
- root, gh, child.number, op.task_id, approvedFinal,
2306
- { title: child.title, body: child.body },
2307
- false, parentApprovedContent, parentBoundNumber,
2255
+
2256
+ // The only reads besides the start listing; all before the first write. They
2257
+ // exist to find MISSING relations. A relation that is already present but not
2258
+ // approved by this Intent is drift the listing makes visible, so it fails
2259
+ // closed here instead of being reported as already_current.
2260
+ const attachedNumbers: number[] = [];
2261
+ if (existingParent) {
2262
+ const attached = await readSubIssueNumbers(root, gh, "upsert-task", repository, existingParent.number);
2263
+ if (!Array.isArray(attached))
2264
+ return publicationResultWithPlan(plannedTaskIds, attached.status, attached.message);
2265
+ if (new Set(attached).size !== attached.length)
2266
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", "the Initiative Parent lists the same Sub-issue more than once");
2267
+ const ownedNumbers = new Set([...existingChildren.values()].map((issue) => issue.number));
2268
+ if (attached.some((number) => !ownedNumbers.has(number)))
2269
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", "the Initiative Parent carries Sub-issues this batch does not own");
2270
+ attachedNumbers.push(...attached);
2271
+ }
2272
+ const existingBlockerIds = new Map<string, number[]>();
2273
+ for (const operation of prepared.order) {
2274
+ const issue = existingChildren.get(operation.task_id);
2275
+ if (!issue) continue;
2276
+ const ids = await readBlockedByIds(root, gh, "upsert-task", repository, issue.number);
2277
+ if (!Array.isArray(ids))
2278
+ return publicationResultWithPlan(plannedTaskIds, ids.status, ids.message);
2279
+ const approved = new Set<number>();
2280
+ for (const blockerId of operation.projection?.blocked_by ?? []) {
2281
+ const blocker = taskLookup(initial.issues, repository.id, blockerId);
2282
+ if (blocker.kind === "found") approved.add(blocker.issue.id);
2283
+ }
2284
+ if (ids.some((id) => !approved.has(id)))
2285
+ return publicationResultWithPlan(
2286
+ plannedTaskIds,
2287
+ "ambiguous_remote_state",
2288
+ `Task ${operation.task_id} carries native blocked_by relations this Intent does not approve`,
2289
+ );
2290
+ existingBlockerIds.set(operation.task_id, ids);
2291
+ }
2292
+ const beforeParentWrite = publicationIntentDrift(root, prepared.intent_bindings);
2293
+ if (beforeParentWrite)
2294
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", beforeParentWrite);
2295
+
2296
+ let parentNumber: number;
2297
+ if (existingParent) {
2298
+ parentNumber = existingParent.number;
2299
+ const labelArgs = labelMutationArgs(existingParent.labels, []);
2300
+ if (labelArgs.length === 0) {
2301
+ initiativeResult = result("create-initiative", "already_current", "Initiative Issue already carries the requested initial source", existingParent);
2302
+ } else {
2303
+ const edited = await gh.run(
2304
+ ["issue", "edit", String(parentNumber), "--repo", repository.name_with_owner, ...labelArgs],
2305
+ { cwd: root },
2306
+ );
2307
+ if (edited.exit_code !== 0 || edited.output_exceeded)
2308
+ return failed(ghFailure("create-initiative", edited, "Initiative Parent label convergence failed"));
2309
+ initiativeResult = result(
2310
+ "create-initiative",
2311
+ "updated",
2312
+ "Initiative Issue managed labels converged",
2313
+ { ...existingParent, labels: [] },
2314
+ );
2315
+ }
2316
+ } else {
2317
+ const created = await gh.run(
2318
+ ["issue", "create", "--repo", repository.name_with_owner, "--title", desiredParentTitle, "--body-file", "-"],
2319
+ { cwd: root, stdin: desiredParentBody },
2308
2320
  );
2309
- if ("contract" in revalidated) return revalidated;
2310
- observed = revalidated;
2311
- // A newly observed terminal suffix on the remote (approved-final content
2312
- // plus evidence appended after the snapshot) must survive this write: the
2313
- // written body keeps the observed suffix instead of the snapshot-derived one.
2314
- const observedEvent = issueTerminalEventId(observed.body);
2315
- if (observedEvent === "malformed") return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
2316
- const writeBody = typeof observedEvent === "string"
2317
- ? `${body.trimEnd()}${terminalSuffix(observedEvent)}`
2318
- : finalBody;
2319
- const labelArgs = labelMutationArgs(observed.labels, desiredLabels);
2320
- if (observed.title !== title || observed.body !== writeBody || labelArgs.length) {
2321
- const edited = await gh.run([
2322
- "issue", "edit", String(child.number), "--repo", source.repository.name_with_owner,
2321
+ if (created.exit_code !== 0 || created.output_exceeded)
2322
+ return failed(ghFailure("create-initiative", created, "Initiative Issue creation failed"));
2323
+ const number = parseCreatedIssueNumber(created.stdout);
2324
+ if (number === null)
2325
+ return publicationResultWithPlan(plannedTaskIds, "retryable_failure", "Initiative creation could not be confirmed", initiativeResult, taskResults);
2326
+ parentNumber = number;
2327
+ initiativeResult = {
2328
+ contract: CONTRACT,
2329
+ operation: "create-initiative",
2330
+ status: "created",
2331
+ association_found: true,
2332
+ issue_number: number,
2333
+ issue_url: created.stdout.trim(),
2334
+ message: "Initiative Issue created as the single GitHub source",
2335
+ };
2336
+ }
2337
+ const parentReference: GithubIssue = existingParent ?? {
2338
+ id: 0,
2339
+ number: parentNumber,
2340
+ url: initiativeResult.issue_url ?? `https://github.com/${repository.name_with_owner}/issues/${parentNumber}`,
2341
+ title: desiredParentTitle,
2342
+ body: desiredParentBody,
2343
+ state: "open",
2344
+ state_reason: null,
2345
+ labels: [],
2346
+ };
2347
+ const numberByTask = new Map<string, number>();
2348
+ const numberByTaskId = new Map<string, number>();
2349
+ const blockerNumber = (blockerId: string): number | undefined =>
2350
+ numberByTask.get(blockerId) ?? existingChildren.get(blockerId)?.number;
2351
+
2352
+ for (const operation of prepared.order) {
2353
+ const intentDrift = publicationIntentDrift(root, prepared.intent_bindings, [operation.task_id]);
2354
+ if (intentDrift) return failed(result("upsert-task", "ambiguous_remote_state", intentDrift));
2355
+ const blockers: Array<{ taskId: string; number: number; id: number | undefined }> = [];
2356
+ for (const blockerId of operation.projection?.blocked_by ?? []) {
2357
+ if (blockerId === operation.task_id)
2358
+ return failed(result("upsert-task", "ambiguous_remote_state", "a Task cannot block itself"));
2359
+ const number = blockerNumber(blockerId);
2360
+ if (number === undefined)
2361
+ return failed(result("upsert-task", "permanent_failure", `blocking Task ${blockerId} has not been published`));
2362
+ blockers.push({ taskId: blockerId, number, id: numberByTaskId.get(blockerId) ?? existingChildren.get(blockerId)?.id });
2363
+ }
2364
+ const existing = existingChildren.get(operation.task_id);
2365
+ if (existing) {
2366
+ let mutated = false;
2367
+ const labelArgs = labelMutationArgs(existing.labels, desiredTaskLabels(operation));
2368
+ if (labelArgs.length) {
2369
+ const edited = await gh.run(
2370
+ ["issue", "edit", String(existing.number), "--repo", repository.name_with_owner, ...labelArgs],
2371
+ { cwd: root },
2372
+ );
2373
+ if (edited.exit_code !== 0 || edited.output_exceeded)
2374
+ return failed(ghFailure("upsert-task", edited, `Task Issue #${existing.number} label convergence failed`));
2375
+ mutated = true;
2376
+ }
2377
+ if (!attachedNumbers.includes(existing.number)) {
2378
+ const attach = await attachChildDirect(root, gh, repository, parentNumber, existing.id, existing.number);
2379
+ if ("contract" in attach) return failed(attach);
2380
+ attachedNumbers.push(existing.number);
2381
+ mutated = true;
2382
+ }
2383
+ const currentIds = existingBlockerIds.get(operation.task_id) ?? [];
2384
+ for (const blocker of blockers) {
2385
+ if (blocker.id !== undefined && currentIds.includes(blocker.id)) continue;
2386
+ const added = await addBlockedByDirect(root, gh, repository, existing.number, blocker.id, blocker.number);
2387
+ if ("contract" in added) return failed(added);
2388
+ mutated = true;
2389
+ }
2390
+ taskResults.push({
2391
+ task_id: operation.task_id,
2392
+ slice_id: operation.slice_id,
2393
+ status: mutated ? "updated" : "already_current",
2394
+ issue_number: existing.number,
2395
+ issue_url: existing.url,
2396
+ node_id: String(existing.id),
2397
+ });
2398
+ continue;
2399
+ }
2400
+ const title = taskIssueTitle(operation, sliceOrdinalFromChecklist(desiredParentBody, operation.slice_id, operation.projection?.slice_ordinal ?? 1));
2401
+ const body = childBody(repository, operation, parentReference);
2402
+ const created = await gh.run(
2403
+ [
2404
+ "issue", "create", "--repo", repository.name_with_owner,
2323
2405
  "--title", title,
2324
2406
  "--body-file", "-",
2325
- ...labelArgs,
2326
- ], { cwd: root, stdin: writeBody });
2327
- if (edited.exit_code !== 0 || edited.output_exceeded) return ghFailure(op.op, edited, `pending Task Issue #${child.number} update failed`);
2328
- finalBody = writeBody;
2407
+ ...desiredTaskLabels(operation).flatMap((label) => ["--label", label]),
2408
+ ],
2409
+ { cwd: root, stdin: body },
2410
+ );
2411
+ if (created.exit_code !== 0 || created.output_exceeded)
2412
+ return failed(ghFailure("upsert-task", created, "Task Issue creation failed"));
2413
+ const number = parseCreatedIssueNumber(created.stdout);
2414
+ if (number === null)
2415
+ return publicationResultWithPlan(plannedTaskIds, "retryable_failure", "Task creation could not be confirmed", initiativeResult, taskResults);
2416
+ const id = await resolveIssueId(root, gh, repository, number);
2417
+ if (typeof id !== "number") return failed(id);
2418
+ const attach = await attachChildDirect(root, gh, repository, parentNumber, id, number);
2419
+ if ("contract" in attach) return failed(attach);
2420
+ for (const blocker of blockers) {
2421
+ const added = await addBlockedByDirect(root, gh, repository, number, blocker.id, blocker.number);
2422
+ if ("contract" in added) return failed(added);
2329
2423
  }
2424
+ numberByTask.set(operation.task_id, number);
2425
+ numberByTaskId.set(operation.task_id, id);
2426
+ taskResults.push({
2427
+ task_id: operation.task_id,
2428
+ slice_id: operation.slice_id,
2429
+ status: "created",
2430
+ issue_number: number,
2431
+ issue_url: created.stdout.trim(),
2432
+ });
2330
2433
  }
2331
- const dependencies = await convergePendingDependencies(root, gh, source, child.number, op.task_id, desiredBlockers, approvedFinal, parentApprovedContent, parentBoundNumber);
2332
- if (!("complete" in dependencies)) return dependencies;
2333
- const refreshed = await snapshot(root, gh, op.op);
2334
- if ("contract" in refreshed) return refreshed;
2335
- const reread = ownedTaskLookup(refreshed.issues, refreshed.repository.id, op.task_id, op.initiative_id, op.slice_id);
2336
- if (reread.kind !== "found" || reread.issue.number !== child.number)
2337
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} changed identity during amendment`, child);
2338
- if (reread.issue.title !== title || reread.issue.body !== finalBody)
2339
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} update did not converge`, reread.issue);
2340
- const labelsCurrent = !desiredLabels.some((label) => !reread.issue.labels.includes(label));
2341
- if (!labelsCurrent)
2342
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} labels did not converge`, reread.issue);
2343
- const currentDependencies = await confirmBlockedBy(root, gh, op.op, refreshed.repository, reread.issue.number, desiredBlockers);
2344
- if (!("complete" in currentDependencies)) return currentDependencies;
2345
- if (!currentDependencies.complete)
2346
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} dependencies did not converge`, reread.issue);
2347
- const contentCurrent = child.title === title && child.body === finalBody
2348
- && labelMutationArgs(child.labels, desiredLabels).length === 0;
2349
- return contentCurrent
2350
- ? result(op.op, "already_current", `pending Task ${op.task_id} already carries the approved amendment content`, reread.issue)
2351
- : result(op.op, "updated", `pending Task ${op.task_id} Agent Brief updated with approved amendment content`, reread.issue);
2352
- }
2353
-
2354
- /** Converge the exact approved dependency set on a pending Child (amendment only). */
2355
- async function convergePendingDependencies(
2434
+
2435
+ const finalIntentDrift = publicationIntentDrift(root, prepared.intent_bindings);
2436
+ if (finalIntentDrift) return failed(result("upsert-task", "ambiguous_remote_state", finalIntentDrift));
2437
+ const statuses = [initiativeResult.status, ...taskResults.map((task) => task.status)];
2438
+ const status: TrackerStatus = statuses.every((item) => item === "created")
2439
+ ? "created"
2440
+ : statuses.every((item) => item === "already_current") ? "already_current" : "updated";
2441
+ const issueNumber = (taskId: string): number => numberByTask.get(taskId) ?? existingChildren.get(taskId)!.number;
2442
+ const firstTaskId = prepared.order[0].task_id;
2443
+ return publicationResultWithPlan(plannedTaskIds, status, "complete Initiative Parent, Children, and dependency graph published", initiativeResult, taskResults, {
2444
+ recommended_first_task_id: firstTaskId,
2445
+ recommended_first_issue_number: issueNumber(firstTaskId),
2446
+ order: prepared.order.map((operation) => operation.task_id),
2447
+ issue_order: prepared.order.map((operation) => issueNumber(operation.task_id)),
2448
+ parallel_groups: prepared.parallel_groups,
2449
+ parallel_issue_groups: prepared.parallel_groups.map((group) => group.map(issueNumber)),
2450
+ });
2451
+ }
2452
+
2453
+ /** Attach one Child as a native Sub-issue of the Parent. */
2454
+ async function attachChildDirect(
2356
2455
  root: string,
2357
2456
  gh: GhTransport,
2358
- source: RepositorySnapshot,
2457
+ repository: RepositoryInfo,
2458
+ parentNumber: number,
2459
+ childId: number,
2359
2460
  childNumber: number,
2360
- childTaskId: string,
2361
- requestedBlockers: GithubIssue[],
2362
- approvedFinal: { title: string; body: string } | undefined,
2363
- parentApprovedContent: { title: string; body: string } | undefined = undefined,
2364
- parentBoundNumber: number | undefined = undefined,
2461
+ ): Promise<GithubTrackerResult | { attached: true }> {
2462
+ const mutation = await gh.run(
2463
+ ["api", "-F", `sub_issue_id=${childId}`, `repos/${repository.name_with_owner}/issues/${parentNumber}/sub_issues`],
2464
+ { cwd: root },
2465
+ );
2466
+ if (mutation.exit_code !== 0 || mutation.output_exceeded)
2467
+ return ghFailure("upsert-task", mutation, `native Sub-issue attachment failed for Issue #${childNumber}`);
2468
+ return { attached: true };
2469
+ }
2470
+
2471
+ /** Create one native `blocked_by` edge; the blocker id is already known. */
2472
+ async function addBlockedByDirect(
2473
+ root: string,
2474
+ gh: GhTransport,
2475
+ repository: RepositoryInfo,
2476
+ childNumber: number,
2477
+ blockerId: number | undefined,
2478
+ blockerNumber: number,
2365
2479
  ): Promise<GithubTrackerResult | { complete: true }> {
2366
- const expected = requestedBlockers.map((blocker) => blocker.id);
2367
- const existing = await readBlockedByIds(root, gh, "upsert-task", source.repository, childNumber);
2368
- if (!Array.isArray(existing)) return existing;
2369
- const removed = existing.filter((id) => !expected.includes(id));
2370
- for (const id of removed) {
2371
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
2372
- if ("contract" in revalidated) return revalidated;
2373
- const mutation = await gh.run([
2374
- "api", "--method", "DELETE",
2375
- `repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by/${id}`,
2376
- ], { cwd: root });
2377
- if (mutation.exit_code !== 0 || mutation.output_exceeded)
2378
- return ghFailure("upsert-task", mutation, `native blocked_by removal failed for Issue #${childNumber}`);
2379
- }
2380
- const additions = requestedBlockers.filter((blocker) => !existing.includes(blocker.id));
2381
- for (const blocker of additions) {
2382
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
2383
- if ("contract" in revalidated) return revalidated;
2384
- const mutation = await gh.run([
2385
- "api", "-F", `issue_id=${blocker.id}`,
2386
- `repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by`,
2387
- ], { cwd: root });
2388
- if (mutation.exit_code !== 0 || mutation.output_exceeded)
2389
- return ghFailure("upsert-task", mutation, `native blocked_by attachment failed for Issue #${blocker.number}`);
2390
- }
2391
- const confirm = await confirmBlockedBy(root, gh, "upsert-task", source.repository, childNumber, requestedBlockers);
2392
- if (!("complete" in confirm)) return confirm;
2393
- return confirm.complete ? { complete: true } : { complete: true };
2480
+ const resolved = blockerId ?? await resolveIssueId(root, gh, repository, blockerNumber);
2481
+ if (typeof resolved !== "number") return resolved;
2482
+ const mutation = await gh.run(
2483
+ ["api", "-F", `issue_id=${resolved}`, `repos/${repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by`],
2484
+ { cwd: root },
2485
+ );
2486
+ if (mutation.exit_code !== 0 || mutation.output_exceeded)
2487
+ return ghFailure("upsert-task", mutation, `native blocked_by attachment failed for Issue #${childNumber}`);
2488
+ return { complete: true };
2394
2489
  }
2395
2490
 
2396
2491
  /**
2397
- * Re-snapshot and re-validate a pending Child's bound identity, open state,
2398
- * ownership, and baseline-or-approved-final content immediately before each
2399
- * dependency write. Any drift stops the remaining mutations and fails closed
2400
- * as ambiguous remote state. On success returns the validated Child
2401
- * observation so callers can write the bytes actually present on the remote
2402
- * (preserving a terminal suffix that landed after their earlier snapshot).
2403
- * The return is discriminated by `contract`: a GithubTrackerResult failure
2404
- * short-circuits the caller, a GithubIssue is the validated observation.
2492
+ * Direct amendment: S2 of docs/specs/tracker-direct-publication.spec.md.
2493
+ *
2494
+ * The approved baseline is verified once at the start, against the start listing
2495
+ * plus Issue-scoped reads (the Parent's Sub-issue list and each adopted Child's
2496
+ * `blocked_by` set). Then the Parent is edited at most once, each pending brief is
2497
+ * edited at most once, absent pending Children are created as in S1, and each
2498
+ * pending Child's `blocked_by` set is converged to the approved set from the
2499
+ * start read. Nothing is re-read after a write and no closing recheck runs, so a
2500
+ * baseline drift that appears after the start listing is not detected in this
2501
+ * run; it surfaces at the start of the next run.
2405
2502
  */
2406
- async function revalidatePendingChildBeforeWrite(
2503
+ async function publishAmendmentDirect(
2407
2504
  root: string,
2408
2505
  gh: GhTransport,
2409
- childNumber: number,
2410
- childTaskId: string,
2411
- approvedFinal: { title: string; body: string } | undefined,
2412
- /** Additional byte-exact content this Child is allowed to carry (e.g. its pre-update baseline). */
2413
- allowedBaseline: { title: string; body: string } | undefined = undefined,
2414
- /** When true the native Sub-issue attachment check is skipped (re-attach phase: the attachment write itself is about to run). */
2415
- skipAttachmentCheck = false,
2416
- /** Expected exact Parent bytes (approved final after the Parent write, baseline before it); undefined skips the Parent content check. */
2417
- parentApproved: { title: string; body: string } | undefined = undefined,
2418
- /** Expected exact bound Parent issue number; undefined skips the Parent issue_number check. */
2419
- parentExpectedNumber: number | undefined = undefined,
2420
- ): Promise<GithubTrackerResult | GithubIssue> {
2421
- const refreshed = await snapshot(root, gh, "upsert-task");
2422
- if ("contract" in refreshed) return refreshed;
2423
- // Repository-wide identity resolution: a duplicate Task Issue introduced after
2424
- // the caller's snapshot must fail closed here, not at a later lookup — the
2425
- // immediate pre-write observation is the authoritative one.
2426
- const resolved = taskLookup(refreshed.issues, refreshed.repository.id, childTaskId);
2427
- if (resolved.kind !== "found")
2428
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} is not uniquely owned before a write: ${resolved.kind}`);
2429
- const child = resolved.issue;
2430
- if (child.number !== childNumber)
2431
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} resolves to Issue #${child.number}, not the bound Issue #${childNumber}`, child);
2432
- if (child.state !== "open")
2433
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer open before a dependency write`, child);
2434
- const contentFinal = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
2435
- const contentBaseline = allowedBaseline !== undefined && child.title === allowedBaseline.title && child.body === allowedBaseline.body;
2436
- if (!contentFinal && !contentBaseline)
2437
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) content is no longer the approved final bytes before a dependency write`, child);
2438
- const initiativeId = [...child.body.matchAll(/<!-- immune-brain:initiative-id=([A-Za-z0-9._:-]+) -->/g)].map((match) => match[1])[0];
2439
- if (!initiativeId)
2440
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its Initiative marker before a dependency write`, child);
2441
- const parent = initiativeLookup(refreshed.issues, refreshed.repository.id, initiativeId);
2442
- if (parent.kind !== "found" || parent.issue.state !== "open")
2443
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its open Parent before a dependency write`, child);
2444
- if (parentExpectedNumber !== undefined && parent.issue.number !== parentExpectedNumber)
2445
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent resolves to Issue #${parent.issue.number}, not the bound Parent Issue #${parentExpectedNumber} before a dependency write`, child);
2446
- // The Parent must still carry exactly the expected amendment bytes before any
2447
- // Child write: after the Parent write it is the approved final content; before
2448
- // the Parent write (non-amendment or pre-write paths) the earlier caller
2449
- // snapshot bytes apply. A concurrent edit that keeps ownership markers intact
2450
- // must fail closed here, not after the dependency mutations in final verification.
2451
- if (parentApproved !== undefined
2452
- && (parent.issue.title !== parentApproved.title || parent.issue.body !== parentApproved.body))
2453
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent changed since the approved amendment content before a dependency write`, child);
2454
- // Native Sub-issue attachment must still hold immediately before each
2455
- // dependency write: a detached Child no longer belongs to the amendment.
2456
- if (!skipAttachmentCheck) {
2457
- const attachedNow = await readSubIssueNumbers(root, gh, "upsert-task", refreshed.repository, parent.issue.number);
2458
- if (!Array.isArray(attachedNow)) return attachedNow;
2459
- if (!attachedNow.includes(childNumber))
2460
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer attached to the Parent before a dependency write`, child);
2506
+ prepared: ReturnType<typeof preflightPublication>,
2507
+ initial: RepositorySnapshot,
2508
+ amendment: ReturnType<typeof validateAmendment>,
2509
+ context: AmendmentExecutionContext,
2510
+ ): Promise<GithubInitiativePublicationResult> {
2511
+ const plannedTaskIds = prepared.order.map((operation) => operation.task_id);
2512
+ const initiativeId = prepared.initiative.initiative_id;
2513
+ const repository = initial.repository;
2514
+ const taskResults: GithubInitiativePublicationResult["tasks"] = [];
2515
+ // The confirmed Parent result, when it exists, must survive a later Child or
2516
+ // relation failure: a step whose response was already received is not lost just
2517
+ // because a later step failed. Set once the Parent write has been confirmed.
2518
+ let confirmedParent: GithubTrackerResult | undefined;
2519
+ const failed = (failure: GithubTrackerResult): GithubInitiativePublicationResult =>
2520
+ publicationResultWithPlan(plannedTaskIds, failure.status, failure.message, confirmedParent, taskResults);
2521
+
2522
+ const parentLookup = initiativeLookup(initial.issues, repository.id, initiativeId);
2523
+ if (parentLookup.kind !== "found")
2524
+ return publicationResultWithPlan(plannedTaskIds, "permanent_failure", "an amendment requires the Initiative Parent to already exist");
2525
+ const parentIssue = parentLookup.issue;
2526
+
2527
+ // Baseline verification, once, before any write: membership, bound identity,
2528
+ // baseline-or-approved-final content, native attachment and terminal ownership.
2529
+ const topology = await validateAmendmentTopology(
2530
+ root, gh, initial, initiativeId, parentIssue,
2531
+ amendment.tasks, amendment.historical,
2532
+ prepared.foreign_dependers,
2533
+ { pendingContent: context.pendingContent, parent: context.parent, historicalSlices: context.historicalSlices },
2534
+ prepared.order,
2535
+ );
2536
+ if (!("get" in topology)) return failed(topology);
2537
+ const bound = topology;
2538
+
2539
+ // The only reads besides the start listing; all before the first write. They
2540
+ // exist to find MISSING relations, so an unapproved relation already present
2541
+ // fails closed here instead of being reported as already_current.
2542
+ const attached = await readSubIssueNumbers(root, gh, "upsert-task", repository, parentIssue.number);
2543
+ if (!Array.isArray(attached)) return failed(attached);
2544
+ if (new Set(attached).size !== attached.length)
2545
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", "the Initiative Parent lists the same Sub-issue more than once");
2546
+ const attachedNumbers = new Set(attached);
2547
+ const blockerIdsByTask = new Map<string, number[]>();
2548
+ for (const operation of prepared.order) {
2549
+ const issue = bound.get(operation.task_id);
2550
+ if (!issue) continue;
2551
+ const ids = await readBlockedByIds(root, gh, "upsert-task", repository, issue.number);
2552
+ if (!Array.isArray(ids)) return failed(ids);
2553
+ // The approved set is the convergence target, so an unapproved edge present at
2554
+ // start is simply removed by the same run; it is state this run owns, not drift.
2555
+ blockerIdsByTask.set(operation.task_id, ids);
2461
2556
  }
2462
- const sliceId = ownershipMarkerValue(child.body, "slice-id");
2463
- if (sliceId && sliceCount(parent.issue.body, sliceId) !== 1)
2464
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its exact Slice in the Parent before a dependency write`, child);
2465
- return child;
2557
+ const beforeWrite = publicationIntentDrift(root, prepared.intent_bindings);
2558
+ if (beforeWrite) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", beforeWrite);
2559
+
2560
+ // One Parent edit at most, and only when the approved bytes or labels differ.
2561
+ const parentNeedsEdit = parentIssue.title !== context.parent.title
2562
+ || parentIssue.body !== context.parent.body
2563
+ || labelMutationArgs(parentIssue.labels, []).length > 0;
2564
+ let parentResult: GithubTrackerResult;
2565
+ if (!parentNeedsEdit) {
2566
+ parentResult = result("create-initiative", "already_current", "Initiative Issue already carries the requested amended content", parentIssue);
2567
+ } else {
2568
+ const edited = await gh.run(
2569
+ [
2570
+ "issue", "edit", String(parentIssue.number), "--repo", repository.name_with_owner,
2571
+ "--title", context.parent.title,
2572
+ "--body-file", "-",
2573
+ ...labelMutationArgs(parentIssue.labels, []),
2574
+ ],
2575
+ { cwd: root, stdin: context.parent.body },
2576
+ );
2577
+ if (edited.exit_code !== 0 || edited.output_exceeded)
2578
+ return failed(ghFailure("create-initiative", edited, "Initiative amendment edit failed"));
2579
+ parentResult = result("create-initiative", "updated", "Initiative Issue updated with approved amendment content", {
2580
+ ...parentIssue,
2581
+ title: context.parent.title,
2582
+ body: context.parent.body,
2583
+ labels: [],
2584
+ });
2585
+ }
2586
+ confirmedParent = parentResult;
2587
+
2588
+ const numberByTask = new Map<string, number>();
2589
+ const idByTask = new Map<string, number>();
2590
+ const blockerRef = (blockerId: string): { number: number; id: number } | undefined => {
2591
+ const number = numberByTask.get(blockerId) ?? bound.get(blockerId)?.number;
2592
+ if (number === undefined) return undefined;
2593
+ const observedBlocker = taskLookup(initial.issues, repository.id, blockerId);
2594
+ const id = idByTask.get(blockerId) ?? bound.get(blockerId)?.id
2595
+ ?? (observedBlocker.kind === "found" ? observedBlocker.issue.id : undefined);
2596
+ if (id === undefined || !Number.isSafeInteger(id)) return undefined;
2597
+ return { number, id };
2598
+ };
2599
+
2600
+ for (const operation of prepared.order) {
2601
+ const intentDrift = publicationIntentDrift(root, prepared.intent_bindings, [operation.task_id]);
2602
+ if (intentDrift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", intentDrift, parentResult, taskResults);
2603
+ const approved = context.pendingContent.get(operation.task_id);
2604
+ if (!approved) return failed(result("upsert-task", "permanent_failure", `pending Task ${operation.task_id} has no approved amendment content`));
2605
+ const desiredBlockers: Array<{ number: number; id: number }> = [];
2606
+ for (const blockerId of operation.projection?.blocked_by ?? []) {
2607
+ if (blockerId === operation.task_id)
2608
+ return failed(result("upsert-task", "ambiguous_remote_state", "a Task cannot block itself"));
2609
+ const resolved = blockerRef(blockerId);
2610
+ if (!resolved)
2611
+ return failed(result("upsert-task", "permanent_failure", `blocking Task ${blockerId} has not been published`));
2612
+ desiredBlockers.push(resolved);
2613
+ }
2614
+
2615
+ const observed = bound.get(operation.task_id);
2616
+ let number: number;
2617
+ let id: number;
2618
+ // Content/label mutation decides the reported status; relation convergence
2619
+ // alone leaves an already-current pending brief as already_current.
2620
+ let contentMutated = false;
2621
+ if (observed) {
2622
+ number = observed.number;
2623
+ id = observed.id;
2624
+ } else {
2625
+ // Absent pending Child: one create, number from the create response and id
2626
+ // resolved once for the relation endpoints (Assumption A1).
2627
+ const created = await gh.run(
2628
+ [
2629
+ "issue", "create", "--repo", repository.name_with_owner,
2630
+ "--title", approved.title,
2631
+ "--body-file", "-",
2632
+ ...desiredTaskLabels(operation).flatMap((label) => ["--label", label]),
2633
+ ],
2634
+ { cwd: root, stdin: approved.body },
2635
+ );
2636
+ if (created.exit_code !== 0 || created.output_exceeded)
2637
+ return failed(ghFailure("upsert-task", created, `Task Issue creation failed for ${operation.task_id}`));
2638
+ const parsed = parseCreatedIssueNumber(created.stdout);
2639
+ if (parsed === null)
2640
+ return publicationResultWithPlan(plannedTaskIds, "retryable_failure", `Task ${operation.task_id} creation could not be confirmed`, parentResult, taskResults);
2641
+ number = parsed;
2642
+ const resolvedId = await resolveIssueId(root, gh, repository, number);
2643
+ if (typeof resolvedId !== "number") return failed(resolvedId);
2644
+ id = resolvedId;
2645
+ }
2646
+
2647
+ if (!attachedNumbers.has(number)) {
2648
+ const attach = await attachChildDirect(root, gh, repository, parentIssue.number, id, number);
2649
+ if ("contract" in attach) return failed(attach);
2650
+ attachedNumbers.add(number);
2651
+ }
2652
+
2653
+ if (observed) {
2654
+ // One edit at most, and only when the approved content or labels differ. A
2655
+ // validated terminal suffix observed in the start listing is terminal
2656
+ // evidence, so it is preserved by the rewrite instead of being dropped.
2657
+ const suffixEvent = issueTerminalEventId(observed.body);
2658
+ if (suffixEvent === "multiple")
2659
+ return failed(result("upsert-task", "ambiguous_remote_state", `pending Task ${operation.task_id} has multiple terminal markers`, observed));
2660
+ if (suffixEvent === "malformed")
2661
+ return failed(result("upsert-task", "ambiguous_remote_state", `pending Task ${operation.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, observed));
2662
+ const writeBody = suffixEvent !== null ? `${approved.body.trimEnd()}${terminalSuffix(suffixEvent)}` : approved.body;
2663
+ const labelArgs = labelMutationArgs(observed.labels, desiredTaskLabels(operation));
2664
+ if (!carriesApprovedContent(observed.title, observed.body, approved) || labelArgs.length) {
2665
+ const edited = await gh.run(
2666
+ [
2667
+ "issue", "edit", String(number), "--repo", repository.name_with_owner,
2668
+ "--title", approved.title,
2669
+ "--body-file", "-",
2670
+ ...labelArgs,
2671
+ ],
2672
+ { cwd: root, stdin: writeBody },
2673
+ );
2674
+ if (edited.exit_code !== 0 || edited.output_exceeded)
2675
+ return failed(ghFailure("upsert-task", edited, `pending Task Issue #${number} update failed`));
2676
+ contentMutated = true;
2677
+ }
2678
+ }
2679
+
2680
+ // Converge the approved dependency set from the start read: no read follows
2681
+ // the relation writes, and only the missing or unapproved edges are touched.
2682
+ const current = blockerIdsByTask.get(operation.task_id) ?? [];
2683
+ const expected = desiredBlockers.map((blocker) => blocker.id);
2684
+ for (const removedId of current.filter((currentId) => !expected.includes(currentId))) {
2685
+ const mutation = await gh.run(
2686
+ ["api", "--method", "DELETE", `repos/${repository.name_with_owner}/issues/${number}/dependencies/blocked_by/${removedId}`],
2687
+ { cwd: root },
2688
+ );
2689
+ if (mutation.exit_code !== 0 || mutation.output_exceeded)
2690
+ return failed(ghFailure("upsert-task", mutation, `native blocked_by removal failed for Issue #${number}`));
2691
+ }
2692
+ for (const blocker of desiredBlockers.filter((candidate) => !current.includes(candidate.id))) {
2693
+ const added = await addBlockedByDirect(root, gh, repository, number, blocker.id, blocker.number);
2694
+ if ("contract" in added) return failed(added);
2695
+ }
2696
+
2697
+ numberByTask.set(operation.task_id, number);
2698
+ idByTask.set(operation.task_id, id);
2699
+ taskResults.push({
2700
+ task_id: operation.task_id,
2701
+ slice_id: operation.slice_id,
2702
+ status: !observed ? "created" : contentMutated ? "updated" : "already_current",
2703
+ issue_number: number,
2704
+ issue_url: observed?.url ?? `https://github.com/${repository.name_with_owner}/issues/${number}`,
2705
+ });
2706
+ }
2707
+
2708
+ const finalIntentDrift = publicationIntentDrift(root, prepared.intent_bindings);
2709
+ if (finalIntentDrift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", finalIntentDrift, parentResult, taskResults);
2710
+ const statuses = [parentResult.status, ...taskResults.map((task) => task.status)];
2711
+ const status: TrackerStatus = statuses.every((item) => item === "created")
2712
+ ? "created"
2713
+ : statuses.every((item) => item === "already_current") ? "already_current" : "updated";
2714
+ const issueNumber = (taskId: string): number => numberByTask.get(taskId) ?? bound.get(taskId)!.number;
2715
+ const firstTaskId = prepared.order[0].task_id;
2716
+ return publicationResultWithPlan(plannedTaskIds, status, "complete Initiative Parent, Children, and dependency graph published", parentResult, taskResults, {
2717
+ recommended_first_task_id: firstTaskId,
2718
+ recommended_first_issue_number: issueNumber(firstTaskId),
2719
+ order: prepared.order.map((operation) => operation.task_id),
2720
+ issue_order: prepared.order.map((operation) => issueNumber(operation.task_id)),
2721
+ parallel_groups: prepared.parallel_groups,
2722
+ parallel_issue_groups: prepared.parallel_groups.map((group) => group.map(issueNumber)),
2723
+ });
2466
2724
  }
2467
2725
 
2468
2726
  export async function runGithubInitiativePublication(
2469
2727
  root: string,
2470
2728
  input: InitiativePublicationInput,
2471
2729
  rawGh: GhTransport = createGhTransport(),
2472
- budget: OperationBudget = { deadline_ms: Date.now() + PUBLICATION_BUDGET_MS },
2730
+ budget?: OperationCancellation,
2473
2731
  ): Promise<GithubInitiativePublicationResult> {
2474
2732
  const absoluteRoot = resolve(root);
2475
- // The whole publication shares one deadline and cancellation signal, supplied
2476
- // by the caller so one entry point owns the budget. The incoming transport is
2477
- // shadowed by this budget-carrying adapter, so *every* remote call in the
2478
- // publication -- including the nested label and amendment helpers -- carries
2479
- // the same budget by construction instead of by remembering to pass it.
2480
- const gh: GhTransport = {
2481
- run: (args, options = {}) => rawGh.run(args, { ...options, budget }),
2482
- };
2733
+ // One publication call is a finite remote step sequence with no cumulative
2734
+ // deadline: the caller may still supply a cancellation signal, which is
2735
+ // forwarded to every call, and each call keeps its own per-call timeout.
2736
+ const gh: GhTransport = budget?.signal === undefined
2737
+ ? rawGh
2738
+ : { run: (args, options = {}) => rawGh.run(args, { ...options, signal: budget.signal }) };
2483
2739
  // The complete planned step set, known before the first remote write. Every
2484
2740
  // unconfirmed Task from this set is a pending step, so a Parent failure or an
2485
2741
  // early Child failure still names each unfinished Task.
2486
2742
  let prepared: ReturnType<typeof preflightPublication>;
2487
2743
  let amendment: ReturnType<typeof validateAmendment> | undefined;
2488
- let amendmentContext: AmendmentExecutionContext | undefined;
2489
2744
  // Assigned the moment the plan exists. Every result built after that point
2490
2745
  // reports the same planned Task set, including the error paths.
2491
2746
  let plannedTaskIds: string[] = [];
@@ -2527,17 +2782,6 @@ export async function runGithubInitiativePublication(
2527
2782
  if (conflict) return publicationResultWithPlan(plannedTaskIds, conflict.status, conflict.message, conflict);
2528
2783
  const initial = await snapshot(absoluteRoot, gh, "create-initiative");
2529
2784
  if ("contract" in initial) return publicationResultWithPlan(plannedTaskIds, initial.status, initial.message, initial);
2530
- if (!amendment) {
2531
- // Whole-batch prevalidation: every already published Child of this batch must
2532
- // still match the Intent being published, and the check happens before the
2533
- // first recovery write rather than after earlier Children were written.
2534
- const batchDrift = publicationBatchDrift(
2535
- initial,
2536
- prepared.initiative.initiative_id,
2537
- prepared.order as Extract<TrackerOperation, { op: "upsert-task" }>[],
2538
- );
2539
- if (batchDrift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", batchDrift);
2540
- }
2541
2785
  const initialParent = initiativeLookup(initial.issues, initial.repository.id, prepared.initiative.initiative_id);
2542
2786
  if (initialParent.kind === "ambiguous") return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", initialParent.message);
2543
2787
  if (amendment && initialParent.kind === "missing")
@@ -2564,244 +2808,61 @@ export async function runGithubInitiativePublication(
2564
2808
  const childFailure = bodyLimitFailure("upsert-task", childBody(initial.repository, operation, parentForPreflight), MAX_TERMINAL_SUFFIX_BYTES);
2565
2809
  if (childFailure) return publicationResultWithPlan(plannedTaskIds, childFailure.status, childFailure.message, childFailure);
2566
2810
  }
2811
+ // Ordinary publication and amendment share the direct write flow: neither
2812
+ // re-reads after a write.
2813
+ if (!amendment) return publishInitiativeDirect(absoluteRoot, gh, prepared, initial);
2567
2814
 
2568
- if (amendment) {
2569
- if (initialParent.kind !== "found")
2570
- return publicationResultWithPlan(plannedTaskIds, "permanent_failure", "an amendment requires the Initiative Parent to already exist");
2571
- // Caller-controlled binding constraints (missing or duplicate historical
2572
- // Slice markers) must surface as structured fail-closed publication results,
2573
- // never as thrown exceptions escaping the guarded validation boundary.
2574
- let approvedFinalParent: ReturnType<typeof approvedAmendmentContent>;
2575
- try {
2576
- approvedFinalParent = approvedAmendmentContent(absoluteRoot, initial.repository, initialParent.issue, prepared, amendment);
2577
- } catch (error) {
2578
- return publicationResultWithPlan(
2579
- plannedTaskIds,
2580
- "permanent_failure",
2581
- error instanceof Error ? error.message : String(error),
2582
- );
2583
- }
2584
- // Slice identity collisions are deterministic input contract violations (the
2585
- // batch itself declares two Children with one Slice id), not remote drift.
2586
- if (typeof approvedFinalParent === "string" && approvedFinalParent.includes("collides with a historical Task Slice"))
2587
- return publicationResultWithPlan(plannedTaskIds, "permanent_failure", approvedFinalParent);
2588
- if (typeof approvedFinalParent === "string") return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", approvedFinalParent);
2589
- const { parent, pendingContent, historicalSlices } = approvedFinalParent;
2590
- // Deterministic prerequisite completion is validated before any write: a
2591
- // historical prerequisite already closed without state_reason "completed"
2592
- // (e.g. not_planned) makes the batch permanently unfulfillable, so it must
2593
- // fail closed in preflight with zero mutations instead of after the Parent
2594
- // and Child writes (the final check still guards concurrent races).
2595
- for (const operation of prepared.order) {
2596
- for (const blockerId of operation.projection?.blocked_by ?? []) {
2597
- if (!amendment.historical.has(blockerId)) continue;
2598
- const blocker = taskLookup(initial.issues, initial.repository.id, blockerId);
2599
- if (blocker.kind === "found" && (blocker.issue.state !== "closed" || blocker.issue.state_reason !== "completed"))
2600
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `Task ${operation.task_id} depends on stopped historical prerequisite ${blockerId}`);
2601
- }
2602
- }
2603
- const parentBaseline = amendment.parent.body !== initialParent.issue.body || amendment.parent.title !== initialParent.issue.title;
2604
- const parentApproved = initialParent.issue.title === parent.title && initialParent.issue.body === parent.body;
2605
- if (parentBaseline && !parentApproved) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `amendment Parent changed since the approved amendment baseline`);
2606
- if (amendment.parent.issue_number !== initialParent.issue.number)
2607
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `amendment Parent is bound to Issue #${amendment.parent.issue_number} but observed Issue #${initialParent.issue.number}`);
2608
- if (initialParent.issue.state !== "open")
2609
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", "an amendment requires the Initiative Parent to remain open");
2610
- const topology = await validateAmendmentTopology(
2611
- absoluteRoot, gh, initial, prepared.initiative.initiative_id, initialParent.issue,
2612
- amendment.tasks, amendment.historical,
2613
- prepared.foreign_dependers,
2614
- { pendingContent, parent, historicalSlices },
2615
- prepared.order,
2815
+ if (initialParent.kind !== "found")
2816
+ return publicationResultWithPlan(plannedTaskIds, "permanent_failure", "an amendment requires the Initiative Parent to already exist");
2817
+ // Caller-controlled binding constraints (missing or duplicate historical
2818
+ // Slice markers) must surface as structured fail-closed publication results,
2819
+ // never as thrown exceptions escaping the guarded validation boundary.
2820
+ let approvedFinalParent: ReturnType<typeof approvedAmendmentContent>;
2821
+ try {
2822
+ approvedFinalParent = approvedAmendmentContent(absoluteRoot, initial.repository, initialParent.issue, prepared, amendment);
2823
+ } catch (error) {
2824
+ return publicationResultWithPlan(
2825
+ plannedTaskIds,
2826
+ "permanent_failure",
2827
+ error instanceof Error ? error.message : String(error),
2616
2828
  );
2617
- if (!("get" in topology)) return publicationResultWithPlan(plannedTaskIds, topology.status, topology.message, topology);
2618
- const historicalRelations = new Map<string, { blocked_by: number[]; state_reason: string | null }>();
2619
- for (const [taskId] of amendment.historical) {
2620
- const issue = topology.get(taskId)!;
2621
- const observed = await readBlockedByIds(absoluteRoot, gh, "upsert-task", initial.repository, issue.number);
2622
- if (!Array.isArray(observed)) return publicationResultWithPlan(plannedTaskIds, observed.status, observed.message, observed);
2623
- historicalRelations.set(taskId, { blocked_by: observed, state_reason: issue.state_reason });
2624
- }
2625
- amendmentContext = {
2626
- pendingContent,
2627
- parent,
2628
- parentIssueNumber: amendment.parent.issue_number,
2629
- historicalSlices,
2630
- historicalRelations,
2631
- };
2632
- }
2633
-
2634
- const beforeParentWrite = publicationIntentDrift(absoluteRoot, prepared.intent_bindings);
2635
- if (beforeParentWrite) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", beforeParentWrite);
2636
- let parentResult: GithubTrackerResult;
2637
- if (amendment && amendmentContext) {
2638
- parentResult = await amendInitiativeParent(absoluteRoot, gh, prepared.initiative, initial, amendment.parent, amendmentContext);
2639
- } else {
2640
- parentResult = await runGithubTrackerOperation(absoluteRoot, prepared.initiative, gh);
2641
- }
2642
- if (!isSuccessfulTrackerStatus(parentResult.status))
2643
- return publicationResultWithPlan(plannedTaskIds, parentResult.status, parentResult.message, parentResult);
2644
- const taskResults: GithubInitiativePublicationResult["tasks"] = [];
2645
- for (const operation of prepared.order) {
2646
- const intentDrift = publicationIntentDrift(absoluteRoot, prepared.intent_bindings, [operation.task_id]);
2647
- if (intentDrift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", intentDrift, parentResult, taskResults);
2648
- const taskResult = amendment
2649
- ? await runAmendmentTaskOperation(absoluteRoot, gh, operation, amendment.tasks.get(operation.task_id), amendmentContext)
2650
- : await runGithubTrackerOperation(absoluteRoot, operation, gh);
2651
- taskResults.push({
2652
- task_id: operation.task_id,
2653
- slice_id: operation.slice_id,
2654
- status: taskResult.status,
2655
- ...(taskResult.issue_number === undefined ? {} : { issue_number: taskResult.issue_number }),
2656
- ...(taskResult.issue_url === undefined ? {} : { issue_url: taskResult.issue_url }),
2657
- ...(taskResult.node_id === undefined ? {} : { node_id: taskResult.node_id }),
2658
- });
2659
- if (!isSuccessfulTrackerStatus(taskResult.status))
2660
- return publicationResultWithPlan(plannedTaskIds, taskResult.status, taskResult.message, parentResult, taskResults);
2661
2829
  }
2662
-
2663
- const finalIntentDrift = publicationIntentDrift(absoluteRoot, prepared.intent_bindings);
2664
- if (finalIntentDrift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", finalIntentDrift, parentResult, taskResults);
2665
- const finalSource = await snapshot(absoluteRoot, gh, "upsert-task");
2666
- if ("contract" in finalSource) return publicationResultWithPlan(plannedTaskIds, finalSource.status, finalSource.message, parentResult, taskResults);
2667
- if (amendment) {
2668
- // Repeat complete observed-membership classification on the final snapshot:
2669
- // an undeclared closed Task carrying this Initiative's markers that becomes
2670
- // observable only after preflight (detached, so the Sub-issue parity check
2671
- // cannot see it) must still fail the publication, not report success.
2672
- const finalClassified = classifyObservedChildren(finalSource, prepared.initiative.initiative_id, amendment.tasks, amendment.historical);
2673
- if (typeof finalClassified === "string") return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", finalClassified, parentResult, taskResults);
2674
- }
2675
- const parent = initiativeLookup(finalSource.issues, finalSource.repository.id, prepared.initiative.initiative_id);
2676
- if (parent.kind !== "found") return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", parent.kind === "ambiguous" ? parent.message : "published Initiative Parent disappeared", parentResult, taskResults);
2677
- const amendedFinalSliceIds = new Set(prepared.initiative.slices.map((slice) => slice.id));
2678
- const parentDrift = publicationIssueDrift(
2679
- parentResult,
2680
- parent.issue,
2681
- amendmentContext ? amendmentContext.parent.title : initiativeIssueTitle(prepared.initiative.initiative_id, prepared.initiative.projection),
2682
- amendmentContext
2683
- ? amendmentContext.parent.body
2684
- : createInitiativeBody(finalSource.repository, prepared.initiative),
2685
- "Initiative Parent",
2686
- );
2687
- if (parentDrift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", parentDrift, parentResult, taskResults);
2688
- if (amendmentContext) {
2689
- // The rewritten Parent must carry each declared historical Slice marker
2690
- // exactly once: generation drops or duplicates would break terminal
2691
- // ownership without failing any other final check.
2692
- for (const line of amendmentContext.historicalSlices) {
2693
- const sliceId = ownershipMarkerValue(line, "slice-id");
2694
- if (!sliceId || countLiteral(parent.issue.body, marker("slice-id", sliceId)) !== 1)
2695
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `regenerated Parent does not carry historical Slice ${sliceId ?? "?"} exactly once`, parentResult, taskResults);
2696
- }
2697
- }
2698
- const resultByTask = new Map(taskResults.map((task) => [task.task_id, task]));
2699
- const expectedNumbers: number[] = [];
2830
+ // Slice identity collisions are deterministic input contract violations (the
2831
+ // batch itself declares two Children with one Slice id), not remote drift.
2832
+ if (typeof approvedFinalParent === "string" && approvedFinalParent.includes("collides with a historical Task Slice"))
2833
+ return publicationResultWithPlan(plannedTaskIds, "permanent_failure", approvedFinalParent);
2834
+ if (typeof approvedFinalParent === "string") return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", approvedFinalParent);
2835
+ const { parent, pendingContent, historicalSlices } = approvedFinalParent;
2836
+ // Deterministic prerequisite completion is validated before any write: a
2837
+ // historical prerequisite already closed without state_reason "completed"
2838
+ // (e.g. not_planned) makes the batch permanently unfulfillable.
2700
2839
  for (const operation of prepared.order) {
2701
- const child = ownedTaskLookup(finalSource.issues, finalSource.repository.id, operation.task_id, operation.initiative_id, operation.slice_id);
2702
- if (child.kind !== "found") return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", child.kind === "ambiguous" ? child.message : `published Task ${operation.task_id} disappeared`, parentResult, taskResults);
2703
- const childResult = resultByTask.get(operation.task_id);
2704
- if (!childResult) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `published Task ${operation.task_id} has no batch result`, parentResult, taskResults);
2705
- const childDrift = publicationIssueDrift(
2706
- childResult,
2707
- child.issue,
2708
- taskIssueTitle(operation, sliceOrdinalFromChecklist(parent.issue.body, operation.slice_id, operation.projection?.slice_ordinal ?? 1)),
2709
- childBody(finalSource.repository, operation, parent.issue),
2710
- `Task ${operation.task_id}`,
2711
- {
2712
- allowTerminalSuffix: amendmentContext !== undefined,
2713
- taskId: operation.task_id,
2714
- expectedIntentHash: operation.intent_hash,
2715
- },
2716
- );
2717
- if (childDrift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", childDrift, parentResult, taskResults);
2718
- const expectedLabels = desiredTaskLabels(operation);
2719
- if (childResult.status !== "already_current" && expectedLabels.some((label) => !child.issue.labels.includes(label)))
2720
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `Task ${operation.task_id} is missing publication labels after publication`, parentResult, taskResults);
2721
- expectedNumbers.push(child.issue.number);
2722
- const ownership = await confirmTerminalOwnership(absoluteRoot, gh, "upsert-task", finalSource, child.issue);
2723
- if (!("owned" in ownership)) return publicationResultWithPlan(plannedTaskIds, ownership.status, ownership.message, parentResult, taskResults);
2724
- const blockers: GithubIssue[] = [];
2725
2840
  for (const blockerId of operation.projection?.blocked_by ?? []) {
2726
- const blocker = taskLookup(finalSource.issues, finalSource.repository.id, blockerId);
2727
- if (blocker.kind !== "found") return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `published blocker ${blockerId} disappeared`, parentResult, taskResults);
2728
- blockers.push(blocker.issue);
2841
+ if (!amendment.historical.has(blockerId)) continue;
2842
+ const blocker = taskLookup(initial.issues, initial.repository.id, blockerId);
2843
+ if (blocker.kind === "found" && (blocker.issue.state !== "closed" || blocker.issue.state_reason !== "completed"))
2844
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `Task ${operation.task_id} depends on stopped historical prerequisite ${blockerId}`);
2729
2845
  }
2730
- const dependencies = await confirmBlockedBy(absoluteRoot, gh, "upsert-task", finalSource.repository, child.issue.number, blockers);
2731
- if (!("complete" in dependencies)) return publicationResultWithPlan(plannedTaskIds, dependencies.status, dependencies.message, parentResult, taskResults);
2732
- if (!dependencies.complete) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `Task ${operation.task_id} has incomplete blocking relations`, parentResult, taskResults);
2733
2846
  }
2734
- const attached = await readSubIssueNumbers(absoluteRoot, gh, "upsert-task", finalSource.repository, parent.issue.number);
2735
- if (!Array.isArray(attached)) return publicationResultWithPlan(plannedTaskIds, attached.status, attached.message, parentResult, taskResults);
2736
- if (amendment) {
2737
- for (const [taskId, binding] of amendment.historical) {
2738
- // Full ownership lookup: repository, protocol, marker uniqueness, and
2739
- // repo-wide Task identity must resolve the exact bound Issue.
2740
- const owned = taskLookup(finalSource.issues, finalSource.repository.id, taskId);
2741
- if (owned.kind !== "found" || owned.issue.number !== binding.issue_number)
2742
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `historical Task ${taskId} ownership failed after publication`, parentResult, taskResults);
2743
- const historical = owned.issue;
2744
- const drift = amendmentBindingDrift(binding, historical, `historical Task ${taskId}`);
2745
- if (drift) return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", drift, parentResult, taskResults);
2746
- if (!attached.includes(historical.number))
2747
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `historical Task ${taskId} lost its native Sub-issue attachment`, parentResult, taskResults);
2748
- // R1 final check: historical Slice identities must not collide with any
2749
- // pending Slice published by this batch — two Children must never share
2750
- // one Slice identity, including historical Children whose Parent lines are
2751
- // carried over rather than re-rendered.
2752
- const historicalSliceId = ownershipMarkerValue(historical.body, "slice-id");
2753
- if (historicalSliceId && amendedFinalSliceIds.has(historicalSliceId))
2754
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `historical Task ${taskId} Slice ${historicalSliceId} collides with a pending Task Slice of the same id`, parentResult, taskResults);
2755
- const snapshotRelations = amendmentContext?.historicalRelations.get(taskId);
2756
- if (snapshotRelations) {
2757
- if (historical.state_reason !== snapshotRelations.state_reason)
2758
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `historical Task ${taskId} terminal state_reason changed during amendment`, parentResult, taskResults);
2759
- const finalBlockedBy = await readBlockedByIds(absoluteRoot, gh, "upsert-task", finalSource.repository, historical.number);
2760
- if (!Array.isArray(finalBlockedBy)) return publicationResultWithPlan(plannedTaskIds, finalBlockedBy.status, finalBlockedBy.message, parentResult, taskResults);
2761
- if (finalBlockedBy.length !== snapshotRelations.blocked_by.length || finalBlockedBy.some((id, index) => id !== snapshotRelations.blocked_by[index]))
2762
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `historical Task ${taskId} native blocked_by relations changed during amendment`, parentResult, taskResults);
2763
- }
2764
- }
2765
- const attachedNumbers = new Set(attached);
2766
- for (const number of attachedNumbers) {
2767
- const issue = finalSource.issues.find((candidate) => candidate.number === number);
2768
- const taskId = issue ? ownershipMarkerValue(issue.body, "task-id") : null;
2769
- if (!taskId || (!amendment.tasks.has(taskId) && !amendment.historical.has(taskId)))
2770
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `Parent Sub-issue #${number} is not part of the declared amendment membership`, parentResult, taskResults);
2771
- }
2772
- for (const operation of prepared.order) {
2773
- for (const blockerId of operation.projection?.blocked_by ?? []) {
2774
- if (amendment.historical.has(blockerId)) {
2775
- const blocker = taskLookup(finalSource.issues, finalSource.repository.id, blockerId);
2776
- if (blocker.kind !== "found" || blocker.issue.state_reason !== "completed")
2777
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `Task ${operation.task_id} depends on stopped historical prerequisite ${blockerId}`, parentResult, taskResults);
2778
- }
2779
- }
2780
- }
2781
- }
2782
- const sortedAttached = [...attached].sort((left, right) => left - right);
2783
- const sortedExpected = [...expectedNumbers, ...(amendment ? [...amendment.historical.values()].map((binding) => binding.issue_number) : [])].sort((left, right) => left - right);
2784
- if (sortedAttached.length !== sortedExpected.length || sortedAttached.some((number, index) => number !== sortedExpected[index]))
2785
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", "Initiative Parent Sub-issues do not match the complete publication batch", parentResult, taskResults);
2786
- const statuses = [parentResult.status, ...taskResults.map((task) => task.status)];
2787
- const status: TrackerStatus = statuses.every((item) => item === "created")
2788
- ? "created"
2789
- : statuses.every((item) => item === "already_current") ? "already_current" : "updated";
2790
- const issueByTask = new Map(taskResults.map((task) => [task.task_id, task.issue_number]));
2791
- if ([...issueByTask.values()].some((number) => number === undefined))
2792
- return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", "published Task result is missing an Issue number", parentResult, taskResults);
2793
- const issueNumber = (taskId: string): number => issueByTask.get(taskId)!;
2794
- const firstTaskId = prepared.order[0].task_id;
2795
- return publicationResultWithPlan(plannedTaskIds, status, "complete Initiative Parent, Children, and dependency graph published", parentResult, taskResults, {
2796
- recommended_first_task_id: firstTaskId,
2797
- recommended_first_issue_number: issueNumber(firstTaskId),
2798
- order: prepared.order.map((operation) => operation.task_id),
2799
- issue_order: prepared.order.map((operation) => issueNumber(operation.task_id)),
2800
- parallel_groups: prepared.parallel_groups,
2801
- parallel_issue_groups: prepared.parallel_groups.map((group) => group.map(issueNumber)),
2802
- });
2847
+ // The approved baseline is the only accepted starting point. Every fact here
2848
+ // comes from the start listing, before any write.
2849
+ const parentBaseline = amendment.parent.body !== initialParent.issue.body || amendment.parent.title !== initialParent.issue.title;
2850
+ const parentApproved = initialParent.issue.title === parent.title && initialParent.issue.body === parent.body;
2851
+ if (parentBaseline && !parentApproved)
2852
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", "amendment Parent changed since the approved amendment baseline");
2853
+ if (amendment.parent.issue_number !== initialParent.issue.number)
2854
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", `amendment Parent is bound to Issue #${amendment.parent.issue_number} but observed Issue #${initialParent.issue.number}`);
2855
+ if (initialParent.issue.state !== "open")
2856
+ return publicationResultWithPlan(plannedTaskIds, "ambiguous_remote_state", "an amendment requires the Initiative Parent to remain open");
2857
+ const amendmentContextValue: AmendmentExecutionContext = {
2858
+ pendingContent,
2859
+ parent,
2860
+ historicalSlices,
2861
+ };
2862
+ return publishAmendmentDirect(absoluteRoot, gh, prepared, initial, amendment, amendmentContextValue);
2803
2863
  }
2804
2864
 
2865
+
2805
2866
  function isSuccessfulTrackerStatus(status: TrackerStatus): boolean {
2806
2867
  return status === "created" || status === "updated" || status === "already_current";
2807
2868
  }