immune-brain 3.6.5 → 3.6.6

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 (31) hide show
  1. package/README.md +45 -0
  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-work.ts +31 -2
  5. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +38 -10
  6. package/plugins/immune-brain/dist/BASELINE.md +48 -15
  7. package/plugins/immune-brain/dist/claude/mcp-server.mjs +20 -10
  8. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  9. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  10. package/plugins/immune-brain/dist/imm-agent-doc-maintain.md +9 -1
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +49 -35
  12. package/plugins/immune-brain/dist/imm-doc-prune.md +7 -1
  13. package/plugins/immune-brain/dist/imm-loop.md +23 -11
  14. package/plugins/immune-brain/dist/imm-planner.md +65 -28
  15. package/plugins/immune-brain/dist/imm-pr-fix.md +6 -2
  16. package/plugins/immune-brain/dist/role-prompts/executor.md +18 -10
  17. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +5 -2
  18. package/plugins/immune-brain/runtime/assurance/verification.ts +13 -2
  19. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1018 -17
  20. package/plugins/immune-brain/runtime/kernel/canary_application.ts +7 -1
  21. package/plugins/immune-brain/runtime/kernel/reducer.ts +3 -1
  22. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  23. package/plugins/immune-brain/runtime/prompts/executor.md +18 -10
  24. package/plugins/immune-brain/runtime/prompts/pr-fix.md +5 -2
  25. package/plugins/immune-brain/skills/BASELINE.md +48 -15
  26. package/plugins/immune-brain/skills/imm-agent-doc-maintain/SKILL.md +20 -4
  27. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +24 -64
  28. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +18 -3
  29. package/plugins/immune-brain/skills/imm-loop/SKILL.md +20 -6
  30. package/plugins/immune-brain/skills/imm-planner/SKILL.md +35 -8
  31. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +17 -3
@@ -60,6 +60,35 @@ export interface InitiativePublicationInput {
60
60
  acceptance: Array<{ id: string; summary: string }>;
61
61
  projection?: TaskProjection;
62
62
  }>;
63
+ /**
64
+ * Explicit amendment input: binds the caller's desired pending frontier to
65
+ * the exact observed remote content it was approved against, and declares
66
+ * read-only historical Child identities. When omitted, publication keeps
67
+ * today's strict default semantics (create once, never rewrite).
68
+ */
69
+ amendment?: {
70
+ parent: InitiativeAmendmentBinding;
71
+ tasks: Array<InitiativeTaskAmendment>;
72
+ historical: InitiativeHistoricalChild[];
73
+ };
74
+ }
75
+
76
+ /** Identity plus expected remote title/body/state for one bound Issue. */
77
+ export interface InitiativeAmendmentBinding {
78
+ issue_number: number;
79
+ title: string;
80
+ body: string;
81
+ state: "open" | "closed";
82
+ }
83
+
84
+ export interface InitiativeTaskAmendment {
85
+ task_id: string;
86
+ binding?: InitiativeAmendmentBinding;
87
+ }
88
+
89
+ export interface InitiativeHistoricalChild {
90
+ task_id: string;
91
+ binding: InitiativeAmendmentBinding;
63
92
  }
64
93
 
65
94
  export interface GithubInitiativePublicationResult {
@@ -211,6 +240,12 @@ function terminalEvent(value: unknown): string {
211
240
  if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,500}$/.test(value))
212
241
  throw new Error("terminal_event_id must be a bounded opaque Kernel event id");
213
242
  if (/(?:gh[pousr]_|github_pat_)/i.test(value)) throw new Error("terminal_event_id must not contain a token-like value");
243
+ // The suffix parser reports failures through the string sentinels "multiple"
244
+ // and "malformed"; an event id equal to either sentinel would make valid
245
+ // terminal evidence indistinguishable from a parser failure. Reject at the
246
+ // validation boundary so no published marker can ever collide.
247
+ if (value === "multiple" || value === "malformed")
248
+ throw new Error(`terminal_event_id must not be the reserved parser sentinel: ${value}`);
214
249
  return value;
215
250
  }
216
251
 
@@ -680,14 +715,18 @@ function bodyLimitFailure(operation: TrackerOperation["op"], body: string, reser
680
715
  : result(operation, "permanent_failure", "rendered GitHub Issue body exceeds 65,536 UTF-8 bytes");
681
716
  }
682
717
 
683
- function createInitiativeBody(repository: RepositoryInfo, operation: Extract<TrackerOperation, { op: "create-initiative" }>): string {
718
+ function createInitiativeBody(
719
+ repository: RepositoryInfo,
720
+ operation: Extract<TrackerOperation, { op: "create-initiative" }>,
721
+ historicalSlices: string[] = [],
722
+ ): string {
684
723
  const projection = operation.projection ?? {};
685
724
  return `${[
686
725
  PROTOCOL_MARKER,
687
726
  KIND_INITIATIVE_MARKER,
688
727
  marker("repo-id", repository.id),
689
728
  marker("initiative-id", operation.initiative_id),
690
- ].join("\n")}\n\n# ${titleText(projection.result ?? operation.goal)}\n\nOpt-in, non-authoritative Immune-Brain Initiative planning carrier. Kernel TaskIntent, TaskRecord, and Assurance remain the execution authority.\n\n## 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 === 0 ? "No Slices recorded yet." : 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## Authority boundary\n\nThis Issue is outbound visibility only. GitHub state never starts, authorizes, reprioritizes, or settles work. Native Sub-issues identify published Tasks; their state is only an observation.\n`;
729
+ ].join("\n")}\n\n# ${titleText(projection.result ?? operation.goal)}\n\nOpt-in, non-authoritative Immune-Brain Initiative planning carrier. Kernel TaskIntent, TaskRecord, and Assurance remain the execution authority.\n\n## 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## Authority boundary\n\nThis Issue is outbound visibility only. GitHub state never starts, authorizes, reprioritizes, or settles work. Native Sub-issues identify published Tasks; their state is only an observation.\n`;
691
730
  }
692
731
 
693
732
  async function createInitiative(
@@ -749,11 +788,183 @@ function childBody(
749
788
  ].join("\n")}\n\n# ${titleText(projection.result ?? operation.goal)}\n\nOpt-in, non-authoritative Immune-Brain Task Issue. Kernel TaskIntent, TaskRecord, and Assurance remain the execution authority.\n\n## Parent\n\n| Initiative | \`${operation.initiative_id}\` |\n| Parent Issue | [#${parent.number}](${parent.url}) |\n| Slice | \`${operation.slice_id}\` |\n| Risk | \`${operation.risk}\` |\n\n## What to build\n\n${publicText(projection.result ?? operation.goal, "projection.result")}\n\n## Current behavior\n\n${publicText(projection.current_behavior ?? "The current behavior is defined by the repository's existing contract.", "projection.current_behavior")}\n\n## Desired behavior\n\n${publicText(projection.desired_behavior ?? operation.goal, "projection.desired_behavior")}\n\n## Key interfaces\n\n${listText(projection.key_interfaces, "- Canonical TaskIntent acceptance and Kernel lifecycle remain authoritative.")}\n\n## Acceptance criteria\n\n${acceptance}\n\n## Verification\n\n${publicText(projection.verification ?? "Run the focused acceptance verification declared by the TaskIntent.", "projection.verification")}\n\n## Blocked by\n\n${projection.blocked_by?.length ? projection.blocked_by.map((id) => `- \`${identifier(id, "blocked_by task_id")}\``).join("\n") : "None"}\n\n## Out of scope\n\n${listText(projection.out_of_scope, "- Scope not declared by the validated TaskIntent.")}\n\n## Agent handoff\n\n${publicText(projection.agent_handoff ?? "Implement only the bounded TaskIntent result and run the focused checks. Do not widen scope or treat GitHub as authorization.", "projection.agent_handoff")}\n\n## Lifecycle\n\n- **Open** means this Task still needs attention; it does not mean the Task is authorized or executing.\n- Only a fresh claimless terminal projection can close this Issue: \`done\` becomes **Completed**, and \`stopped\` becomes **Not planned**.\n\n## Authority boundary\n\nThis Issue is outbound visibility only. GitHub state never changes TaskIntent, TaskRecord, QA, Review, authorization, or Kernel settlement. Internal role prompts, tool policies, review gates, model reservations, and prompt digests are not part of this external handoff.\n`;
750
789
  }
751
790
 
791
+ /** Derived approved-final content and baseline-derived historical evidence for an amendment. */
792
+ interface AmendmentExecutionContext {
793
+ /** Approved final title/body per pending Task id. */
794
+ pendingContent: Map<string, { title: string; body: string }>;
795
+ /** Approved final Parent title/body. */
796
+ parent: { title: string; body: string };
797
+ /** Approved bound Parent issue number. */
798
+ parentIssueNumber: number;
799
+ /** Historical Slice lines derived from the approved Parent baseline (exact bytes). */
800
+ historicalSlices: string[];
801
+ /** Historical dependency database IDs and terminal state_reason snapshotted before any write. */
802
+ historicalRelations: Map<string, { blocked_by: number[]; state_reason: string | null }>;
803
+ }
804
+
805
+ /**
806
+ * Derive the approved-final content for every amendment write from the current
807
+ * TaskIntents and the approved baseline. Historical Slice lines are extracted
808
+ * from the Parent binding's baseline body so the final expectation is fixed
809
+ * before any mutation, never derived from post-write remote state.
810
+ */
811
+ function approvedAmendmentContent(
812
+ root: string,
813
+ repository: RepositoryInfo,
814
+ parentIssue: GithubIssue,
815
+ prepared: ReturnType<typeof preflightPublication>,
816
+ amendment: ReturnType<typeof validateAmendment>,
817
+ ): AmendmentExecutionContext | string {
818
+ const parent = validateOperation({
819
+ op: "create-initiative",
820
+ initiative_id: prepared.initiative.initiative_id,
821
+ goal: prepared.initiative.goal,
822
+ projection: prepared.initiative.projection,
823
+ slices: prepared.order.map((operation) => ({
824
+ id: operation.slice_id,
825
+ goal: operation.goal,
826
+ result: operation.projection?.result,
827
+ blocked_by: operation.projection?.blocked_by,
828
+ })),
829
+ }) as Extract<TrackerOperation, { op: "create-initiative" }>;
830
+ const historicalSliceIds: Set<string> = new Set();
831
+ for (const child of amendment.historical.values()) {
832
+ const sliceId = ownershipMarkerValue(child.body, "slice-id");
833
+ if (!sliceId) throw new Error("historical amendment binding must carry a slice-id marker");
834
+ if (historicalSliceIds.has(sliceId)) throw new Error(`duplicate historical slice-id: ${sliceId}`);
835
+ historicalSliceIds.add(sliceId);
836
+ }
837
+ const amendedSliceIds = new Set(parent.slices.map((slice) => slice.id));
838
+ for (const sliceId of amendedSliceIds) {
839
+ if (historicalSliceIds.has(sliceId))
840
+ return `pending Task Slice ${sliceId} collides with a historical Task Slice of the same id; Slice identities must be unique across historical and pending Children`;
841
+ }
842
+ const boundSliceIds = new Set(
843
+ parent.slices
844
+ .filter((slice) => prepared.order.some((operation) => operation.slice_id === slice.id && amendment.tasks.get(operation.task_id) !== undefined))
845
+ .map((slice) => slice.id),
846
+ );
847
+ const historicalSlices = baselineHistoricalSlices(amendment.parent.body, amendedSliceIds, boundSliceIds);
848
+ if (typeof historicalSlices === "string") return historicalSlices;
849
+ const pendingContent = new Map<string, { title: string; body: string }>();
850
+ const sourceStub = {
851
+ repository,
852
+ issues: [parentIssue],
853
+ };
854
+ void sourceStub;
855
+ for (const operation of prepared.order) {
856
+ const body = childBody(repository, operation, parentIssue);
857
+ const oversized = bodyLimitFailure("upsert-task", body, MAX_TERMINAL_SUFFIX_BYTES);
858
+ if (oversized) return `${oversized.status}: ${oversized.message}`;
859
+ pendingContent.set(operation.task_id, {
860
+ title: issueTitle(`${operation.initiative_id}/${operation.slice_id}`, operation.projection?.result ?? operation.goal),
861
+ body,
862
+ });
863
+ }
864
+ const parentBody = createInitiativeBody(sourceStub.repository, parent, historicalSlices);
865
+ const oversizedParent = bodyLimitFailure("create-initiative", parentBody);
866
+ if (oversizedParent) return `${oversizedParent.status}: ${oversizedParent.message}`;
867
+ return {
868
+ pendingContent,
869
+ parent: {
870
+ title: issueTitle(parent.initiative_id, parent.projection?.result ?? parent.goal),
871
+ body: parentBody,
872
+ },
873
+ parentIssueNumber: parentIssue.number,
874
+ historicalSlices,
875
+ historicalRelations: new Map(),
876
+ };
877
+ }
878
+
879
+ /**
880
+ * Extract historical Slice lines from the approved Parent baseline body.
881
+ * Accepts any exactly-once Slice marker representation and returns the exact
882
+ * baseline line bytes; fails closed on malformed Slice entries. Only Slices of
883
+ * bound pending Tasks must already exist in the baseline; unbound new Tasks
884
+ * contribute fresh Slice lines to the approved final Parent body.
885
+ */
886
+ function baselineHistoricalSlices(parentBaselineBody: string, amendedSliceIds: Set<string>, boundSliceIds: Set<string>): string[] | string {
887
+ const lines: string[] = [];
888
+ for (const match of parentBaselineBody.matchAll(/^.*?<!-- immune-brain:slice-id=([A-Za-z0-9._:-]+) -->.*$/gm)) {
889
+ const sliceId = match[1];
890
+ const line = match[0];
891
+ // A baseline line must carry exactly one Slice marker: a line mixing a
892
+ // historical marker with a pending marker, or duplicating a marker, is
893
+ // ambiguous remote state — greedy whole-line matching would otherwise
894
+ // silently drop one side's marker when the Parent is rewritten.
895
+ const markersOnLine = [...line.matchAll(/<!-- immune-brain:slice-id=([A-Za-z0-9._:-]+) -->/g)];
896
+ if (markersOnLine.length !== 1)
897
+ return `historical Slice ${sliceId} shares a baseline line with another Slice marker; each Slice marker must sit on its own line`;
898
+ if (amendedSliceIds.has(sliceId)) {
899
+ // The pending batch replaces this Slice line: the line belongs to the
900
+ // amendment's own pending or historical Children (membership and slice
901
+ // uniqueness are validated elsewhere), so it must not be carried over.
902
+ continue;
903
+ }
904
+ if (countLiteral(line, marker("slice-id", sliceId)) !== 1)
905
+ return `historical Slice ${sliceId} has missing or duplicate Slice markers in the approved Parent baseline`;
906
+ if (!ownershipMarkerValue(line, "slice-id"))
907
+ return `historical Slice ${sliceId} has a malformed Slice marker in the approved Parent baseline`;
908
+ lines.push(line);
909
+ }
910
+ for (const sliceId of boundSliceIds) {
911
+ if (countLiteral(parentBaselineBody, marker("slice-id", sliceId)) !== 1)
912
+ return `approved Parent baseline does not carry exactly one Slice marker for bound pending Slice ${sliceId}`;
913
+ }
914
+ return lines;
915
+ }
916
+
917
+ /** Edit an open Initiative Parent's title/body to the approved amendment content. */
918
+ async function amendInitiativeParent(
919
+ root: string,
920
+ gh: GhTransport,
921
+ operation: Extract<TrackerOperation, { op: "create-initiative" }>,
922
+ source: RepositorySnapshot,
923
+ binding: InitiativeAmendmentBinding,
924
+ context: AmendmentExecutionContext,
925
+ ): Promise<GithubTrackerResult> {
926
+ const lookup = (issues: GithubIssue[]) => initiativeLookup(issues, source.repository.id, operation.initiative_id);
927
+ const body = context.parent.body;
928
+ const oversized = bodyLimitFailure(operation.op, body);
929
+ if (oversized) return oversized;
930
+ const title = context.parent.title;
931
+ // Re-read the Parent immediately before writing: earlier topology and
932
+ // historical-dependency reads may have raced a concurrent user edit.
933
+ const reread = await snapshot(root, gh, operation.op);
934
+ if ("contract" in reread) return reread;
935
+ const found = lookup(reread.issues);
936
+ if (found.kind !== "found")
937
+ return result(operation.op, "permanent_failure", "an amendment requires the Initiative Parent to already exist");
938
+ if (found.issue.state !== "open")
939
+ return result(operation.op, "ambiguous_remote_state", "an amendment requires the Initiative Parent to remain open", found.issue);
940
+ if (binding.issue_number !== found.issue.number)
941
+ return result(operation.op, "ambiguous_remote_state", `amendment Parent is bound to Issue #${binding.issue_number} but observed Issue #${found.issue.number}`, found.issue);
942
+ if (found.issue.body === body && found.issue.title === title)
943
+ return result(operation.op, "already_current", "Initiative Issue already carries the requested amended content", found.issue);
944
+ if (found.issue.body !== binding.body || found.issue.title !== binding.title)
945
+ return result(operation.op, "ambiguous_remote_state", "Initiative Parent changed since the approved amendment baseline", found.issue);
946
+ const edited = await gh.run([
947
+ "issue", "edit", String(found.issue.number), "--repo", source.repository.name_with_owner,
948
+ "--title", title,
949
+ "--body-file", "-",
950
+ ], { cwd: root, stdin: body });
951
+ if (edited.exit_code !== 0 || edited.output_exceeded) return ghFailure(operation.op, edited, "Initiative amendment edit failed");
952
+ const refreshed = await snapshot(root, gh, operation.op);
953
+ if ("contract" in refreshed) return refreshed;
954
+ const confirmed = lookup(refreshed.issues);
955
+ if (confirmed.kind !== "found") return result(operation.op, "ambiguous_remote_state", "Initiative Parent became ambiguous after amendment", found.issue);
956
+ return confirmed.issue.body === body && confirmed.issue.title === title
957
+ ? result(operation.op, "updated", "Initiative Issue updated with approved amendment content", confirmed.issue)
958
+ : result(operation.op, "retryable_failure", "Initiative amendment did not converge to the requested title and body", confirmed.issue);
959
+ }
960
+
752
961
  async function upsertTask(
753
962
  root: string,
754
963
  gh: GhTransport,
755
964
  operation: Extract<TrackerOperation, { op: "upsert-task" }>,
756
965
  source: RepositorySnapshot,
966
+ pendingBinding: InitiativeAmendmentBinding | undefined | null = null,
967
+ amendmentContext: AmendmentExecutionContext | undefined = undefined,
757
968
  ): Promise<GithubTrackerResult> {
758
969
  const parent = initiativeLookup(source.issues, source.repository.id, operation.initiative_id);
759
970
  if (parent.kind === "ambiguous") return result(operation.op, "ambiguous_remote_state", parent.message);
@@ -769,6 +980,8 @@ async function upsertTask(
769
980
  const lookup = (issues: GithubIssue[]) => taskLookup(issues, source.repository.id, operation.task_id);
770
981
  const found = lookup(source.issues);
771
982
  if (found.kind === "ambiguous") return result(operation.op, "ambiguous_remote_state", found.message);
983
+ if (found.kind === "missing" && pendingBinding !== null && pendingBinding !== undefined)
984
+ 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);
772
985
  const blockerIds = operation.projection?.blocked_by ?? [];
773
986
  const blockers: GithubIssue[] = [];
774
987
  for (const blockerId of blockerIds) {
@@ -788,6 +1001,40 @@ async function upsertTask(
788
1001
  let child: GithubIssue;
789
1002
  let createdChild = false;
790
1003
  if (found.kind === "missing") {
1004
+ // Pre-create re-read (amendment path): a concurrent writer may have already
1005
+ // created this unbound Task between the initial snapshot and our create — the
1006
+ // same resumable-creation contract applies before we issue any write, so we
1007
+ // fail closed on divergent content instead of issuing a duplicate create.
1008
+ if (amendmentContext !== undefined) {
1009
+ const reRead = await snapshot(root, gh, operation.op);
1010
+ if ("contract" in reRead) return reRead;
1011
+ // The re-read snapshot must also still hold the amendment's Parent exactly
1012
+ // as approved: a Parent closed or edited after the initial snapshot fails
1013
+ // closed here, before any Child create (avoiding an avoidable remote write
1014
+ // that the post-create attachment guard would otherwise reject).
1015
+ const reReadParent = initiativeLookup(reRead.issues, reRead.repository.id, operation.initiative_id);
1016
+ if (reReadParent.kind !== "found")
1017
+ return result(operation.op, "ambiguous_remote_state", reReadParent.kind === "ambiguous" ? reReadParent.message : "amendment Parent is not observable before creating a new Child", parent.issue);
1018
+ if (reReadParent.issue.number !== amendmentContext.parentIssueNumber)
1019
+ 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);
1020
+ if (reReadParent.issue.state !== "open")
1021
+ return result(operation.op, "ambiguous_remote_state", "amendment Parent is no longer open before creating a new Child", reReadParent.issue);
1022
+ if (amendmentContext.parent.title !== reReadParent.issue.title || amendmentContext.parent.body !== reReadParent.issue.body)
1023
+ return result(operation.op, "ambiguous_remote_state", "amendment Parent content changed before creating a new Child", reReadParent.issue);
1024
+ if (sliceCount(reReadParent.issue.body, operation.slice_id) !== 1)
1025
+ 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);
1026
+ const raced = lookup(reRead.issues);
1027
+ if (raced.kind === "ambiguous") return result(operation.op, "ambiguous_remote_state", raced.message);
1028
+ if (raced.kind === "found") {
1029
+ const approved = amendmentContext.pendingContent.get(operation.task_id);
1030
+ const resumable = approved !== undefined
1031
+ && raced.issue.state === "open"
1032
+ && carriesApprovedContent(raced.issue.title, raced.issue.body, approved);
1033
+ if (!resumable)
1034
+ 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);
1035
+ return updatePendingChild(root, gh, reRead, operation, raced.issue, undefined, blockers, approved, amendmentContext);
1036
+ }
1037
+ }
791
1038
  const mutation = await gh.run([
792
1039
  "issue", "create", "--repo", source.repository.name_with_owner,
793
1040
  "--title", title,
@@ -804,14 +1051,61 @@ async function upsertTask(
804
1051
  }
805
1052
  if (created.issue.body !== body || created.issue.title !== title)
806
1053
  return result(operation.op, "retryable_failure", "Task Issue did not converge to the requested title and body", created.issue);
1054
+ if (amendmentContext !== undefined) {
1055
+ // Amendment unbound new Child: creation converged, but the Child must
1056
+ // still be open — a concurrent close between create and read fails closed
1057
+ // instead of attaching and wiring dependencies onto closed work.
1058
+ if (created.issue.state !== "open")
1059
+ return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} (Issue #${created.issue.number}) is not open after creation`, created.issue);
1060
+ // Route through the amendment convergence path so attachment (R4
1061
+ // re-attach) and every dependency write carry the same pre-write
1062
+ // revalidation as bound pending Children.
1063
+ return updatePendingChild(root, gh, source, operation, created.issue, undefined, blockers, amendmentContext.pendingContent.get(operation.task_id), amendmentContext);
1064
+ }
807
1065
  child = created.issue;
808
1066
  createdChild = true;
809
1067
  } else {
810
- if (found.issue.body !== body || found.issue.title !== title)
811
- 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);
812
1068
  const owned = ownedTaskLookup(source.issues, source.repository.id, operation.task_id, operation.initiative_id, operation.slice_id);
813
1069
  if (owned.kind !== "found") return result(operation.op, "ambiguous_remote_state", owned.kind === "ambiguous" ? owned.message : "Task Issue ownership changed during publication", found.issue);
814
1070
  child = owned.issue;
1071
+ if (pendingBinding !== null) {
1072
+ if (child.state !== "open")
1073
+ return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is closed and cannot be amended as pending work`, found.issue);
1074
+ // The bound pending Child must still be the Issue its binding pins (review-5):
1075
+ // a replacement Issue that took over the markers fails closed instead of
1076
+ // being edited or recreated.
1077
+ if (pendingBinding !== undefined && pendingBinding.issue_number !== child.number)
1078
+ 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);
1079
+ const approvedFinal = amendmentContext?.pendingContent.get(operation.task_id);
1080
+ // A Child left open with a validated terminal suffix (failed terminal close)
1081
+ // counts as approved-final when its suffix-free bytes match the approved bytes.
1082
+ const isFinal = approvedFinal !== undefined && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
1083
+ if (!isFinal) {
1084
+ // Suffix-aware baseline equality: a Child whose remote bytes carry a validated
1085
+ // terminal suffix over the binding baseline (failed terminal close) still
1086
+ // matches its binding, because the suffix is terminal evidence, not drift.
1087
+ const baselineMatches = pendingBinding !== undefined
1088
+ && pendingBinding.title === found.issue.title
1089
+ && carriesApprovedContent(found.issue.title, found.issue.body, pendingBinding);
1090
+ if (!baselineMatches)
1091
+ return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} changed since the approved amendment baseline`, found.issue);
1092
+ }
1093
+ return updatePendingChild(root, gh, source, operation, child, pendingBinding?.issue_number, blockers, approvedFinal, amendmentContext);
1094
+ }
1095
+ if (amendmentContext !== undefined) {
1096
+ // Amendment unbound Child observed before any write: it is only valid
1097
+ // as the exact approved-final creation of this same batch (resumable
1098
+ // creation); anything else fails closed.
1099
+ const approvedFinal = amendmentContext.pendingContent.get(operation.task_id);
1100
+ const resumable = approvedFinal !== undefined
1101
+ && child.state === "open"
1102
+ && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
1103
+ if (!resumable)
1104
+ 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);
1105
+ return updatePendingChild(root, gh, source, operation, child, undefined, blockers, approvedFinal, amendmentContext);
1106
+ }
1107
+ if (found.issue.body !== body || found.issue.title !== title)
1108
+ 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);
815
1109
  }
816
1110
  const attachment = await confirmAttachment(root, gh, operation.op, source.repository, parent.issue.number, child.number);
817
1111
  if (!("attached" in attachment)) return attachment;
@@ -1051,6 +1345,20 @@ function validateOperation(operation: TrackerOperation): TrackerOperation {
1051
1345
  };
1052
1346
  }
1053
1347
 
1348
+ /** Run one amendment pending Task: create unbound new Children, amend bound drift. */
1349
+ async function runAmendmentTaskOperation(
1350
+ root: string,
1351
+ gh: GhTransport,
1352
+ operation: Extract<TrackerOperation, { op: "upsert-task" }>,
1353
+ pendingBinding: InitiativeAmendmentBinding | undefined | null,
1354
+ context: AmendmentExecutionContext | undefined,
1355
+ ): Promise<GithubTrackerResult> {
1356
+ const absoluteRoot = resolve(root);
1357
+ const source = await snapshot(absoluteRoot, gh, operation.op);
1358
+ if ("contract" in source) return source;
1359
+ return upsertTask(absoluteRoot, gh, operation, source, pendingBinding, context);
1360
+ }
1361
+
1054
1362
  export async function runGithubTrackerOperation(
1055
1363
  root: string,
1056
1364
  input: TrackerOperation,
@@ -1100,12 +1408,12 @@ interface PreparedPublicationTask {
1100
1408
  intent_content_hash: string;
1101
1409
  }
1102
1410
 
1103
- function publicationPlan(operations: Array<Extract<TrackerOperation, { op: "upsert-task" }>>): {
1411
+ function publicationPlan(operations: Array<Extract<TrackerOperation, { op: "upsert-task" }>>, satisfiedPrerequisites: Set<string> = new Set()): {
1104
1412
  order: Array<Extract<TrackerOperation, { op: "upsert-task" }>>;
1105
1413
  parallel_groups: string[][];
1106
1414
  } {
1107
1415
  const remaining = new Set(operations.map((operation) => operation.task_id));
1108
- const done = new Set<string>();
1416
+ const done = new Set<string>(satisfiedPrerequisites);
1109
1417
  const order: Array<Extract<TrackerOperation, { op: "upsert-task" }>> = [];
1110
1418
  const parallelGroups: string[][] = [];
1111
1419
  while (remaining.size) {
@@ -1127,9 +1435,10 @@ function preflightPublication(root: string, input: InitiativePublicationInput):
1127
1435
  order: Array<Extract<TrackerOperation, { op: "upsert-task" }>>;
1128
1436
  parallel_groups: string[][];
1129
1437
  intent_bindings: Map<string, Pick<PreparedPublicationTask, "intent_path" | "intent_content_hash">>;
1438
+ foreign_dependers: Map<string, string[]>;
1130
1439
  } {
1131
1440
  if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("publication must be an object");
1132
- if (!Array.isArray(input.tasks) || input.tasks.length < 2)
1441
+ if (!Array.isArray(input.tasks) || input.tasks.length < (input.amendment ? 1 : 2))
1133
1442
  throw new Error("a complete Initiative publication requires at least two Tasks");
1134
1443
  if (!input.projection || typeof input.projection !== "object" || Array.isArray(input.projection))
1135
1444
  throw new Error("publication projection must be an object");
@@ -1142,6 +1451,11 @@ function preflightPublication(root: string, input: InitiativePublicationInput):
1142
1451
  if (typeof task.intent !== "string") throw new Error(`tasks[${index}].intent must be a string`);
1143
1452
  return taskPublication(root, input.initiative_id, task.slice_id, task.intent, task.acceptance, task.projection);
1144
1453
  });
1454
+ const historicalIds = new Set<string>();
1455
+ if (input.amendment) {
1456
+ if (!Array.isArray(input.amendment.historical)) throw new Error("amendment.historical must be an array");
1457
+ for (const child of input.amendment.historical) historicalIds.add(identifier(child.task_id, "amendment.historical task_id"));
1458
+ }
1145
1459
  const operations = publications.map((publication) => publication.operation);
1146
1460
  const taskIds = new Set<string>();
1147
1461
  const sliceIds = new Set<string>();
@@ -1151,12 +1465,20 @@ function preflightPublication(root: string, input: InitiativePublicationInput):
1151
1465
  taskIds.add(operation.task_id);
1152
1466
  sliceIds.add(operation.slice_id);
1153
1467
  }
1468
+ const foreignDependers = new Map<string, string[]>();
1154
1469
  for (const operation of operations) {
1155
1470
  for (const blocker of operation.projection?.blocked_by ?? []) {
1156
- if (!taskIds.has(blocker)) throw new Error(`Task ${operation.task_id} depends on ${blocker}, which is outside the complete Initiative batch`);
1471
+ if (!taskIds.has(blocker) && !historicalIds.has(blocker)) {
1472
+ if (input.amendment === undefined)
1473
+ throw new Error(`Task ${operation.task_id} depends on ${blocker}, which is outside the complete Initiative batch`);
1474
+ foreignDependers.set(blocker, [...(foreignDependers.get(blocker) ?? []), operation.task_id]);
1475
+ }
1157
1476
  }
1158
1477
  }
1159
- const plan = publicationPlan(operations);
1478
+ const foreignIds = new Set(foreignDependers.keys());
1479
+ const plan = input.amendment === undefined
1480
+ ? publicationPlan(operations)
1481
+ : publicationPlan(operations, new Set([...historicalIds, ...foreignIds]));
1160
1482
  const initiative = validateOperation({
1161
1483
  op: "create-initiative",
1162
1484
  initiative_id: input.initiative_id,
@@ -1173,7 +1495,7 @@ function preflightPublication(root: string, input: InitiativePublicationInput):
1173
1495
  intent_path: publication.intent_path,
1174
1496
  intent_content_hash: publication.intent_content_hash,
1175
1497
  }]));
1176
- return { initiative, ...plan, intent_bindings: intentBindings };
1498
+ return { initiative, ...plan, intent_bindings: intentBindings, foreign_dependers: foreignDependers };
1177
1499
  }
1178
1500
 
1179
1501
  function publicationIntentDrift(
@@ -1199,14 +1521,524 @@ function publicationIssueDrift(expected: {
1199
1521
  issue_number?: number;
1200
1522
  issue_url?: string;
1201
1523
  node_id?: string;
1202
- }, actual: GithubIssue, title: string, body: string, label: string): string | null {
1524
+ }, actual: GithubIssue, title: string, body: string, label: string, options?: { allowTerminalSuffix?: boolean }): string | null {
1203
1525
  if (expected.issue_number !== actual.number || expected.issue_url !== actual.url || expected.node_id !== String(actual.id))
1204
1526
  return `${label} identity changed during Initiative publication`;
1205
1527
  if (actual.state !== "open") return `${label} is no longer open`;
1206
- if (actual.title !== title || actual.body !== body) return `${label} content changed during Initiative publication`;
1528
+ if (actual.title !== title || actual.body !== body) {
1529
+ // Only a pending amendment Child left open with a validated terminal suffix
1530
+ // (failed terminal close) matches its suffix-free expected content. The
1531
+ // Parent and the strict non-amendment path have no terminal-suffix
1532
+ // lifecycle: byte-exact comparison, any extra suffix is real drift.
1533
+ if (options?.allowTerminalSuffix && carriesApprovedContent(actual.title, actual.body, { title, body })) return null;
1534
+ return `${label} content changed during Initiative publication`;
1535
+ }
1207
1536
  return null;
1208
1537
  }
1209
1538
 
1539
+ /** One bound Issue's expected remote identity and content. */
1540
+ function validateAmendmentBinding(binding: unknown, label: string): InitiativeAmendmentBinding {
1541
+ if (!binding || typeof binding !== "object" || Array.isArray(binding)) throw new Error(`${label} must be an object`);
1542
+ const raw = binding as Record<string, unknown>;
1543
+ if (!Number.isSafeInteger(raw.issue_number) || (raw.issue_number as number) < 1)
1544
+ throw new Error(`${label}.issue_number must be a positive integer`);
1545
+ if (raw.state !== "open" && raw.state !== "closed") throw new Error(`${label}.state must be "open" or "closed"`);
1546
+ if (typeof raw.title !== "string" || !raw.title.trim()) throw new Error(`${label}.title must be a non-empty string`);
1547
+ if (typeof raw.body !== "string") throw new Error(`${label}.body must be a string`);
1548
+ if (Buffer.byteLength(raw.body, "utf8") > GITHUB_ISSUE_BODY_LIMIT) throw new Error(`${label}.body exceeds 65,536 UTF-8 bytes`);
1549
+ return { issue_number: raw.issue_number as number, title: raw.title, body: raw.body, state: raw.state };
1550
+ }
1551
+
1552
+ function validateAmendment(input: InitiativePublicationInput): {
1553
+ parent: InitiativeAmendmentBinding;
1554
+ tasks: Map<string, InitiativeAmendmentBinding | undefined>;
1555
+ historical: Map<string, InitiativeAmendmentBinding>;
1556
+ } {
1557
+ const amendment = input.amendment!;
1558
+ const parent = validateAmendmentBinding(amendment.parent, "amendment.parent");
1559
+ if (parent.state !== "open") throw new Error("amendment.parent must be an open Issue");
1560
+ const tasks = new Map<string, InitiativeAmendmentBinding | undefined>();
1561
+ const historical = new Map<string, InitiativeAmendmentBinding>();
1562
+ if (!Array.isArray(amendment.tasks)) throw new Error("amendment.tasks must be an array");
1563
+ for (const [index, task] of amendment.tasks.entries()) {
1564
+ if (!task || typeof task !== "object" || Array.isArray(task)) throw new Error(`amendment.tasks[${index}] must be an object`);
1565
+ const taskId = identifier((task as { task_id: unknown }).task_id, `amendment.tasks[${index}].task_id`);
1566
+ if (tasks.has(taskId) || historical.has(taskId)) throw new Error(`duplicate amendment Task id: ${taskId}`);
1567
+ const binding = (task as { binding: unknown }).binding as InitiativeAmendmentBinding | undefined;
1568
+ if (binding !== undefined && binding.state !== "open")
1569
+ throw new Error(`amendment.tasks[${index}].binding.state must be "open" for pending Children`);
1570
+ tasks.set(taskId, binding === undefined ? undefined : validateAmendmentBinding(binding, `amendment.tasks[${index}].binding`));
1571
+ }
1572
+ if (!Array.isArray(amendment.historical)) throw new Error("amendment.historical must be an array");
1573
+ for (const [index, child] of amendment.historical.entries()) {
1574
+ if (!child || typeof child !== "object" || Array.isArray(child)) throw new Error(`amendment.historical[${index}] must be an object`);
1575
+ const raw = child as { task_id: unknown; binding: unknown };
1576
+ const taskId = identifier(raw.task_id, `amendment.historical[${index}].task_id`);
1577
+ if (tasks.has(taskId) || historical.has(taskId)) throw new Error(`duplicate amendment Task id: ${taskId}`);
1578
+ historical.set(taskId, validateAmendmentBinding(raw.binding, `amendment.historical[${index}].binding`));
1579
+ }
1580
+ return { parent, tasks, historical };
1581
+ }
1582
+
1583
+ /** Compare a bound Issue's observed remote identity/content against the approved baseline. */
1584
+ function amendmentBindingDrift(binding: InitiativeAmendmentBinding, actual: GithubIssue, label: string): string | null {
1585
+ if (binding.issue_number !== actual.number) return `${label} is bound to Issue #${binding.issue_number} but observed Issue #${actual.number}`;
1586
+ if (binding.state !== actual.state) return `${label} is bound as ${binding.state} but observed as ${actual.state}`;
1587
+ if (binding.title !== actual.title || binding.body !== actual.body) return `${label} remote content does not match the approved baseline`;
1588
+ return null;
1589
+ }
1590
+
1591
+ /** Split observed Sub-issues into bound pending vs bound historical, verifying complete membership. */
1592
+ function classifyObservedChildren(
1593
+ source: RepositorySnapshot,
1594
+ initiativeId: string,
1595
+ pendingBindings: Map<string, InitiativeAmendmentBinding | undefined>,
1596
+ historicalBindings: Map<string, InitiativeAmendmentBinding>,
1597
+ ): Map<string, GithubIssue> | string {
1598
+ const observed = new Map<string, GithubIssue>();
1599
+ for (const issue of source.issues) {
1600
+ if (issue.body.includes(marker("initiative-id", initiativeId)) && issue.body.includes(KIND_TASK_MARKER)) {
1601
+ const taskId = ownershipMarkerValue(issue.body, "task-id");
1602
+ if (!taskId) return `Issue #${issue.number} has missing or duplicate task-id ownership markers`;
1603
+ if (observed.has(taskId)) return `duplicate Task Issue identity: ${taskId}`;
1604
+ observed.set(taskId, issue);
1605
+ }
1606
+ }
1607
+ const pendingIds = new Set(pendingBindings.keys());
1608
+ const historicalIds = new Set(historicalBindings.keys());
1609
+ for (const [taskId, issue] of observed) {
1610
+ if (issue.state === "closed" && pendingIds.has(taskId))
1611
+ return `pending Task ${taskId} (Issue #${issue.number}) is closed; the amendment cannot treat closed work as pending`;
1612
+ if (issue.state === "closed" && !historicalIds.has(taskId))
1613
+ return `closed Child ${taskId} (Issue #${issue.number}) must be declared as historical`;
1614
+ if (pendingIds.has(taskId)) continue;
1615
+ if (historicalIds.has(taskId)) {
1616
+ const binding = historicalBindings.get(taskId)!;
1617
+ const drift = amendmentBindingDrift(binding, issue, `historical Task ${taskId}`);
1618
+ if (drift) return drift;
1619
+ continue;
1620
+ }
1621
+ return `observed Child ${taskId} (Issue #${issue.number}) is neither approved pending nor declared historical; the amendment must declare the complete membership`;
1622
+ }
1623
+ return observed;
1624
+ }
1625
+
1626
+ async function validateAmendmentTopology(
1627
+ root: string,
1628
+ gh: GhTransport,
1629
+ source: RepositorySnapshot,
1630
+ initiativeId: string,
1631
+ parentIssue: GithubIssue,
1632
+ pendingBindings: Map<string, InitiativeAmendmentBinding | undefined>,
1633
+ historicalBindings: Map<string, InitiativeAmendmentBinding>,
1634
+ foreignDependers: Map<string, string[]>,
1635
+ approvedFinal: {
1636
+ pendingContent: Map<string, { title: string; body: string }>;
1637
+ parent: { title: string; body: string };
1638
+ historicalSlices: string[];
1639
+ },
1640
+ order: Array<Extract<TrackerOperation, { op: "upsert-task" }>>,
1641
+ ): Promise<Map<string, GithubIssue> | GithubTrackerResult> {
1642
+ const classified = classifyObservedChildren(source, initiativeId, pendingBindings, historicalBindings);
1643
+ if (typeof classified === "string") return result("create-initiative", "ambiguous_remote_state", classified, parentIssue);
1644
+ const bound = classified;
1645
+ const pendingIds = new Set(pendingBindings.keys());
1646
+ const historicalIds = new Set(historicalBindings.keys());
1647
+ const attached = await readSubIssueNumbers(root, gh, "create-initiative", source.repository, parentIssue.number);
1648
+ if (!Array.isArray(attached)) return attached;
1649
+ const attachedNumbers = new Set(attached);
1650
+ for (const [taskId, binding] of historicalBindings) {
1651
+ const issue = bound.get(taskId);
1652
+ if (!issue) return result("create-initiative", "ambiguous_remote_state", `historical Task ${taskId} is not observable`, parentIssue);
1653
+ if (binding.issue_number !== issue.number)
1654
+ return result("create-initiative", "ambiguous_remote_state", `historical Task ${taskId} is bound to Issue #${binding.issue_number} but observed Issue #${issue.number}`, issue);
1655
+ // Historical Children carry the same repository/protocol/marker-uniqueness
1656
+ // ownership guarantees as pending Children: a full taskLookup must resolve
1657
+ // the exact bound Issue, or the amendment fails closed before any write.
1658
+ const owned = taskLookup(source.issues, source.repository.id, taskId);
1659
+ if (owned.kind !== "found" || owned.issue.number !== issue.number)
1660
+ return result("create-initiative", "ambiguous_remote_state", owned.kind === "found" ? `historical Task ${taskId} resolves to Issue #${owned.issue.number}, not the bound Issue #${issue.number}` : `historical Task ${taskId} ownership failed: ${owned.kind === "ambiguous" ? owned.message : "not observable"}`, issue);
1661
+ if (!attachedNumbers.has(issue.number))
1662
+ return result("create-initiative", "ambiguous_remote_state", `historical Task ${taskId} (Issue #${issue.number}) is not attached to the Parent`, issue);
1663
+ const ownership = await confirmTerminalOwnership(root, gh, "create-initiative", source, issue);
1664
+ if (!("owned" in ownership)) return ownership;
1665
+ }
1666
+ for (const [taskId, binding] of pendingBindings) {
1667
+ const issue = bound.get(taskId);
1668
+ // Terminal evidence is validated before any mutation, including exact-baseline
1669
+ // matches: a lone marker, truncated suffix, or multiple markers in a bound
1670
+ // or resumable Child is malformed evidence (Kernel-only boundary), never
1671
+ // synthesizable content.
1672
+ if (issue) {
1673
+ const suffixEvent = issueTerminalEventId(issue.body);
1674
+ if (suffixEvent === "multiple")
1675
+ return result("create-initiative", "ambiguous_remote_state", `pending Task ${taskId} has multiple terminal markers`, issue);
1676
+ if (suffixEvent === "malformed")
1677
+ return result("create-initiative", "ambiguous_remote_state", `pending Task ${taskId} carries a malformed terminal marker (marker without its exact canonical suffix)`, issue);
1678
+ }
1679
+ if (binding === undefined) {
1680
+ // Unbound new Child: either it must not exist yet (fresh creation), or it
1681
+ // may already be the exact approved-final creation from a prior partial
1682
+ // write of this same batch (resumable creation) — anything else fails closed.
1683
+ // Resolve the task_id repo-wide regardless of whether the issue was found
1684
+ // under this Initiative: an unbound Task whose id is already owned by
1685
+ // another Initiative (or ambiguous repo-wide) must be rejected here,
1686
+ // before any Parent or preceding Child rewrite — not later in upsertTask.
1687
+ const ownedNew = taskLookup(source.issues, source.repository.id, taskId);
1688
+ if (ownedNew.kind !== "missing") {
1689
+ if (ownedNew.kind === "found") {
1690
+ if (issue && ownedNew.issue.number === issue.number) {
1691
+ const approved = approvedFinal.pendingContent.get(taskId)!;
1692
+ // An open Child left by a failed terminal close carries a terminal suffix;
1693
+ // the suffix is validated terminal evidence, not baseline drift.
1694
+ const resumable = issue.state === "open"
1695
+ && approved !== undefined
1696
+ && carriesApprovedContent(issue.title, issue.body, approved);
1697
+ if (!resumable)
1698
+ return result("create-initiative", "ambiguous_remote_state", `new pending Task ${taskId} is unbound but Issue #${issue.number} already exists with divergent content; bind it to amend`, issue);
1699
+ } else {
1700
+ return result("create-initiative", "ambiguous_remote_state", `new pending Task ${taskId} is unbound but task_id is already owned by Issue #${ownedNew.issue.number}; bind it to amend`, ownedNew.issue);
1701
+ }
1702
+ } else {
1703
+ return result("create-initiative", "ambiguous_remote_state", `new pending Task ${taskId} ownership failed: ${ownedNew.kind === "ambiguous" ? ownedNew.message : "not observable"}`, parentIssue);
1704
+ }
1705
+ }
1706
+ continue;
1707
+ }
1708
+ if (!issue) return result("create-initiative", "ambiguous_remote_state", `bound pending Task ${taskId} is not observable`, parentIssue);
1709
+ if (binding.issue_number !== issue.number)
1710
+ return result("create-initiative", "ambiguous_remote_state", `pending Task ${taskId} is bound to Issue #${binding.issue_number} but observed Issue #${issue.number}`, issue);
1711
+ if (issue.state !== "open")
1712
+ return result("create-initiative", "ambiguous_remote_state", `pending Task ${taskId} (Issue #${issue.number}) is closed and cannot be amended`, issue);
1713
+ // Repository-wide ownership resolution must hold for pending Children too,
1714
+ // before any Parent rewrite: a missing repo/protocol marker or a duplicate
1715
+ // task-id in another Initiative fails closed here, not later in upsertTask.
1716
+ const ownedPending = taskLookup(source.issues, source.repository.id, taskId);
1717
+ if (ownedPending.kind !== "found" || ownedPending.issue.number !== issue.number)
1718
+ return result("create-initiative", "ambiguous_remote_state", ownedPending.kind === "found" ? `pending Task ${taskId} resolves to Issue #${ownedPending.issue.number}, not the observed Issue #${issue.number}` : `pending Task ${taskId} ownership failed: ${ownedPending.kind === "ambiguous" ? ownedPending.message : "not observable"}`, issue);
1719
+ // The observed Child's Slice identity must match the operation's requested
1720
+ // slice before any write: a bound Child observed under another Task's slice
1721
+ // (e.g. a historical slice) is immutable ownership, never a rewrite target.
1722
+ const requestedSlice = order.find((operation) => operation.task_id === taskId)?.slice_id;
1723
+ const observedSlice = ownershipMarkerValue(issue.body, "slice-id");
1724
+ if (requestedSlice !== undefined && observedSlice !== null && observedSlice !== requestedSlice)
1725
+ return result("create-initiative", "ambiguous_remote_state", `pending Task ${taskId} (Issue #${issue.number}) carries slice-id=${observedSlice}, which does not match its requested Slice ${requestedSlice}`, issue);
1726
+ const approved = approvedFinal.pendingContent.get(taskId)!;
1727
+ const matchesBaseline = binding.title === issue.title
1728
+ && carriesApprovedContent(issue.title, issue.body, binding);
1729
+ const matchesApprovedFinal = approved && carriesApprovedContent(issue.title, issue.body, approved);
1730
+ // Attachment is required only for the baseline observation; an unattached
1731
+ // Child that already carries exact approved-final content (a prior partial
1732
+ // write) re-attaches during the upsert instead of failing closed.
1733
+ const carriesApprovedFinal = matchesApprovedFinal && issue.state === "open";
1734
+ if (!attachedNumbers.has(issue.number) && !carriesApprovedFinal)
1735
+ return result("create-initiative", "ambiguous_remote_state", `pending Task ${taskId} (Issue #${issue.number}) is not attached to the Parent`, issue);
1736
+ if (!matchesBaseline && !matchesApprovedFinal)
1737
+ return result("create-initiative", "ambiguous_remote_state", `pending Task ${taskId} remote content does not match the approved baseline or already-applied approved content`, issue);
1738
+ if (!carriesApprovedFinal) {
1739
+ // Ownership demands native Sub-issue attachment; an unattached Child that
1740
+ // already carries exact approved-final content (a prior partial write) skips
1741
+ // the attachment check here and is re-attached during the upsert instead.
1742
+ const ownership = await confirmTerminalOwnership(root, gh, "create-initiative", source, issue);
1743
+ if (!("owned" in ownership)) return ownership;
1744
+ }
1745
+ }
1746
+ for (const number of attachedNumbers) {
1747
+ const issue = source.issues.find((candidate) => candidate.number === number);
1748
+ const taskId = issue ? ownershipMarkerValue(issue.body, "task-id") : null;
1749
+ if (!taskId || (!pendingIds.has(taskId) && !historicalIds.has(taskId)))
1750
+ return result("create-initiative", "ambiguous_remote_state", `Parent Sub-issue #${number} is not part of the declared amendment membership`, parentIssue);
1751
+ }
1752
+ for (const [blockerId, dependers] of foreignDependers) {
1753
+ const issue = [...bound.values()].find((candidate) => ownershipMarkerValue(candidate.body, "task-id") === blockerId)
1754
+ ?? source.issues.find((candidate) => ownershipMarkerValue(candidate.body, "task-id") === blockerId);
1755
+ if (!issue)
1756
+ return result("create-initiative", "ambiguous_remote_state", `Task ${dependers.join(", ")} depends on ${blockerId}, which is not part of the declared amendment membership`, parentIssue);
1757
+ if (!attachedNumbers.has(issue.number))
1758
+ return result("create-initiative", "ambiguous_remote_state", `blocking Task ${blockerId} (Issue #${issue.number}) is not attached to the Parent`, issue);
1759
+ }
1760
+ return bound;
1761
+ }
1762
+
1763
+ /**
1764
+ * Extract a single validated terminal suffix from an Issue body, or null.
1765
+ * A marker only counts as terminal evidence when the body ends with the exact
1766
+ * canonical suffix for that event id (marker + evidence line + bounded id):
1767
+ * a lone marker or truncated suffix is malformed evidence, not a suffix.
1768
+ */
1769
+ function issueTerminalEventId(body: string): string | null | "multiple" | "malformed" {
1770
+ const suffixMatch = [...body.matchAll(/<!-- immune-brain:terminal-event=([A-Za-z0-9._:-]+) -->/g)];
1771
+ if (suffixMatch.length === 0) return null;
1772
+ if (suffixMatch.length > 1) return "multiple";
1773
+ const eventId = suffixMatch[0][1];
1774
+ if (eventId.length > MAX_TERMINAL_EVENT_ID || !body.endsWith(terminalSuffix(eventId)))
1775
+ return "malformed";
1776
+ return eventId;
1777
+ }
1778
+
1779
+ /**
1780
+ * Strip one validated terminal suffix from an Issue body, returning the
1781
+ * suffix-free bytes. Returns null when the body carries no terminal suffix;
1782
+ * callers treat a "multiple" result as fail-closed before calling.
1783
+ */
1784
+ /** Extract a validated terminal suffix from raw body bytes, or null. */
1785
+ function stripTerminalSuffixFromBytes(body: string | undefined): string | null | "multiple" | "malformed" {
1786
+ if (body === undefined) return null;
1787
+ const eventId = issueTerminalEventId(body);
1788
+ if (eventId === null || eventId === "multiple" || eventId === "malformed") return eventId;
1789
+ const suffix = terminalSuffix(eventId);
1790
+ if (!body.endsWith(suffix)) return null;
1791
+ return body.slice(0, body.length - suffix.length);
1792
+ }
1793
+
1794
+ /**
1795
+ * Suffix-aware content equality: an Issue (or raw baseline body) whose bytes
1796
+ * match the approved bytes exactly, or whose suffix-stripped bytes match the
1797
+ * approved (suffix-free) bytes, counts as carrying the approved content. A
1798
+ * validated terminal suffix is terminal evidence, not content drift. Multiple
1799
+ * terminal markers always fail closed.
1800
+ */
1801
+ function carriesApprovedContent(title: string, body: string, approved: { title: string; body: string }): boolean {
1802
+ if (title !== approved.title) return false;
1803
+ if (body === approved.body) return true;
1804
+ const stripped = stripTerminalSuffixFromBytes(body);
1805
+ return typeof stripped === "string" && stripped.trimEnd() === approved.body.trimEnd();
1806
+ }
1807
+
1808
+ async function updatePendingChild(
1809
+ root: string,
1810
+ gh: GhTransport,
1811
+ source: RepositorySnapshot,
1812
+ op: Extract<TrackerOperation, { op: "upsert-task" }>,
1813
+ child: GithubIssue,
1814
+ boundNumber: number | undefined,
1815
+ desiredBlockers: GithubIssue[],
1816
+ approvedFinal: { title: string; body: string } | undefined,
1817
+ amendmentContext: AmendmentExecutionContext | undefined = undefined,
1818
+ ): Promise<GithubTrackerResult> { // eslint-disable-line @typescript-eslint/no-unused-vars -- boundNumber retained for call-site symmetry
1819
+ const parent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
1820
+ const body = childBody(source.repository, op, parent.kind === "found" ? parent.issue : child);
1821
+ const oversized = bodyLimitFailure(op.op, body, MAX_TERMINAL_SUFFIX_BYTES);
1822
+ if (oversized) return oversized;
1823
+ const title = issueTitle(`${op.initiative_id}/${op.slice_id}`, op.projection?.result ?? op.goal);
1824
+ // Parent content expectation for pre-write revalidation: on the amendment
1825
+ // path the expectation is always the fixed approved final Parent bytes —
1826
+ // never re-adopt freshly observed content as the baseline, so an edit that
1827
+ // lands between the Parent write and this Child write fails closed. On the
1828
+ // non-amendment path (no amendmentContext) no Parent content expectation is
1829
+ // enforced here.
1830
+ const parentApprovedContent = amendmentContext?.parent;
1831
+ const parentBoundNumber = amendmentContext?.parentIssueNumber;
1832
+ const baseBody = child.body;
1833
+ const suffixEvent = issueTerminalEventId(baseBody);
1834
+ if (suffixEvent === "multiple") return result(op.op, "ambiguous_remote_state", "pending Task has multiple terminal markers", child);
1835
+ 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);
1836
+ // An open Child left by a failed terminal close retains its validated terminal
1837
+ // suffix; the approved-final bytes keep that suffix so the original-input batch
1838
+ // retry converges instead of failing closed on its own partial write.
1839
+ let finalBody = suffixEvent !== null ? `${body.trimEnd()}${terminalSuffix(suffixEvent)}` : body;
1840
+ // The approved-final bytes are suffix-free by construction; a Child carrying a
1841
+ // validated terminal suffix matches when its suffix-free bytes are exact.
1842
+ const approvedMatches = approvedFinal !== undefined && carriesApprovedContent(title, body, approvedFinal);
1843
+ if (!approvedMatches)
1844
+ return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a terminal suffix that diverges from the approved amendment content`, child);
1845
+ // R4 re-attach: a Child that already carries the exact approved-final content
1846
+ // but lost its native Sub-issue attachment (a detached intermediate state from
1847
+ // a prior partial write) is re-attached instead of failing closed, keeping the
1848
+ // original batch retryable. The Child must be bound to this amendment's
1849
+ // Parent (its marker Initiative) and carry no other parent edge — a foreign
1850
+ // attachment is ambiguous remote state, never re-attached.
1851
+ const approvedNow = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
1852
+ if (approvedNow) {
1853
+ const currentParent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
1854
+ if (currentParent.kind !== "found")
1855
+ return result(op.op, "ambiguous_remote_state", "pending Task Parent is not observable before attachment convergence", child);
1856
+ // Re-read and revalidate the Child immediately before the attachment
1857
+ // mutation: the earlier snapshot may have raced a concurrent edit or
1858
+ // close. exact identity, open state and approved-final bytes must hold,
1859
+ // or the re-attach fails closed with zero relation writes. The attachment
1860
+ // check is skipped here: the re-attach itself converges it and verifies
1861
+ // the relation after the write.
1862
+ const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, undefined, true, parentApprovedContent, parentBoundNumber);
1863
+ if ("contract" in revalidated) return revalidated;
1864
+ const targetParentNumber = parentBoundNumber ?? currentParent.issue.number;
1865
+ const attachment = await confirmAttachment(root, gh, op.op, source.repository, targetParentNumber, child.number);
1866
+ if (!("attached" in attachment)) return attachment;
1867
+ if (!attachment.attached) {
1868
+ const attached = await attachSubIssue(root, gh, op.op, source.repository, targetParentNumber, child);
1869
+ if (!("attached" in attached)) return attached;
1870
+ }
1871
+ }
1872
+ // The authoritative remote observation for the write decision is the
1873
+ // revalidated pre-write read, not the earlier snapshot: a terminal suffix
1874
+ // that landed after the snapshot must be preserved exactly, never
1875
+ // overwritten by bytes computed from stale data.
1876
+ let observed = child;
1877
+ if (child.title !== title || child.body !== finalBody) {
1878
+ // Re-read the Child immediately before writing: earlier blocker ownership
1879
+ // reads may have raced a concurrent user edit. The bound issue_number must
1880
+ // still hold and the remote content must still be baseline-or-approved-final.
1881
+ // revalidatePendingChildBeforeWrite additionally rechecks native Parent
1882
+ // attachment and exact Parent/Slice ownership so a detached or moved Child
1883
+ // never receives a content rewrite.
1884
+ const revalidated = await revalidatePendingChildBeforeWrite(
1885
+ root, gh, child.number, op.task_id, approvedFinal,
1886
+ { title: child.title, body: child.body },
1887
+ false, parentApprovedContent, parentBoundNumber,
1888
+ );
1889
+ if ("contract" in revalidated) return revalidated;
1890
+ observed = revalidated;
1891
+ // A newly observed terminal suffix on the remote (approved-final content
1892
+ // plus evidence appended after the snapshot) must survive this write: the
1893
+ // written body keeps the observed suffix instead of the snapshot-derived one.
1894
+ const observedEvent = issueTerminalEventId(observed.body);
1895
+ 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);
1896
+ const writeBody = typeof observedEvent === "string"
1897
+ ? `${body.trimEnd()}${terminalSuffix(observedEvent)}`
1898
+ : finalBody;
1899
+ if (observed.title !== title || observed.body !== writeBody) {
1900
+ const edited = await gh.run([
1901
+ "issue", "edit", String(child.number), "--repo", source.repository.name_with_owner,
1902
+ "--title", title,
1903
+ "--body-file", "-",
1904
+ ], { cwd: root, stdin: writeBody });
1905
+ if (edited.exit_code !== 0 || edited.output_exceeded) return ghFailure(op.op, edited, `pending Task Issue #${child.number} update failed`);
1906
+ finalBody = writeBody;
1907
+ }
1908
+ }
1909
+ const dependencies = await convergePendingDependencies(root, gh, source, child.number, op.task_id, desiredBlockers, approvedFinal, parentApprovedContent, parentBoundNumber);
1910
+ if (!("complete" in dependencies)) return dependencies;
1911
+ const refreshed = await snapshot(root, gh, op.op);
1912
+ if ("contract" in refreshed) return refreshed;
1913
+ const reread = ownedTaskLookup(refreshed.issues, refreshed.repository.id, op.task_id, op.initiative_id, op.slice_id);
1914
+ if (reread.kind !== "found" || reread.issue.number !== child.number)
1915
+ return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} changed identity during amendment`, child);
1916
+ if (reread.issue.title !== title || reread.issue.body !== finalBody)
1917
+ return result(op.op, "retryable_failure", `pending Task ${op.task_id} update did not converge`, reread.issue);
1918
+ const currentDependencies = await confirmBlockedBy(root, gh, op.op, refreshed.repository, reread.issue.number, desiredBlockers);
1919
+ if (!("complete" in currentDependencies)) return currentDependencies;
1920
+ if (!currentDependencies.complete)
1921
+ return result(op.op, "retryable_failure", `pending Task ${op.task_id} dependencies did not converge`, reread.issue);
1922
+ const contentCurrent = child.title === title && child.body === finalBody;
1923
+ return contentCurrent
1924
+ ? result(op.op, "already_current", `pending Task ${op.task_id} already carries the approved amendment content`, reread.issue)
1925
+ : result(op.op, "updated", `pending Task ${op.task_id} Agent Brief updated with approved amendment content`, reread.issue);
1926
+ }
1927
+
1928
+ /** Converge the exact approved dependency set on a pending Child (amendment only). */
1929
+ async function convergePendingDependencies(
1930
+ root: string,
1931
+ gh: GhTransport,
1932
+ source: RepositorySnapshot,
1933
+ childNumber: number,
1934
+ childTaskId: string,
1935
+ requestedBlockers: GithubIssue[],
1936
+ approvedFinal: { title: string; body: string } | undefined,
1937
+ parentApprovedContent: { title: string; body: string } | undefined = undefined,
1938
+ parentBoundNumber: number | undefined = undefined,
1939
+ ): Promise<GithubTrackerResult | { complete: true }> {
1940
+ const expected = requestedBlockers.map((blocker) => blocker.id);
1941
+ const existing = await readBlockedByIds(root, gh, "upsert-task", source.repository, childNumber);
1942
+ if (!Array.isArray(existing)) return existing;
1943
+ const removed = existing.filter((id) => !expected.includes(id));
1944
+ for (const id of removed) {
1945
+ const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
1946
+ if ("contract" in revalidated) return revalidated;
1947
+ const mutation = await gh.run([
1948
+ "api", "--method", "DELETE",
1949
+ `repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by/${id}`,
1950
+ ], { cwd: root });
1951
+ if (mutation.exit_code !== 0 || mutation.output_exceeded)
1952
+ return ghFailure("upsert-task", mutation, `native blocked_by removal failed for Issue #${childNumber}`);
1953
+ }
1954
+ const additions = requestedBlockers.filter((blocker) => !existing.includes(blocker.id));
1955
+ for (const blocker of additions) {
1956
+ const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
1957
+ if ("contract" in revalidated) return revalidated;
1958
+ const mutation = await gh.run([
1959
+ "api", "-F", `issue_id=${blocker.id}`,
1960
+ `repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by`,
1961
+ ], { cwd: root });
1962
+ if (mutation.exit_code !== 0 || mutation.output_exceeded)
1963
+ return ghFailure("upsert-task", mutation, `native blocked_by attachment failed for Issue #${blocker.number}`);
1964
+ }
1965
+ const confirm = await confirmBlockedBy(root, gh, "upsert-task", source.repository, childNumber, requestedBlockers);
1966
+ if (!("complete" in confirm)) return confirm;
1967
+ return confirm.complete ? { complete: true } : { complete: true };
1968
+ }
1969
+
1970
+ /**
1971
+ * Re-snapshot and re-validate a pending Child's bound identity, open state,
1972
+ * ownership, and baseline-or-approved-final content immediately before each
1973
+ * dependency write. Any drift stops the remaining mutations and fails closed
1974
+ * as ambiguous remote state. On success returns the validated Child
1975
+ * observation so callers can write the bytes actually present on the remote
1976
+ * (preserving a terminal suffix that landed after their earlier snapshot).
1977
+ * The return is discriminated by `contract`: a GithubTrackerResult failure
1978
+ * short-circuits the caller, a GithubIssue is the validated observation.
1979
+ */
1980
+ async function revalidatePendingChildBeforeWrite(
1981
+ root: string,
1982
+ gh: GhTransport,
1983
+ childNumber: number,
1984
+ childTaskId: string,
1985
+ approvedFinal: { title: string; body: string } | undefined,
1986
+ /** Additional byte-exact content this Child is allowed to carry (e.g. its pre-update baseline). */
1987
+ allowedBaseline: { title: string; body: string } | undefined = undefined,
1988
+ /** When true the native Sub-issue attachment check is skipped (re-attach phase: the attachment write itself is about to run). */
1989
+ skipAttachmentCheck = false,
1990
+ /** Expected exact Parent bytes (approved final after the Parent write, baseline before it); undefined skips the Parent content check. */
1991
+ parentApproved: { title: string; body: string } | undefined = undefined,
1992
+ /** Expected exact bound Parent issue number; undefined skips the Parent issue_number check. */
1993
+ parentExpectedNumber: number | undefined = undefined,
1994
+ ): Promise<GithubTrackerResult | GithubIssue> {
1995
+ const refreshed = await snapshot(root, gh, "upsert-task");
1996
+ if ("contract" in refreshed) return refreshed;
1997
+ // Repository-wide identity resolution: a duplicate Task Issue introduced after
1998
+ // the caller's snapshot must fail closed here, not at a later lookup — the
1999
+ // immediate pre-write observation is the authoritative one.
2000
+ const resolved = taskLookup(refreshed.issues, refreshed.repository.id, childTaskId);
2001
+ if (resolved.kind !== "found")
2002
+ return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} is not uniquely owned before a write: ${resolved.kind}`);
2003
+ const child = resolved.issue;
2004
+ if (child.number !== childNumber)
2005
+ return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} resolves to Issue #${child.number}, not the bound Issue #${childNumber}`, child);
2006
+ if (child.state !== "open")
2007
+ return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer open before a dependency write`, child);
2008
+ const contentFinal = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
2009
+ const contentBaseline = allowedBaseline !== undefined && child.title === allowedBaseline.title && child.body === allowedBaseline.body;
2010
+ if (!contentFinal && !contentBaseline)
2011
+ 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);
2012
+ const initiativeId = [...child.body.matchAll(/<!-- immune-brain:initiative-id=([A-Za-z0-9._:-]+) -->/g)].map((match) => match[1])[0];
2013
+ if (!initiativeId)
2014
+ return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its Initiative marker before a dependency write`, child);
2015
+ const parent = initiativeLookup(refreshed.issues, refreshed.repository.id, initiativeId);
2016
+ if (parent.kind !== "found" || parent.issue.state !== "open")
2017
+ return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its open Parent before a dependency write`, child);
2018
+ if (parentExpectedNumber !== undefined && parent.issue.number !== parentExpectedNumber)
2019
+ 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);
2020
+ // The Parent must still carry exactly the expected amendment bytes before any
2021
+ // Child write: after the Parent write it is the approved final content; before
2022
+ // the Parent write (non-amendment or pre-write paths) the earlier caller
2023
+ // snapshot bytes apply. A concurrent edit that keeps ownership markers intact
2024
+ // must fail closed here, not after the dependency mutations in final verification.
2025
+ if (parentApproved !== undefined
2026
+ && (parent.issue.title !== parentApproved.title || parent.issue.body !== parentApproved.body))
2027
+ return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent changed since the approved amendment content before a dependency write`, child);
2028
+ // Native Sub-issue attachment must still hold immediately before each
2029
+ // dependency write: a detached Child no longer belongs to the amendment.
2030
+ if (!skipAttachmentCheck) {
2031
+ const attachedNow = await readSubIssueNumbers(root, gh, "upsert-task", refreshed.repository, parent.issue.number);
2032
+ if (!Array.isArray(attachedNow)) return attachedNow;
2033
+ if (!attachedNow.includes(childNumber))
2034
+ return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer attached to the Parent before a dependency write`, child);
2035
+ }
2036
+ const sliceId = ownershipMarkerValue(child.body, "slice-id");
2037
+ if (sliceId && sliceCount(parent.issue.body, sliceId) !== 1)
2038
+ return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its exact Slice in the Parent before a dependency write`, child);
2039
+ return child;
2040
+ }
2041
+
1210
2042
  export async function runGithubInitiativePublication(
1211
2043
  root: string,
1212
2044
  input: InitiativePublicationInput,
@@ -1214,8 +2046,38 @@ export async function runGithubInitiativePublication(
1214
2046
  ): Promise<GithubInitiativePublicationResult> {
1215
2047
  const absoluteRoot = resolve(root);
1216
2048
  let prepared: ReturnType<typeof preflightPublication>;
2049
+ let amendment: ReturnType<typeof validateAmendment> | undefined;
2050
+ let amendmentContext: AmendmentExecutionContext | undefined;
1217
2051
  try {
1218
2052
  prepared = preflightPublication(absoluteRoot, input);
2053
+ if (input.amendment !== undefined) {
2054
+ if (prepared.order.length < 1)
2055
+ throw new Error("an amendment requires at least one pending Task");
2056
+ const validated = validateAmendment(input);
2057
+ const declared = new Set([...validated.tasks.keys(), ...validated.historical.keys()]);
2058
+ for (const operation of prepared.order) {
2059
+ if (!declared.has(operation.task_id))
2060
+ throw new Error(`amendment.tasks must declare pending Task ${operation.task_id}`);
2061
+ }
2062
+ // The pending batch must be exactly the declared amendment membership: an
2063
+ // input.tasks projection that omits a declared pending Task would pass
2064
+ // topology preflight and mutate the Parent before failing at final
2065
+ // membership, leaving the Parent without that Child's Slice line.
2066
+ const projectedIds = new Set(prepared.order.map((operation) => operation.task_id));
2067
+ for (const taskId of validated.tasks.keys()) {
2068
+ if (!projectedIds.has(taskId))
2069
+ throw new Error(`amendment.tasks declares pending Task ${taskId} but input.tasks omits its projection; the pending batch must cover every declared pending Task`);
2070
+ }
2071
+ for (const [taskId] of validated.historical) {
2072
+ if (projectedIds.has(taskId))
2073
+ throw new Error(`Task ${taskId} is declared historical but also projected in input.tasks`);
2074
+ }
2075
+ for (const [taskId, binding] of validated.historical) {
2076
+ if (binding.state === "open")
2077
+ throw new Error(`historical Task ${taskId} must be a closed Issue; open work belongs in amendment.tasks`);
2078
+ }
2079
+ amendment = validated;
2080
+ }
1219
2081
  } catch (error) {
1220
2082
  return publicationResult("permanent_failure", error instanceof Error ? error.message : String(error));
1221
2083
  }
@@ -1225,6 +2087,8 @@ export async function runGithubInitiativePublication(
1225
2087
  if ("contract" in initial) return publicationResult(initial.status, initial.message, initial);
1226
2088
  const initialParent = initiativeLookup(initial.issues, initial.repository.id, prepared.initiative.initiative_id);
1227
2089
  if (initialParent.kind === "ambiguous") return publicationResult("ambiguous_remote_state", initialParent.message);
2090
+ if (amendment && initialParent.kind === "missing")
2091
+ return publicationResult("permanent_failure", "an amendment requires the Initiative Parent to already exist");
1228
2092
  const parentForPreflight = initialParent.kind === "found" ? initialParent.issue : {
1229
2093
  id: Number.MAX_SAFE_INTEGER,
1230
2094
  number: Number.MAX_SAFE_INTEGER,
@@ -1241,16 +2105,85 @@ export async function runGithubInitiativePublication(
1241
2105
  if (childFailure) return publicationResult(childFailure.status, childFailure.message, childFailure);
1242
2106
  }
1243
2107
 
2108
+ if (amendment) {
2109
+ if (initialParent.kind !== "found")
2110
+ return publicationResult("permanent_failure", "an amendment requires the Initiative Parent to already exist");
2111
+ // Caller-controlled binding constraints (missing or duplicate historical
2112
+ // Slice markers) must surface as structured fail-closed publication results,
2113
+ // never as thrown exceptions escaping the guarded validation boundary.
2114
+ let approvedFinalParent: ReturnType<typeof approvedAmendmentContent>;
2115
+ try {
2116
+ approvedFinalParent = approvedAmendmentContent(absoluteRoot, initial.repository, initialParent.issue, prepared, amendment);
2117
+ } catch (error) {
2118
+ return publicationResult("permanent_failure", error instanceof Error ? error.message : String(error));
2119
+ }
2120
+ // Slice identity collisions are deterministic input contract violations (the
2121
+ // batch itself declares two Children with one Slice id), not remote drift.
2122
+ if (typeof approvedFinalParent === "string" && approvedFinalParent.includes("collides with a historical Task Slice"))
2123
+ return publicationResult("permanent_failure", approvedFinalParent);
2124
+ if (typeof approvedFinalParent === "string") return publicationResult("ambiguous_remote_state", approvedFinalParent);
2125
+ const { parent, pendingContent, historicalSlices } = approvedFinalParent;
2126
+ // Deterministic prerequisite completion is validated before any write: a
2127
+ // historical prerequisite already closed without state_reason "completed"
2128
+ // (e.g. not_planned) makes the batch permanently unfulfillable, so it must
2129
+ // fail closed in preflight with zero mutations instead of after the Parent
2130
+ // and Child writes (the final check still guards concurrent races).
2131
+ for (const operation of prepared.order) {
2132
+ for (const blockerId of operation.projection?.blocked_by ?? []) {
2133
+ if (!amendment.historical.has(blockerId)) continue;
2134
+ const blocker = taskLookup(initial.issues, initial.repository.id, blockerId);
2135
+ if (blocker.kind === "found" && (blocker.issue.state !== "closed" || blocker.issue.state_reason !== "completed"))
2136
+ return publicationResult("ambiguous_remote_state", `Task ${operation.task_id} depends on stopped historical prerequisite ${blockerId}`);
2137
+ }
2138
+ }
2139
+ const parentBaseline = amendment.parent.body !== initialParent.issue.body || amendment.parent.title !== initialParent.issue.title;
2140
+ const parentApproved = initialParent.issue.title === parent.title && initialParent.issue.body === parent.body;
2141
+ if (parentBaseline && !parentApproved) return publicationResult("ambiguous_remote_state", `amendment Parent changed since the approved amendment baseline`);
2142
+ if (amendment.parent.issue_number !== initialParent.issue.number)
2143
+ return publicationResult("ambiguous_remote_state", `amendment Parent is bound to Issue #${amendment.parent.issue_number} but observed Issue #${initialParent.issue.number}`);
2144
+ if (initialParent.issue.state !== "open")
2145
+ return publicationResult("ambiguous_remote_state", "an amendment requires the Initiative Parent to remain open");
2146
+ const topology = await validateAmendmentTopology(
2147
+ absoluteRoot, gh, initial, prepared.initiative.initiative_id, initialParent.issue,
2148
+ amendment.tasks, amendment.historical,
2149
+ prepared.foreign_dependers,
2150
+ { pendingContent, parent, historicalSlices },
2151
+ prepared.order,
2152
+ );
2153
+ if (!("get" in topology)) return publicationResult(topology.status, topology.message, topology);
2154
+ const historicalRelations = new Map<string, { blocked_by: number[]; state_reason: string | null }>();
2155
+ for (const [taskId, binding] of amendment.historical) {
2156
+ const issue = topology.get(taskId)!;
2157
+ const observed = await readBlockedByIds(absoluteRoot, gh, "upsert-task", initial.repository, issue.number);
2158
+ if (!Array.isArray(observed)) return publicationResult(observed.status, observed.message, observed);
2159
+ historicalRelations.set(taskId, { blocked_by: observed, state_reason: issue.state_reason });
2160
+ }
2161
+ amendmentContext = {
2162
+ pendingContent,
2163
+ parent,
2164
+ parentIssueNumber: amendment.parent.issue_number,
2165
+ historicalSlices,
2166
+ historicalRelations,
2167
+ };
2168
+ }
2169
+
1244
2170
  const beforeParentWrite = publicationIntentDrift(absoluteRoot, prepared.intent_bindings);
1245
2171
  if (beforeParentWrite) return publicationResult("ambiguous_remote_state", beforeParentWrite);
1246
- const parentResult = await runGithubTrackerOperation(absoluteRoot, prepared.initiative, gh);
2172
+ let parentResult: GithubTrackerResult;
2173
+ if (amendment && amendmentContext) {
2174
+ parentResult = await amendInitiativeParent(absoluteRoot, gh, prepared.initiative, initial, amendment.parent, amendmentContext);
2175
+ } else {
2176
+ parentResult = await runGithubTrackerOperation(absoluteRoot, prepared.initiative, gh);
2177
+ }
1247
2178
  if (!isSuccessfulTrackerStatus(parentResult.status))
1248
2179
  return publicationResult(parentResult.status, parentResult.message, parentResult);
1249
2180
  const taskResults: GithubInitiativePublicationResult["tasks"] = [];
1250
2181
  for (const operation of prepared.order) {
1251
2182
  const intentDrift = publicationIntentDrift(absoluteRoot, prepared.intent_bindings, [operation.task_id]);
1252
2183
  if (intentDrift) return publicationResult("ambiguous_remote_state", intentDrift, parentResult, taskResults);
1253
- const taskResult = await runGithubTrackerOperation(absoluteRoot, operation, gh);
2184
+ const taskResult = amendment
2185
+ ? await runAmendmentTaskOperation(absoluteRoot, gh, operation, amendment.tasks.get(operation.task_id), amendmentContext)
2186
+ : await runGithubTrackerOperation(absoluteRoot, operation, gh);
1254
2187
  taskResults.push({
1255
2188
  task_id: operation.task_id,
1256
2189
  slice_id: operation.slice_id,
@@ -1267,16 +2200,37 @@ export async function runGithubInitiativePublication(
1267
2200
  if (finalIntentDrift) return publicationResult("ambiguous_remote_state", finalIntentDrift, parentResult, taskResults);
1268
2201
  const finalSource = await snapshot(absoluteRoot, gh, "upsert-task");
1269
2202
  if ("contract" in finalSource) return publicationResult(finalSource.status, finalSource.message, parentResult, taskResults);
2203
+ if (amendment) {
2204
+ // Repeat complete observed-membership classification on the final snapshot:
2205
+ // an undeclared closed Task carrying this Initiative's markers that becomes
2206
+ // observable only after preflight (detached, so the Sub-issue parity check
2207
+ // cannot see it) must still fail the publication, not report success.
2208
+ const finalClassified = classifyObservedChildren(finalSource, prepared.initiative.initiative_id, amendment.tasks, amendment.historical);
2209
+ if (typeof finalClassified === "string") return publicationResult("ambiguous_remote_state", finalClassified, parentResult, taskResults);
2210
+ }
1270
2211
  const parent = initiativeLookup(finalSource.issues, finalSource.repository.id, prepared.initiative.initiative_id);
1271
2212
  if (parent.kind !== "found") return publicationResult("ambiguous_remote_state", parent.kind === "ambiguous" ? parent.message : "published Initiative Parent disappeared", parentResult, taskResults);
2213
+ const amendedFinalSliceIds = new Set(prepared.initiative.slices.map((slice) => slice.id));
1272
2214
  const parentDrift = publicationIssueDrift(
1273
2215
  parentResult,
1274
2216
  parent.issue,
1275
- issueTitle(prepared.initiative.initiative_id, prepared.initiative.projection?.result ?? prepared.initiative.goal),
1276
- createInitiativeBody(finalSource.repository, prepared.initiative),
2217
+ amendmentContext ? amendmentContext.parent.title : issueTitle(prepared.initiative.initiative_id, prepared.initiative.projection?.result ?? prepared.initiative.goal),
2218
+ amendmentContext
2219
+ ? amendmentContext.parent.body
2220
+ : createInitiativeBody(finalSource.repository, prepared.initiative),
1277
2221
  "Initiative Parent",
1278
2222
  );
1279
2223
  if (parentDrift) return publicationResult("ambiguous_remote_state", parentDrift, parentResult, taskResults);
2224
+ if (amendmentContext) {
2225
+ // The rewritten Parent must carry each declared historical Slice marker
2226
+ // exactly once: generation drops or duplicates would break terminal
2227
+ // ownership without failing any other final check.
2228
+ for (const line of amendmentContext.historicalSlices) {
2229
+ const sliceId = ownershipMarkerValue(line, "slice-id");
2230
+ if (!sliceId || countLiteral(parent.issue.body, marker("slice-id", sliceId)) !== 1)
2231
+ return publicationResult("ambiguous_remote_state", `regenerated Parent does not carry historical Slice ${sliceId ?? "?"} exactly once`, parentResult, taskResults);
2232
+ }
2233
+ }
1280
2234
  const resultByTask = new Map(taskResults.map((task) => [task.task_id, task]));
1281
2235
  const expectedNumbers: number[] = [];
1282
2236
  for (const operation of prepared.order) {
@@ -1290,6 +2244,7 @@ export async function runGithubInitiativePublication(
1290
2244
  issueTitle(`${operation.initiative_id}/${operation.slice_id}`, operation.projection?.result ?? operation.goal),
1291
2245
  childBody(finalSource.repository, operation, parent.issue),
1292
2246
  `Task ${operation.task_id}`,
2247
+ { allowTerminalSuffix: amendmentContext !== undefined },
1293
2248
  );
1294
2249
  if (childDrift) return publicationResult("ambiguous_remote_state", childDrift, parentResult, taskResults);
1295
2250
  expectedNumbers.push(child.issue.number);
@@ -1307,8 +2262,54 @@ export async function runGithubInitiativePublication(
1307
2262
  }
1308
2263
  const attached = await readSubIssueNumbers(absoluteRoot, gh, "upsert-task", finalSource.repository, parent.issue.number);
1309
2264
  if (!Array.isArray(attached)) return publicationResult(attached.status, attached.message, parentResult, taskResults);
2265
+ if (amendment) {
2266
+ for (const [taskId, binding] of amendment.historical) {
2267
+ // Full ownership lookup: repository, protocol, marker uniqueness, and
2268
+ // repo-wide Task identity must resolve the exact bound Issue.
2269
+ const owned = taskLookup(finalSource.issues, finalSource.repository.id, taskId);
2270
+ if (owned.kind !== "found" || owned.issue.number !== binding.issue_number)
2271
+ return publicationResult("ambiguous_remote_state", `historical Task ${taskId} ownership failed after publication`, parentResult, taskResults);
2272
+ const historical = owned.issue;
2273
+ const drift = amendmentBindingDrift(binding, historical, `historical Task ${taskId}`);
2274
+ if (drift) return publicationResult("ambiguous_remote_state", drift, parentResult, taskResults);
2275
+ if (!attached.includes(historical.number))
2276
+ return publicationResult("ambiguous_remote_state", `historical Task ${taskId} lost its native Sub-issue attachment`, parentResult, taskResults);
2277
+ // R1 final check: historical Slice identities must not collide with any
2278
+ // pending Slice published by this batch — two Children must never share
2279
+ // one Slice identity, including historical Children whose Parent lines are
2280
+ // carried over rather than re-rendered.
2281
+ const historicalSliceId = ownershipMarkerValue(historical.body, "slice-id");
2282
+ if (historicalSliceId && amendedFinalSliceIds.has(historicalSliceId))
2283
+ return publicationResult("ambiguous_remote_state", `historical Task ${taskId} Slice ${historicalSliceId} collides with a pending Task Slice of the same id`, parentResult, taskResults);
2284
+ const snapshotRelations = amendmentContext?.historicalRelations.get(taskId);
2285
+ if (snapshotRelations) {
2286
+ if (historical.state_reason !== snapshotRelations.state_reason)
2287
+ return publicationResult("ambiguous_remote_state", `historical Task ${taskId} terminal state_reason changed during amendment`, parentResult, taskResults);
2288
+ const finalBlockedBy = await readBlockedByIds(absoluteRoot, gh, "upsert-task", finalSource.repository, historical.number);
2289
+ if (!Array.isArray(finalBlockedBy)) return publicationResult(finalBlockedBy.status, finalBlockedBy.message, parentResult, taskResults);
2290
+ if (finalBlockedBy.length !== snapshotRelations.blocked_by.length || finalBlockedBy.some((id, index) => id !== snapshotRelations.blocked_by[index]))
2291
+ return publicationResult("ambiguous_remote_state", `historical Task ${taskId} native blocked_by relations changed during amendment`, parentResult, taskResults);
2292
+ }
2293
+ }
2294
+ const attachedNumbers = new Set(attached);
2295
+ for (const number of attachedNumbers) {
2296
+ const issue = finalSource.issues.find((candidate) => candidate.number === number);
2297
+ const taskId = issue ? ownershipMarkerValue(issue.body, "task-id") : null;
2298
+ if (!taskId || (!amendment.tasks.has(taskId) && !amendment.historical.has(taskId)))
2299
+ return publicationResult("ambiguous_remote_state", `Parent Sub-issue #${number} is not part of the declared amendment membership`, parentResult, taskResults);
2300
+ }
2301
+ for (const operation of prepared.order) {
2302
+ for (const blockerId of operation.projection?.blocked_by ?? []) {
2303
+ if (amendment.historical.has(blockerId)) {
2304
+ const blocker = taskLookup(finalSource.issues, finalSource.repository.id, blockerId);
2305
+ if (blocker.kind !== "found" || blocker.issue.state_reason !== "completed")
2306
+ return publicationResult("ambiguous_remote_state", `Task ${operation.task_id} depends on stopped historical prerequisite ${blockerId}`, parentResult, taskResults);
2307
+ }
2308
+ }
2309
+ }
2310
+ }
1310
2311
  const sortedAttached = [...attached].sort((left, right) => left - right);
1311
- const sortedExpected = [...expectedNumbers].sort((left, right) => left - right);
2312
+ const sortedExpected = [...expectedNumbers, ...(amendment ? [...amendment.historical.values()].map((binding) => binding.issue_number) : [])].sort((left, right) => left - right);
1312
2313
  if (sortedAttached.length !== sortedExpected.length || sortedAttached.some((number, index) => number !== sortedExpected[index]))
1313
2314
  return publicationResult("ambiguous_remote_state", "Initiative Parent Sub-issues do not match the complete publication batch", parentResult, taskResults);
1314
2315
  const statuses = [parentResult.status, ...taskResults.map((task) => task.status)];