immune-brain 3.6.7 → 3.6.9

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.
@@ -14,6 +14,15 @@ const GH_TIMEOUT_MS = 20_000;
14
14
  const MAX_SNAPSHOT_PAGES = 100;
15
15
  const GITHUB_ISSUE_BODY_LIMIT = 65_536;
16
16
  const MAX_TERMINAL_EVENT_ID = 500;
17
+ const MAX_TITLE_LENGTH = 80;
18
+ const MAX_DISPLAY_SHORT_NAME = 32;
19
+ const MAX_DISPLAY_TITLE = 60;
20
+ const READY_FOR_AGENT_LABEL = "ready-for-agent";
21
+ const BLOCKED_LABEL = "blocked";
22
+ const MANAGED_ISSUE_LABELS: readonly string[] = [READY_FOR_AGENT_LABEL, BLOCKED_LABEL];
23
+
24
+ /** Single compressed Activity/Authority footer shared by Parent and Task Issues. */
25
+ const ISSUE_FOOTER = "---\n\n_Outbound visibility only: GitHub state never authorizes or settles work — Kernel TaskIntent, TaskRecord, QA, Review, and Assurance remain the execution authority. An Open Issue only means the Task still needs attention; only a claimless terminal projection closes it (`done` → Completed, `stopped` → Not planned)._";
17
26
 
18
27
  type TrackerStatus =
19
28
  | "created"
@@ -42,6 +51,9 @@ export interface InitiativeSlice {
42
51
  }
43
52
 
44
53
  export interface InitiativeProjection {
54
+ short_name?: string;
55
+ title?: string;
56
+ source_issue?: string;
45
57
  problem?: string;
46
58
  result?: string;
47
59
  design?: string;
@@ -128,6 +140,9 @@ export interface GithubInitiativeObservation {
128
140
  }
129
141
 
130
142
  export interface TaskProjection {
143
+ short_name?: string;
144
+ title?: string;
145
+ slice_ordinal?: number;
131
146
  result?: string;
132
147
  current_behavior?: string;
133
148
  desired_behavior?: string;
@@ -188,6 +203,7 @@ interface GithubIssue {
188
203
  body: string;
189
204
  state: "open" | "closed";
190
205
  state_reason: string | null;
206
+ labels: string[];
191
207
  }
192
208
 
193
209
  interface RepositorySnapshot {
@@ -276,12 +292,120 @@ function titleText(value: string): string {
276
292
  return redactSecrets(value).replace(/\s+/g, " ");
277
293
  }
278
294
 
279
- function issueTitle(owner: string, result: string): string {
280
- const title = `[${owner}] ${titleText(result)}`;
281
- if (title.length > 256) throw new Error("GitHub Issue title must not exceed 256 characters");
295
+ /**
296
+ * A Planner-supplied display name for an Issue title. Display names are the
297
+ * only title source: the full goal/result prose stays in the body, and a
298
+ * missing or oversized display name fails closed instead of being truncated.
299
+ */
300
+ function displayName(value: unknown, name: string, max: number): string {
301
+ if (typeof value !== "string" || !value.trim())
302
+ throw new Error(`${name} is required: publish a bounded display name instead of the full goal text`);
303
+ // Brackets are rejected on the raw value: redaction later introduces its own
304
+ // bracketed marker, which must never be mistaken for caller-supplied syntax.
305
+ if (/[[\]]/.test(value)) throw new Error(`${name} must not contain square brackets`);
306
+ return projectionText(value, name, max);
307
+ }
308
+
309
+ function issueTitle(value: string): string {
310
+ const title = titleText(value);
311
+ if (title.length > MAX_TITLE_LENGTH)
312
+ throw new Error(`GitHub Issue title must not exceed ${MAX_TITLE_LENGTH} characters: shorten the Planner display names`);
282
313
  return title;
283
314
  }
284
315
 
316
+ function initiativeDisplayNames(projection: InitiativeProjection | undefined): { shortName: string } {
317
+ const missing = (["short_name", "title"] as const).filter((field) => projection?.[field] === undefined);
318
+ if (missing.length)
319
+ throw new Error(`Initiative projection requires display names; missing ${missing.map((field) => `projection.${field}`).join(", ")}`);
320
+ return { shortName: displayName(projection?.short_name, "projection.short_name", MAX_DISPLAY_SHORT_NAME) };
321
+ }
322
+
323
+ function initiativeIssueTitle(initiativeId: string, projection: InitiativeProjection | undefined): string {
324
+ const { shortName } = initiativeDisplayNames(projection);
325
+ const title = displayName(projection?.title, "projection.title", MAX_DISPLAY_TITLE);
326
+ return issueTitle(`[${shortName}] ${title}`);
327
+ }
328
+
329
+ function taskDisplayNames(operation: Extract<TrackerOperation, { op: "upsert-task" }>): { shortName: string; title: string; ordinal: number } {
330
+ const projection = operation.projection;
331
+ if (projection?.short_name === undefined || projection.title === undefined || projection.slice_ordinal === undefined)
332
+ throw new Error(`Task ${operation.task_id} requires projection.short_name, projection.title, and projection.slice_ordinal display names`);
333
+ return {
334
+ shortName: displayName(projection.short_name, "projection.short_name", MAX_DISPLAY_SHORT_NAME),
335
+ title: displayName(projection.title, "projection.title", MAX_DISPLAY_TITLE),
336
+ ordinal: projection.slice_ordinal,
337
+ };
338
+ }
339
+
340
+ function taskIssueTitle(operation: Extract<TrackerOperation, { op: "upsert-task" }>, fallbackOrdinal?: number): string {
341
+ const { shortName, title, ordinal } = taskDisplayNames(operation);
342
+ return issueTitle(`[${shortName}] S${fallbackOrdinal ?? ordinal} ${title}`);
343
+ }
344
+
345
+ /**
346
+ * The Slice position in the Initiative: the Parent's Slices checklist is the
347
+ * declared order, so a Child keeps the same `S<n>` across amendment batches
348
+ * instead of renumbering to its position inside the current batch.
349
+ */
350
+ function sliceOrdinalFromChecklist(parentBody: string, sliceId: string, fallback: number): number {
351
+ const declared = [...parentBody.matchAll(/^- \[[ xX]\] <!-- immune-brain:slice-id=([A-Za-z0-9._:-]+) -->/gm)].map((match) => match[1]);
352
+ const index = declared.indexOf(sliceId);
353
+ return index === -1 ? fallback : index + 1;
354
+ }
355
+
356
+ /** Labels a published Task Issue must carry; the Parent carries none of them. */
357
+ function desiredTaskLabels(operation: Extract<TrackerOperation, { op: "upsert-task" }>): string[] {
358
+ return (operation.projection?.blocked_by ?? []).length
359
+ ? [READY_FOR_AGENT_LABEL, BLOCKED_LABEL]
360
+ : [READY_FOR_AGENT_LABEL];
361
+ }
362
+
363
+ /** Converge managed labels from the observed set: add what is desired, remove only managed labels that are not. */
364
+ function labelMutationArgs(observed: string[], desired: string[]): string[] {
365
+ const args: string[] = [];
366
+ for (const label of desired) if (!observed.includes(label)) args.push("--add-label", label);
367
+ for (const label of MANAGED_ISSUE_LABELS)
368
+ if (observed.includes(label) && !desired.includes(label)) args.push("--remove-label", label);
369
+ return args;
370
+ }
371
+
372
+ async function repositoryLabels(root: string, gh: GhTransport, repository: RepositoryInfo): Promise<string[] | GithubTrackerResult> {
373
+ const execution = await gh.run(
374
+ ["label", "list", "--repo", repository.name_with_owner, "--json", "name", "--limit", "1000"],
375
+ { cwd: root },
376
+ );
377
+ if (execution.exit_code !== 0 || execution.output_exceeded)
378
+ return ghFailure("upsert-task", execution, "cannot query repository labels");
379
+ try {
380
+ const parsed = JSON.parse(execution.stdout) as unknown;
381
+ if (!Array.isArray(parsed)) throw new Error("gh returned malformed label list");
382
+ return parsed
383
+ .map((item) => (item as { name?: unknown })?.name)
384
+ .filter((name): name is string => typeof name === "string");
385
+ } catch (error) {
386
+ return result("upsert-task", "permanent_failure", error instanceof Error ? error.message : String(error));
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Labels are never created by the tracker: a repository missing a required
392
+ * label fails closed before any Issue mutation, naming the exact label.
393
+ */
394
+ async function labelAvailabilityFailure(
395
+ root: string,
396
+ gh: GhTransport,
397
+ repository: RepositoryInfo,
398
+ required: string[],
399
+ ): Promise<GithubTrackerResult | null> {
400
+ if (!required.length) return null;
401
+ const labels = await repositoryLabels(root, gh, repository);
402
+ if (!Array.isArray(labels)) return labels;
403
+ const missing = required.filter((label) => !labels.includes(label));
404
+ return missing.length
405
+ ? result("upsert-task", "permanent_failure", `repository labels missing: ${missing.join(", ")}; create them before publishing so Task Issues carry publication state`)
406
+ : null;
407
+ }
408
+
285
409
  export function redactGithubDiagnostic(value: string): string {
286
410
  return redactSecrets(value)
287
411
  .replace(/\s+/g, " ")
@@ -414,6 +538,11 @@ function parseIssues(raw: string): GithubIssue[] {
414
538
  body: typeof item.body === "string" ? item.body : "",
415
539
  state: item.state,
416
540
  state_reason: typeof item.state_reason === "string" ? item.state_reason.toLowerCase() : null,
541
+ labels: Array.isArray(item.labels)
542
+ ? item.labels
543
+ .map((label) => (typeof label === "string" ? label : (label as { name?: unknown })?.name))
544
+ .filter((name): name is string => typeof name === "string")
545
+ : [],
417
546
  };
418
547
  });
419
548
  }
@@ -721,12 +850,15 @@ function createInitiativeBody(
721
850
  historicalSlices: string[] = [],
722
851
  ): string {
723
852
  const projection = operation.projection ?? {};
853
+ const provenance = projection.source_issue
854
+ ? `## Provenance\n\n- Derived from #${projection.source_issue}: the originating feature Issue for this Initiative.\n\n`
855
+ : "";
724
856
  return `${[
725
857
  PROTOCOL_MARKER,
726
858
  KIND_INITIATIVE_MARKER,
727
859
  marker("repo-id", repository.id),
728
860
  marker("initiative-id", operation.initiative_id),
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`;
861
+ ].join("\n")}\n\n${provenance}## How to use this Issue\n\n- Edit planning prose and Slice ordering directly after creation.\n- Keep each Slice marker attached to exactly one stable Slice entry.\n- The tracker never rewrites or closes this Parent after creation; the tracker never changes or closes it automatically.\n\n## Problem\n\n${publicText(projection.problem ?? "The Initiative addresses the bounded delivery described below.", "projection.problem")}\n\n## Result\n\n${publicText(projection.result ?? operation.goal, "projection.result")}\n\n## Initiative design\n\n${publicText(projection.design ?? "Each Child preserves the shared Initiative decisions and boundaries recorded here.", "projection.design")}\n\n## Decisions\n\n${listText(projection.decisions, "- No additional Initiative decisions recorded.")}\n\n## Testing strategy\n\n${publicText(projection.testing_strategy ?? "Each Child closes from its focused acceptance verification.", "projection.testing_strategy")}\n\n## Out of scope\n\n${listText(projection.out_of_scope, "- Unrelated work outside this Initiative.")}\n\n## Slices\n\n${operation.slices.length + historicalSlices.length === 0 ? "No Slices recorded yet." : [...historicalSlices, ...operation.slices.map((slice) => `- [ ] ${marker("slice-id", slice.id)} **${slice.id}**: ${slice.result ?? slice.goal}${slice.blocked_by?.length ? ` (blocked by: ${slice.blocked_by.join(", ")})` : ""}`)].join("\n")}\n\n${ISSUE_FOOTER}\n`;
730
862
  }
731
863
 
732
864
  async function createInitiative(
@@ -741,7 +873,7 @@ async function createInitiative(
741
873
  const body = createInitiativeBody(source.repository, operation);
742
874
  const oversized = bodyLimitFailure(operation.op, body);
743
875
  if (oversized) return oversized;
744
- const title = issueTitle(operation.initiative_id, operation.projection?.result ?? operation.goal);
876
+ const title = initiativeIssueTitle(operation.initiative_id, operation.projection);
745
877
  if (found.kind === "missing") {
746
878
  const mutation = await gh.run([
747
879
  "issue", "create", "--repo", source.repository.name_with_owner,
@@ -761,8 +893,28 @@ async function createInitiative(
761
893
  ? result(operation.op, "created", "Initiative Issue created as the single GitHub source", confirmed.issue)
762
894
  : result(operation.op, "retryable_failure", "Initiative Issue did not converge to the requested initial title and body", confirmed.issue);
763
895
  }
764
- if (found.issue.body === body && found.issue.title === title)
896
+ if (found.issue.body === body && found.issue.title === title) {
897
+ // A repeated complete batch is a convergence pass: managed labels the Parent
898
+ // must not carry are repaired here, without rewriting its content.
899
+ const labelArgs = labelMutationArgs(found.issue.labels, []);
900
+ if (labelArgs.length) {
901
+ const edited = await gh.run([
902
+ "issue", "edit", String(found.issue.number), "--repo", source.repository.name_with_owner,
903
+ ...labelArgs,
904
+ ], { cwd: root });
905
+ if (edited.exit_code !== 0 || edited.output_exceeded)
906
+ return ghFailure(operation.op, edited, "Initiative Parent label convergence failed");
907
+ const refreshed = await snapshot(root, gh, operation.op);
908
+ if ("contract" in refreshed) return refreshed;
909
+ const confirmed = lookup(refreshed.issues);
910
+ if (confirmed.kind !== "found")
911
+ return result(operation.op, "ambiguous_remote_state", "Initiative Parent became ambiguous after label convergence", found.issue);
912
+ return confirmed.issue.title === title && confirmed.issue.body === body && !labelMutationArgs(confirmed.issue.labels, []).length
913
+ ? result(operation.op, "updated", "Initiative Issue managed labels converged", confirmed.issue)
914
+ : result(operation.op, "retryable_failure", "Initiative Issue did not converge to an unlabeled Parent", confirmed.issue);
915
+ }
765
916
  return result(operation.op, "already_current", "Initiative Issue already carries the requested initial source", found.issue);
917
+ }
766
918
  return result(
767
919
  operation.op,
768
920
  "permanent_failure",
@@ -785,7 +937,7 @@ function childBody(
785
937
  marker("initiative-id", operation.initiative_id),
786
938
  marker("slice-id", operation.slice_id),
787
939
  marker("task-id", operation.task_id),
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`;
940
+ ].join("\n")}\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## 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 ?? projection.result ?? 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${ISSUE_FOOTER}\n`;
789
941
  }
790
942
 
791
943
  /** Derived approved-final content and baseline-derived historical evidence for an amendment. */
@@ -846,28 +998,30 @@ function approvedAmendmentContent(
846
998
  );
847
999
  const historicalSlices = baselineHistoricalSlices(amendment.parent.body, amendedSliceIds, boundSliceIds);
848
1000
  if (typeof historicalSlices === "string") return historicalSlices;
849
- const pendingContent = new Map<string, { title: string; body: string }>();
850
1001
  const sourceStub = {
851
1002
  repository,
852
1003
  issues: [parentIssue],
853
1004
  };
854
1005
  void sourceStub;
1006
+ // The approved final Parent body fixes the Slices checklist, so every Child
1007
+ // title is numbered from the Initiative order rather than from this batch.
1008
+ const parentBody = createInitiativeBody(sourceStub.repository, parent, historicalSlices);
1009
+ const oversizedParent = bodyLimitFailure("create-initiative", parentBody);
1010
+ if (oversizedParent) return `${oversizedParent.status}: ${oversizedParent.message}`;
1011
+ const pendingContent = new Map<string, { title: string; body: string }>();
855
1012
  for (const operation of prepared.order) {
856
1013
  const body = childBody(repository, operation, parentIssue);
857
1014
  const oversized = bodyLimitFailure("upsert-task", body, MAX_TERMINAL_SUFFIX_BYTES);
858
1015
  if (oversized) return `${oversized.status}: ${oversized.message}`;
859
1016
  pendingContent.set(operation.task_id, {
860
- title: issueTitle(`${operation.initiative_id}/${operation.slice_id}`, operation.projection?.result ?? operation.goal),
1017
+ title: taskIssueTitle(operation, sliceOrdinalFromChecklist(parentBody, operation.slice_id, operation.projection?.slice_ordinal ?? 1)),
861
1018
  body,
862
1019
  });
863
1020
  }
864
- const parentBody = createInitiativeBody(sourceStub.repository, parent, historicalSlices);
865
- const oversizedParent = bodyLimitFailure("create-initiative", parentBody);
866
- if (oversizedParent) return `${oversizedParent.status}: ${oversizedParent.message}`;
867
1021
  return {
868
1022
  pendingContent,
869
1023
  parent: {
870
- title: issueTitle(parent.initiative_id, parent.projection?.result ?? parent.goal),
1024
+ title: initiativeIssueTitle(parent.initiative_id, parent.projection),
871
1025
  body: parentBody,
872
1026
  },
873
1027
  parentIssueNumber: parentIssue.number,
@@ -939,20 +1093,29 @@ async function amendInitiativeParent(
939
1093
  return result(operation.op, "ambiguous_remote_state", "an amendment requires the Initiative Parent to remain open", found.issue);
940
1094
  if (binding.issue_number !== found.issue.number)
941
1095
  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)
1096
+ if (found.issue.body === body && found.issue.title === title && !labelMutationArgs(found.issue.labels, []).length)
943
1097
  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)
1098
+ // Content that already matches the approved final bytes still converges labels;
1099
+ // anything else must still be the approved amendment baseline.
1100
+ if ((found.issue.body !== body || found.issue.title !== title)
1101
+ && (found.issue.body !== binding.body || found.issue.title !== binding.title))
945
1102
  return result(operation.op, "ambiguous_remote_state", "Initiative Parent changed since the approved amendment baseline", found.issue);
1103
+ // The Parent carries no Task state labels: any managed label observed on it is
1104
+ // drift and is removed by the same edit that writes the amended content.
1105
+ const labelArgs = labelMutationArgs(found.issue.labels, []);
946
1106
  const edited = await gh.run([
947
1107
  "issue", "edit", String(found.issue.number), "--repo", source.repository.name_with_owner,
948
1108
  "--title", title,
949
1109
  "--body-file", "-",
1110
+ ...labelArgs,
950
1111
  ], { cwd: root, stdin: body });
951
1112
  if (edited.exit_code !== 0 || edited.output_exceeded) return ghFailure(operation.op, edited, "Initiative amendment edit failed");
952
1113
  const refreshed = await snapshot(root, gh, operation.op);
953
1114
  if ("contract" in refreshed) return refreshed;
954
1115
  const confirmed = lookup(refreshed.issues);
955
1116
  if (confirmed.kind !== "found") return result(operation.op, "ambiguous_remote_state", "Initiative Parent became ambiguous after amendment", found.issue);
1117
+ if (MANAGED_ISSUE_LABELS.some((label) => confirmed.issue.labels.includes(label)))
1118
+ return result(operation.op, "retryable_failure", "Initiative amendment did not converge to an unlabeled Parent", confirmed.issue);
956
1119
  return confirmed.issue.body === body && confirmed.issue.title === title
957
1120
  ? result(operation.op, "updated", "Initiative Issue updated with approved amendment content", confirmed.issue)
958
1121
  : result(operation.op, "retryable_failure", "Initiative amendment did not converge to the requested title and body", confirmed.issue);
@@ -966,6 +1129,8 @@ async function upsertTask(
966
1129
  pendingBinding: InitiativeAmendmentBinding | undefined | null = null,
967
1130
  amendmentContext: AmendmentExecutionContext | undefined = undefined,
968
1131
  ): Promise<GithubTrackerResult> {
1132
+ const labelFailure = await labelAvailabilityFailure(root, gh, source.repository, desiredTaskLabels(operation));
1133
+ if (labelFailure) return labelFailure;
969
1134
  const parent = initiativeLookup(source.issues, source.repository.id, operation.initiative_id);
970
1135
  if (parent.kind === "ambiguous") return result(operation.op, "ambiguous_remote_state", parent.message);
971
1136
  if (parent.kind === "missing")
@@ -997,9 +1162,10 @@ async function upsertTask(
997
1162
  const body = childBody(source.repository, operation, parent.issue);
998
1163
  const oversized = bodyLimitFailure(operation.op, body, MAX_TERMINAL_SUFFIX_BYTES);
999
1164
  if (oversized) return oversized;
1000
- const title = issueTitle(`${operation.initiative_id}/${operation.slice_id}`, operation.projection?.result ?? operation.goal);
1165
+ const title = taskIssueTitle(operation, sliceOrdinalFromChecklist(parent.issue.body, operation.slice_id, operation.projection?.slice_ordinal ?? 1));
1001
1166
  let child: GithubIssue;
1002
1167
  let createdChild = false;
1168
+ let labelsConverged = false;
1003
1169
  if (found.kind === "missing") {
1004
1170
  // Pre-create re-read (amendment path): a concurrent writer may have already
1005
1171
  // created this unbound Task between the initial snapshot and our create — the
@@ -1039,6 +1205,7 @@ async function upsertTask(
1039
1205
  "issue", "create", "--repo", source.repository.name_with_owner,
1040
1206
  "--title", title,
1041
1207
  "--body-file", "-",
1208
+ ...desiredTaskLabels(operation).flatMap((label) => ["--label", label]),
1042
1209
  ], { cwd: root, stdin: body });
1043
1210
  const refreshed = await snapshot(root, gh, operation.op);
1044
1211
  if ("contract" in refreshed) return refreshed;
@@ -1106,6 +1273,18 @@ async function upsertTask(
1106
1273
  }
1107
1274
  if (found.issue.body !== body || found.issue.title !== title)
1108
1275
  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);
1276
+ // A repeated complete batch is a convergence pass: managed label drift is
1277
+ // repaired here without rewriting content the tracker never owns.
1278
+ const observedLabelArgs = labelMutationArgs(child.labels, desiredTaskLabels(operation));
1279
+ if (observedLabelArgs.length) {
1280
+ const edited = await gh.run([
1281
+ "issue", "edit", String(child.number), "--repo", source.repository.name_with_owner,
1282
+ ...observedLabelArgs,
1283
+ ], { cwd: root });
1284
+ if (edited.exit_code !== 0 || edited.output_exceeded)
1285
+ return ghFailure(operation.op, edited, `Task Issue #${child.number} label convergence failed`);
1286
+ labelsConverged = true;
1287
+ }
1109
1288
  }
1110
1289
  const attachment = await confirmAttachment(root, gh, operation.op, source.repository, parent.issue.number, child.number);
1111
1290
  if (!("attached" in attachment)) return attachment;
@@ -1141,7 +1320,7 @@ async function upsertTask(
1141
1320
  if (attachment.attached && dependencies.complete)
1142
1321
  return createdChild
1143
1322
  ? result(operation.op, "created", "Task Issue created and attached with native blocking relations", child)
1144
- : result(operation.op, "already_current", "Task Issue, native Sub-issue relation, and blocking relations are current", child);
1323
+ : result(operation.op, labelsConverged ? "updated" : "already_current", "Task Issue, native Sub-issue relation, and blocking relations are current", child);
1145
1324
  return createdChild
1146
1325
  ? result(operation.op, "created", "Task Issue created and attached as a native Sub-issue", child)
1147
1326
  : result(operation.op, "updated", "existing Task Issue attached as a native Sub-issue", child);
@@ -1272,7 +1451,13 @@ function normalizeProjection(value: TaskProjection | undefined): TaskProjection
1272
1451
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("projection must be an object");
1273
1452
  const blockedBy = normalizedProjectionList(value.blocked_by, "projection.blocked_by", 128)?.map((id) => identifier(id, "projection.blocked_by task_id"));
1274
1453
  if (blockedBy && new Set(blockedBy).size !== blockedBy.length) throw new Error("projection.blocked_by must not contain duplicate Task IDs");
1454
+ if (value.slice_ordinal !== undefined
1455
+ && (typeof value.slice_ordinal !== "number" || !Number.isSafeInteger(value.slice_ordinal) || value.slice_ordinal < 1 || value.slice_ordinal > 999))
1456
+ throw new Error("projection.slice_ordinal must be an integer between 1 and 999");
1275
1457
  return {
1458
+ short_name: value.short_name,
1459
+ title: value.title,
1460
+ slice_ordinal: value.slice_ordinal,
1276
1461
  result: value.result === undefined ? undefined : projectionText(value.result, "projection.result"),
1277
1462
  current_behavior: value.current_behavior === undefined ? undefined : projectionText(value.current_behavior, "projection.current_behavior"),
1278
1463
  desired_behavior: value.desired_behavior === undefined ? undefined : projectionText(value.desired_behavior, "projection.desired_behavior"),
@@ -1287,7 +1472,12 @@ function normalizeProjection(value: TaskProjection | undefined): TaskProjection
1287
1472
  function normalizeInitiativeProjection(value: InitiativeProjection | undefined): InitiativeProjection | undefined {
1288
1473
  if (value === undefined) return undefined;
1289
1474
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("projection must be an object");
1475
+ if (value.source_issue !== undefined && (typeof value.source_issue !== "string" || !/^[1-9][0-9]{0,9}$/.test(value.source_issue)))
1476
+ throw new Error("projection.source_issue must be a GitHub Issue number");
1290
1477
  return {
1478
+ short_name: value.short_name,
1479
+ title: value.title,
1480
+ source_issue: value.source_issue,
1291
1481
  problem: value.problem === undefined ? undefined : projectionText(value.problem, "projection.problem"),
1292
1482
  result: value.result === undefined ? undefined : projectionText(value.result, "projection.result"),
1293
1483
  design: value.design === undefined ? undefined : projectionText(value.design, "projection.design"),
@@ -1318,7 +1508,7 @@ function validateOperation(operation: TrackerOperation): TrackerOperation {
1318
1508
  };
1319
1509
  }),
1320
1510
  };
1321
- issueTitle(normalized.initiative_id, normalized.projection?.result ?? normalized.goal);
1511
+ initiativeIssueTitle(normalized.initiative_id, normalized.projection);
1322
1512
  return normalized;
1323
1513
  }
1324
1514
  if (operation.op === "upsert-task") {
@@ -1335,7 +1525,7 @@ function validateOperation(operation: TrackerOperation): TrackerOperation {
1335
1525
  summary: projectionText(item.summary, `acceptance[${index}].summary`, 500),
1336
1526
  })),
1337
1527
  };
1338
- issueTitle(`${normalized.initiative_id}/${normalized.slice_id}`, normalized.projection?.result ?? normalized.goal);
1528
+ taskIssueTitle(normalized);
1339
1529
  return normalized;
1340
1530
  }
1341
1531
  return {
@@ -1446,10 +1636,14 @@ function preflightPublication(root: string, input: InitiativePublicationInput):
1446
1636
  if (input.projection[field] === undefined)
1447
1637
  throw new Error(`a complete Initiative publication requires projection.${field}`);
1448
1638
  }
1639
+ // The Initiative display names are validated once here, before any Task
1640
+ // projection is stamped with them, so a missing or oversized display name
1641
+ // fails the batch closed with zero remote writes.
1642
+ const initiativeShortName = initiativeDisplayNames(input.projection).shortName;
1449
1643
  const publications = input.tasks.map((task, index) => {
1450
1644
  if (!task || typeof task !== "object" || Array.isArray(task)) throw new Error(`tasks[${index}] must be an object`);
1451
1645
  if (typeof task.intent !== "string") throw new Error(`tasks[${index}].intent must be a string`);
1452
- return taskPublication(root, input.initiative_id, task.slice_id, task.intent, task.acceptance, task.projection);
1646
+ return taskPublication(root, input.initiative_id, task.slice_id, task.intent, task.acceptance, task.projection, index + 1, initiativeShortName);
1453
1647
  });
1454
1648
  const historicalIds = new Set<string>();
1455
1649
  if (input.amendment) {
@@ -1820,7 +2014,7 @@ async function updatePendingChild(
1820
2014
  const body = childBody(source.repository, op, parent.kind === "found" ? parent.issue : child);
1821
2015
  const oversized = bodyLimitFailure(op.op, body, MAX_TERMINAL_SUFFIX_BYTES);
1822
2016
  if (oversized) return oversized;
1823
- const title = issueTitle(`${op.initiative_id}/${op.slice_id}`, op.projection?.result ?? op.goal);
2017
+ const title = taskIssueTitle(op, sliceOrdinalFromChecklist(parent.kind === "found" ? parent.issue.body : child.body, op.slice_id, op.projection?.slice_ordinal ?? 1));
1824
2018
  // Parent content expectation for pre-write revalidation: on the amendment
1825
2019
  // path the expectation is always the fixed approved final Parent bytes —
1826
2020
  // never re-adopt freshly observed content as the baseline, so an edit that
@@ -1874,7 +2068,8 @@ async function updatePendingChild(
1874
2068
  // that landed after the snapshot must be preserved exactly, never
1875
2069
  // overwritten by bytes computed from stale data.
1876
2070
  let observed = child;
1877
- if (child.title !== title || child.body !== finalBody) {
2071
+ const desiredLabels = desiredTaskLabels(op);
2072
+ if (child.title !== title || child.body !== finalBody || labelMutationArgs(child.labels, desiredLabels).length) {
1878
2073
  // Re-read the Child immediately before writing: earlier blocker ownership
1879
2074
  // reads may have raced a concurrent user edit. The bound issue_number must
1880
2075
  // still hold and the remote content must still be baseline-or-approved-final.
@@ -1896,11 +2091,13 @@ async function updatePendingChild(
1896
2091
  const writeBody = typeof observedEvent === "string"
1897
2092
  ? `${body.trimEnd()}${terminalSuffix(observedEvent)}`
1898
2093
  : finalBody;
1899
- if (observed.title !== title || observed.body !== writeBody) {
2094
+ const labelArgs = labelMutationArgs(observed.labels, desiredLabels);
2095
+ if (observed.title !== title || observed.body !== writeBody || labelArgs.length) {
1900
2096
  const edited = await gh.run([
1901
2097
  "issue", "edit", String(child.number), "--repo", source.repository.name_with_owner,
1902
2098
  "--title", title,
1903
2099
  "--body-file", "-",
2100
+ ...labelArgs,
1904
2101
  ], { cwd: root, stdin: writeBody });
1905
2102
  if (edited.exit_code !== 0 || edited.output_exceeded) return ghFailure(op.op, edited, `pending Task Issue #${child.number} update failed`);
1906
2103
  finalBody = writeBody;
@@ -1915,11 +2112,15 @@ async function updatePendingChild(
1915
2112
  return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} changed identity during amendment`, child);
1916
2113
  if (reread.issue.title !== title || reread.issue.body !== finalBody)
1917
2114
  return result(op.op, "retryable_failure", `pending Task ${op.task_id} update did not converge`, reread.issue);
2115
+ const labelsCurrent = !desiredLabels.some((label) => !reread.issue.labels.includes(label));
2116
+ if (!labelsCurrent)
2117
+ return result(op.op, "retryable_failure", `pending Task ${op.task_id} labels did not converge`, reread.issue);
1918
2118
  const currentDependencies = await confirmBlockedBy(root, gh, op.op, refreshed.repository, reread.issue.number, desiredBlockers);
1919
2119
  if (!("complete" in currentDependencies)) return currentDependencies;
1920
2120
  if (!currentDependencies.complete)
1921
2121
  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;
2122
+ const contentCurrent = child.title === title && child.body === finalBody
2123
+ && labelMutationArgs(child.labels, desiredLabels).length === 0;
1923
2124
  return contentCurrent
1924
2125
  ? result(op.op, "already_current", `pending Task ${op.task_id} already carries the approved amendment content`, reread.issue)
1925
2126
  : result(op.op, "updated", `pending Task ${op.task_id} Agent Brief updated with approved amendment content`, reread.issue);
@@ -2089,6 +2290,12 @@ export async function runGithubInitiativePublication(
2089
2290
  if (initialParent.kind === "ambiguous") return publicationResult("ambiguous_remote_state", initialParent.message);
2090
2291
  if (amendment && initialParent.kind === "missing")
2091
2292
  return publicationResult("permanent_failure", "an amendment requires the Initiative Parent to already exist");
2293
+ // Label availability is validated before any remote mutation: the tracker
2294
+ // never creates labels, so a missing label fails the whole batch closed
2295
+ // instead of half-publishing an Initiative.
2296
+ const requiredLabels = [...new Set(prepared.order.flatMap((operation) => desiredTaskLabels(operation)))];
2297
+ const labelFailure = await labelAvailabilityFailure(absoluteRoot, gh, initial.repository, requiredLabels);
2298
+ if (labelFailure) return publicationResult(labelFailure.status, labelFailure.message, labelFailure);
2092
2299
  const parentForPreflight = initialParent.kind === "found" ? initialParent.issue : {
2093
2300
  id: Number.MAX_SAFE_INTEGER,
2094
2301
  number: Number.MAX_SAFE_INTEGER,
@@ -2097,6 +2304,7 @@ export async function runGithubInitiativePublication(
2097
2304
  body: "",
2098
2305
  state: "open" as const,
2099
2306
  state_reason: null,
2307
+ labels: [],
2100
2308
  };
2101
2309
  const parentBodyFailure = bodyLimitFailure("create-initiative", createInitiativeBody(initial.repository, prepared.initiative));
2102
2310
  if (parentBodyFailure) return publicationResult(parentBodyFailure.status, parentBodyFailure.message, parentBodyFailure);
@@ -2214,7 +2422,7 @@ export async function runGithubInitiativePublication(
2214
2422
  const parentDrift = publicationIssueDrift(
2215
2423
  parentResult,
2216
2424
  parent.issue,
2217
- amendmentContext ? amendmentContext.parent.title : issueTitle(prepared.initiative.initiative_id, prepared.initiative.projection?.result ?? prepared.initiative.goal),
2425
+ amendmentContext ? amendmentContext.parent.title : initiativeIssueTitle(prepared.initiative.initiative_id, prepared.initiative.projection),
2218
2426
  amendmentContext
2219
2427
  ? amendmentContext.parent.body
2220
2428
  : createInitiativeBody(finalSource.repository, prepared.initiative),
@@ -2241,12 +2449,15 @@ export async function runGithubInitiativePublication(
2241
2449
  const childDrift = publicationIssueDrift(
2242
2450
  childResult,
2243
2451
  child.issue,
2244
- issueTitle(`${operation.initiative_id}/${operation.slice_id}`, operation.projection?.result ?? operation.goal),
2452
+ taskIssueTitle(operation, sliceOrdinalFromChecklist(parent.issue.body, operation.slice_id, operation.projection?.slice_ordinal ?? 1)),
2245
2453
  childBody(finalSource.repository, operation, parent.issue),
2246
2454
  `Task ${operation.task_id}`,
2247
2455
  { allowTerminalSuffix: amendmentContext !== undefined },
2248
2456
  );
2249
2457
  if (childDrift) return publicationResult("ambiguous_remote_state", childDrift, parentResult, taskResults);
2458
+ const expectedLabels = desiredTaskLabels(operation);
2459
+ if (childResult.status !== "already_current" && expectedLabels.some((label) => !child.issue.labels.includes(label)))
2460
+ return publicationResult("ambiguous_remote_state", `Task ${operation.task_id} is missing publication labels after publication`, parentResult, taskResults);
2250
2461
  expectedNumbers.push(child.issue.number);
2251
2462
  const ownership = await confirmTerminalOwnership(absoluteRoot, gh, "upsert-task", finalSource, child.issue);
2252
2463
  if (!("owned" in ownership)) return publicationResult(ownership.status, ownership.message, parentResult, taskResults);
@@ -2341,7 +2552,9 @@ function taskPublication(
2341
2552
  sliceId: string,
2342
2553
  intentPath: string,
2343
2554
  acceptance: unknown,
2344
- projection?: TaskProjection,
2555
+ projection: TaskProjection | undefined,
2556
+ ordinal: number,
2557
+ initiativeShortName: string,
2345
2558
  ): PreparedPublicationTask {
2346
2559
  const absoluteRoot = resolve(root);
2347
2560
  const absolutePath = resolve(absoluteRoot, intentPath);
@@ -2367,6 +2580,18 @@ function taskPublication(
2367
2580
  });
2368
2581
  const missingIds = [...expectedIds].filter((id) => !publicById.has(id));
2369
2582
  if (missingIds.length) throw new Error(`Task ${taskId} is missing public acceptance ids: ${missingIds.join(", ")}`);
2583
+ // Display names are stamped from the Initiative so every Child title carries
2584
+ // the same short name and its declared Slice position; an explicitly
2585
+ // supplied value must agree, never silently override.
2586
+ if (projection?.short_name !== undefined && projection.short_name !== initiativeShortName)
2587
+ throw new Error(`Task ${taskId} projection.short_name must match the Initiative short name`);
2588
+ if (projection?.slice_ordinal !== undefined && projection.slice_ordinal !== ordinal)
2589
+ throw new Error(`Task ${taskId} projection.slice_ordinal must match its declared Slice position ${ordinal}`);
2590
+ const stampedProjection: TaskProjection = {
2591
+ ...(projection ?? {}),
2592
+ short_name: initiativeShortName,
2593
+ slice_ordinal: ordinal,
2594
+ };
2370
2595
  return {
2371
2596
  operation: validateOperation({
2372
2597
  op: "upsert-task",
@@ -2376,7 +2601,7 @@ function taskPublication(
2376
2601
  goal: intent.goal,
2377
2602
  risk: intent.risk,
2378
2603
  acceptance: intent.acceptance.map((item) => publicById.get(item.id)!),
2379
- projection,
2604
+ projection: stampedProjection,
2380
2605
  }) as Extract<TrackerOperation, { op: "upsert-task" }>,
2381
2606
  intent_path: read.intent_ref.path,
2382
2607
  intent_content_hash: read.content_hash,
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/plugin_versioning.ts from the root package.json.
2
- export const PLUGIN_VERSION = "3.6.7" as const;
2
+ export const PLUGIN_VERSION = "3.6.9" as const;
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: imm-review-retro
3
+ description: Use when the user explicitly requests Immune-Brain ranking of models by cross-model review load or a project usage retro.
4
+ ---
5
+
6
+ # Immune-Brain: Review Retro
7
+
8
+ Use [`../../dist/imm-review-retro.md`](../../dist/imm-review-retro.md) as the
9
+ canonical contract index, not a whole-document read. This is a
10
+ standalone host-native analysis entry, not a Managed Path continuation
11
+ and not an `imm-loop` internal-role dispatch.
12
+
13
+ Mandatory constraints: read-only on pi session logs. Do not edit code, tests,
14
+ Specs, or workflow state. Do not write session logs or `.imm/` files.
15
+
16
+ Section routes - load a section's instructions only when its branch applies.
17
+ Read each linked heading body up to the next heading; nested sections and
18
+ references load only under their own condition. Never read the whole contract
19
+ or all references as an entry prerequisite.
20
+
21
+ - common: [Boundary](../../dist/imm-review-retro.md#boundary), [Invocation](../../dist/imm-review-retro.md#invocation)
22
+ - running the analyzer: [Counting rules](../../dist/imm-review-retro.md#counting-rules), [CLI](../../dist/imm-review-retro.md#cli)
23
+ - interpreting the report: [Report](../../dist/imm-review-retro.md#report), [Caveats](../../dist/imm-review-retro.md#caveats)