immune-brain 3.0.2 → 3.2.1
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.
- package/README.md +9 -7
- package/README.zh-CN.md +9 -7
- package/package.json +3 -3
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +1 -3
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +3 -3
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +39 -9
- package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +2 -2
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +8 -4
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +2 -2
- package/plugins/immune-brain/dist/imm-agent-doc-maintain.md +137 -0
- package/plugins/immune-brain/dist/imm-loop.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +56 -47
- package/plugins/immune-brain/dist/registry.yaml +9 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +341 -51
- package/plugins/immune-brain/runtime/v4_runtime.ts +2 -3
- package/plugins/immune-brain/skills/imm-agent-doc-maintain/SKILL.md +11 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +1 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +56 -47
- package/plugins/immune-brain/skills/registry.yaml +9 -0
|
@@ -30,6 +30,7 @@ export interface GithubTrackerResult {
|
|
|
30
30
|
association_found: boolean;
|
|
31
31
|
issue_number?: number;
|
|
32
32
|
issue_url?: string;
|
|
33
|
+
node_id?: string;
|
|
33
34
|
message: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -43,11 +44,47 @@ export interface InitiativeSlice {
|
|
|
43
44
|
export interface InitiativeProjection {
|
|
44
45
|
problem?: string;
|
|
45
46
|
result?: string;
|
|
47
|
+
design?: string;
|
|
46
48
|
decisions?: string[];
|
|
47
49
|
testing_strategy?: string;
|
|
48
50
|
out_of_scope?: string[];
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
export interface InitiativePublicationInput {
|
|
54
|
+
initiative_id: string;
|
|
55
|
+
goal: string;
|
|
56
|
+
projection: InitiativeProjection;
|
|
57
|
+
tasks: Array<{
|
|
58
|
+
slice_id: string;
|
|
59
|
+
intent: string;
|
|
60
|
+
projection?: TaskProjection;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GithubInitiativePublicationResult {
|
|
65
|
+
contract: "immune_brain/github_initiative_publication/v1";
|
|
66
|
+
operation: "publish-initiative";
|
|
67
|
+
status: TrackerStatus;
|
|
68
|
+
initiative?: GithubTrackerResult;
|
|
69
|
+
tasks: Array<{
|
|
70
|
+
task_id: string;
|
|
71
|
+
slice_id: string;
|
|
72
|
+
status: TrackerStatus;
|
|
73
|
+
issue_number?: number;
|
|
74
|
+
issue_url?: string;
|
|
75
|
+
node_id?: string;
|
|
76
|
+
}>;
|
|
77
|
+
execution?: {
|
|
78
|
+
recommended_first_task_id: string;
|
|
79
|
+
recommended_first_issue_number: number;
|
|
80
|
+
order: string[];
|
|
81
|
+
issue_order: number[];
|
|
82
|
+
parallel_groups: string[][];
|
|
83
|
+
parallel_issue_groups: number[][];
|
|
84
|
+
};
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
51
88
|
export interface TaskProjection {
|
|
52
89
|
result?: string;
|
|
53
90
|
current_behavior?: string;
|
|
@@ -215,7 +252,7 @@ function result(
|
|
|
215
252
|
operation,
|
|
216
253
|
status,
|
|
217
254
|
association_found: issue !== undefined,
|
|
218
|
-
...(issue ? { issue_number: issue.number, issue_url: issue.url } : {}),
|
|
255
|
+
...(issue ? { issue_number: issue.number, issue_url: issue.url, node_id: String(issue.id) } : {}),
|
|
219
256
|
message: redactGithubDiagnostic(message),
|
|
220
257
|
};
|
|
221
258
|
}
|
|
@@ -431,24 +468,34 @@ function sliceCount(parentBody: string, sliceId: string): number {
|
|
|
431
468
|
return countLiteral(parentBody, marker("slice-id", sliceId));
|
|
432
469
|
}
|
|
433
470
|
|
|
434
|
-
async function
|
|
471
|
+
async function readSubIssueNumbers(
|
|
435
472
|
root: string,
|
|
436
473
|
gh: GhTransport,
|
|
437
474
|
operation: TrackerOperation["op"],
|
|
438
475
|
repository: RepositoryInfo,
|
|
439
476
|
parentNumber: number,
|
|
440
|
-
|
|
441
|
-
): Promise<GithubTrackerResult | { attached: boolean }> {
|
|
477
|
+
): Promise<GithubTrackerResult | number[]> {
|
|
442
478
|
const listed = await gh.run(["api", "--paginate", "--slurp", `repos/${repository.name_with_owner}/issues/${parentNumber}/sub_issues?per_page=100`], { cwd: root });
|
|
443
479
|
if (listed.exit_code !== 0 || listed.output_exceeded)
|
|
444
480
|
return ghFailure(operation, listed, "cannot read native Sub-issue relations");
|
|
445
|
-
let numbers: number[];
|
|
446
481
|
try {
|
|
447
|
-
|
|
482
|
+
return parseSubIssueNumbers(listed.stdout);
|
|
448
483
|
} catch (error) {
|
|
449
484
|
return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
|
|
450
485
|
}
|
|
451
|
-
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function confirmAttachment(
|
|
489
|
+
root: string,
|
|
490
|
+
gh: GhTransport,
|
|
491
|
+
operation: TrackerOperation["op"],
|
|
492
|
+
repository: RepositoryInfo,
|
|
493
|
+
parentNumber: number,
|
|
494
|
+
childNumber: number,
|
|
495
|
+
): Promise<GithubTrackerResult | { attached: boolean }> {
|
|
496
|
+
const read = await readSubIssueNumbers(root, gh, operation, repository, parentNumber);
|
|
497
|
+
if (!Array.isArray(read)) return read;
|
|
498
|
+
const matches = read.filter((candidate) => candidate === childNumber).length;
|
|
452
499
|
if (matches > 1) return result(operation, "ambiguous_remote_state", `Issue #${parentNumber} lists the Task Issue more than once`);
|
|
453
500
|
return { attached: matches === 1 };
|
|
454
501
|
}
|
|
@@ -565,7 +612,7 @@ function createInitiativeBody(repository: RepositoryInfo, operation: Extract<Tra
|
|
|
565
612
|
KIND_INITIATIVE_MARKER,
|
|
566
613
|
marker("repo-id", repository.id),
|
|
567
614
|
marker("initiative-id", operation.initiative_id),
|
|
568
|
-
].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## 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`;
|
|
615
|
+
].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`;
|
|
569
616
|
}
|
|
570
617
|
|
|
571
618
|
async function createInitiative(
|
|
@@ -610,7 +657,11 @@ async function createInitiative(
|
|
|
610
657
|
);
|
|
611
658
|
}
|
|
612
659
|
|
|
613
|
-
function childBody(
|
|
660
|
+
function childBody(
|
|
661
|
+
repository: RepositoryInfo,
|
|
662
|
+
operation: Extract<TrackerOperation, { op: "upsert-task" }>,
|
|
663
|
+
parent: GithubIssue,
|
|
664
|
+
): string {
|
|
614
665
|
const projection = operation.projection ?? {};
|
|
615
666
|
const acceptance = operation.acceptance.map((item) => `- \`${item.id}\`: ${item.summary}`).join("\n");
|
|
616
667
|
return `${[
|
|
@@ -620,7 +671,7 @@ function childBody(repository: RepositoryInfo, operation: Extract<TrackerOperati
|
|
|
620
671
|
marker("initiative-id", operation.initiative_id),
|
|
621
672
|
marker("slice-id", operation.slice_id),
|
|
622
673
|
marker("task-id", operation.task_id),
|
|
623
|
-
].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| 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`;
|
|
674
|
+
].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`;
|
|
624
675
|
}
|
|
625
676
|
|
|
626
677
|
async function upsertTask(
|
|
@@ -655,7 +706,7 @@ async function upsertTask(
|
|
|
655
706
|
if (!("owned" in ownership)) return ownership;
|
|
656
707
|
blockers.push(blocker.issue);
|
|
657
708
|
}
|
|
658
|
-
const body = childBody(source.repository, operation);
|
|
709
|
+
const body = childBody(source.repository, operation, parent.issue);
|
|
659
710
|
const oversized = bodyLimitFailure(operation.op, body, MAX_TERMINAL_SUFFIX_BYTES);
|
|
660
711
|
if (oversized) return oversized;
|
|
661
712
|
const title = issueTitle(`${operation.initiative_id}/${operation.slice_id}`, operation.projection?.result ?? operation.goal);
|
|
@@ -870,6 +921,7 @@ function normalizeInitiativeProjection(value: InitiativeProjection | undefined):
|
|
|
870
921
|
return {
|
|
871
922
|
problem: value.problem === undefined ? undefined : projectionText(value.problem, "projection.problem"),
|
|
872
923
|
result: value.result === undefined ? undefined : projectionText(value.result, "projection.result"),
|
|
924
|
+
design: value.design === undefined ? undefined : projectionText(value.design, "projection.design"),
|
|
873
925
|
decisions: normalizedProjectionList(value.decisions, "projection.decisions", 500),
|
|
874
926
|
testing_strategy: value.testing_strategy === undefined ? undefined : projectionText(value.testing_strategy, "projection.testing_strategy"),
|
|
875
927
|
out_of_scope: normalizedProjectionList(value.out_of_scope, "projection.out_of_scope", 500),
|
|
@@ -949,12 +1001,265 @@ export async function runGithubTrackerOperation(
|
|
|
949
1001
|
}
|
|
950
1002
|
}
|
|
951
1003
|
|
|
952
|
-
function
|
|
953
|
-
|
|
954
|
-
|
|
1004
|
+
function publicationResult(
|
|
1005
|
+
status: TrackerStatus,
|
|
1006
|
+
message: string,
|
|
1007
|
+
initiative?: GithubTrackerResult,
|
|
1008
|
+
tasks: GithubInitiativePublicationResult["tasks"] = [],
|
|
1009
|
+
execution?: GithubInitiativePublicationResult["execution"],
|
|
1010
|
+
): GithubInitiativePublicationResult {
|
|
1011
|
+
return {
|
|
1012
|
+
contract: "immune_brain/github_initiative_publication/v1",
|
|
1013
|
+
operation: "publish-initiative",
|
|
1014
|
+
status,
|
|
1015
|
+
...(initiative ? { initiative } : {}),
|
|
1016
|
+
tasks,
|
|
1017
|
+
...(execution ? { execution } : {}),
|
|
1018
|
+
message: redactGithubDiagnostic(message),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
interface PreparedPublicationTask {
|
|
1023
|
+
operation: Extract<TrackerOperation, { op: "upsert-task" }>;
|
|
1024
|
+
intent_path: string;
|
|
1025
|
+
intent_content_hash: string;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function publicationPlan(operations: Array<Extract<TrackerOperation, { op: "upsert-task" }>>): {
|
|
1029
|
+
order: Array<Extract<TrackerOperation, { op: "upsert-task" }>>;
|
|
1030
|
+
parallel_groups: string[][];
|
|
1031
|
+
} {
|
|
1032
|
+
const remaining = new Set(operations.map((operation) => operation.task_id));
|
|
1033
|
+
const done = new Set<string>();
|
|
1034
|
+
const order: Array<Extract<TrackerOperation, { op: "upsert-task" }>> = [];
|
|
1035
|
+
const parallelGroups: string[][] = [];
|
|
1036
|
+
while (remaining.size) {
|
|
1037
|
+
const ready = operations.filter((operation) => remaining.has(operation.task_id)
|
|
1038
|
+
&& (operation.projection?.blocked_by ?? []).every((taskId) => done.has(taskId)));
|
|
1039
|
+
if (!ready.length) throw new Error("Initiative Task dependencies must form an acyclic graph");
|
|
1040
|
+
parallelGroups.push(ready.map((operation) => operation.task_id));
|
|
1041
|
+
for (const operation of ready) {
|
|
1042
|
+
remaining.delete(operation.task_id);
|
|
1043
|
+
done.add(operation.task_id);
|
|
1044
|
+
order.push(operation);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return { order, parallel_groups: parallelGroups };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function preflightPublication(root: string, input: InitiativePublicationInput): {
|
|
1051
|
+
initiative: Extract<TrackerOperation, { op: "create-initiative" }>;
|
|
1052
|
+
order: Array<Extract<TrackerOperation, { op: "upsert-task" }>>;
|
|
1053
|
+
parallel_groups: string[][];
|
|
1054
|
+
intent_bindings: Map<string, Pick<PreparedPublicationTask, "intent_path" | "intent_content_hash">>;
|
|
1055
|
+
} {
|
|
1056
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("publication must be an object");
|
|
1057
|
+
if (!Array.isArray(input.tasks) || input.tasks.length < 2)
|
|
1058
|
+
throw new Error("a complete Initiative publication requires at least two Tasks");
|
|
1059
|
+
if (!input.projection || typeof input.projection !== "object" || Array.isArray(input.projection))
|
|
1060
|
+
throw new Error("publication projection must be an object");
|
|
1061
|
+
for (const field of ["problem", "result", "design"] as const) {
|
|
1062
|
+
if (input.projection[field] === undefined)
|
|
1063
|
+
throw new Error(`a complete Initiative publication requires projection.${field}`);
|
|
1064
|
+
}
|
|
1065
|
+
const publications = input.tasks.map((task, index) => {
|
|
1066
|
+
if (!task || typeof task !== "object" || Array.isArray(task)) throw new Error(`tasks[${index}] must be an object`);
|
|
1067
|
+
if (typeof task.intent !== "string") throw new Error(`tasks[${index}].intent must be a string`);
|
|
1068
|
+
return taskPublication(root, input.initiative_id, task.slice_id, task.intent, task.projection);
|
|
1069
|
+
});
|
|
1070
|
+
const operations = publications.map((publication) => publication.operation);
|
|
1071
|
+
const taskIds = new Set<string>();
|
|
1072
|
+
const sliceIds = new Set<string>();
|
|
1073
|
+
for (const operation of operations) {
|
|
1074
|
+
if (taskIds.has(operation.task_id)) throw new Error(`duplicate Task id: ${operation.task_id}`);
|
|
1075
|
+
if (sliceIds.has(operation.slice_id)) throw new Error(`duplicate Slice id: ${operation.slice_id}`);
|
|
1076
|
+
taskIds.add(operation.task_id);
|
|
1077
|
+
sliceIds.add(operation.slice_id);
|
|
1078
|
+
}
|
|
1079
|
+
for (const operation of operations) {
|
|
1080
|
+
for (const blocker of operation.projection?.blocked_by ?? []) {
|
|
1081
|
+
if (!taskIds.has(blocker)) throw new Error(`Task ${operation.task_id} depends on ${blocker}, which is outside the complete Initiative batch`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
const plan = publicationPlan(operations);
|
|
1085
|
+
const initiative = validateOperation({
|
|
1086
|
+
op: "create-initiative",
|
|
1087
|
+
initiative_id: input.initiative_id,
|
|
1088
|
+
goal: input.goal,
|
|
1089
|
+
projection: input.projection,
|
|
1090
|
+
slices: operations.map((operation) => ({
|
|
1091
|
+
id: operation.slice_id,
|
|
1092
|
+
goal: operation.goal,
|
|
1093
|
+
result: operation.projection?.result,
|
|
1094
|
+
blocked_by: operation.projection?.blocked_by,
|
|
1095
|
+
})),
|
|
1096
|
+
}) as Extract<TrackerOperation, { op: "create-initiative" }>;
|
|
1097
|
+
const intentBindings = new Map(publications.map((publication) => [publication.operation.task_id, {
|
|
1098
|
+
intent_path: publication.intent_path,
|
|
1099
|
+
intent_content_hash: publication.intent_content_hash,
|
|
1100
|
+
}]));
|
|
1101
|
+
return { initiative, ...plan, intent_bindings: intentBindings };
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function publicationIntentDrift(
|
|
1105
|
+
root: string,
|
|
1106
|
+
bindings: Map<string, Pick<PreparedPublicationTask, "intent_path" | "intent_content_hash">>,
|
|
1107
|
+
taskIds: Iterable<string> = bindings.keys(),
|
|
1108
|
+
): string | null {
|
|
1109
|
+
for (const taskId of taskIds) {
|
|
1110
|
+
const expected = bindings.get(taskId);
|
|
1111
|
+
if (!expected) return `TaskIntent ${taskId} is missing its publication binding`;
|
|
1112
|
+
try {
|
|
1113
|
+
const current = readTaskIntent(root, taskId);
|
|
1114
|
+
if (current.intent_ref.path !== expected.intent_path || current.content_hash !== expected.intent_content_hash)
|
|
1115
|
+
return `TaskIntent ${taskId} changed during Initiative publication`;
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
return `TaskIntent ${taskId} became unreadable during Initiative publication: ${error instanceof Error ? error.message : String(error)}`;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function publicationIssueDrift(expected: {
|
|
1124
|
+
issue_number?: number;
|
|
1125
|
+
issue_url?: string;
|
|
1126
|
+
node_id?: string;
|
|
1127
|
+
}, actual: GithubIssue, title: string, body: string, label: string): string | null {
|
|
1128
|
+
if (expected.issue_number !== actual.number || expected.issue_url !== actual.url || expected.node_id !== String(actual.id))
|
|
1129
|
+
return `${label} identity changed during Initiative publication`;
|
|
1130
|
+
if (actual.state !== "open") return `${label} is no longer open`;
|
|
1131
|
+
if (actual.title !== title || actual.body !== body) return `${label} content changed during Initiative publication`;
|
|
1132
|
+
return null;
|
|
955
1133
|
}
|
|
956
1134
|
|
|
957
|
-
function
|
|
1135
|
+
export async function runGithubInitiativePublication(
|
|
1136
|
+
root: string,
|
|
1137
|
+
input: InitiativePublicationInput,
|
|
1138
|
+
gh: GhTransport = createGhTransport(),
|
|
1139
|
+
): Promise<GithubInitiativePublicationResult> {
|
|
1140
|
+
const absoluteRoot = resolve(root);
|
|
1141
|
+
let prepared: ReturnType<typeof preflightPublication>;
|
|
1142
|
+
try {
|
|
1143
|
+
prepared = preflightPublication(absoluteRoot, input);
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
return publicationResult("permanent_failure", error instanceof Error ? error.message : String(error));
|
|
1146
|
+
}
|
|
1147
|
+
const conflict = carrierConflict(absoluteRoot, "create-initiative", prepared.initiative.initiative_id);
|
|
1148
|
+
if (conflict) return publicationResult(conflict.status, conflict.message, conflict);
|
|
1149
|
+
const initial = await snapshot(absoluteRoot, gh, "create-initiative");
|
|
1150
|
+
if ("contract" in initial) return publicationResult(initial.status, initial.message, initial);
|
|
1151
|
+
const initialParent = initiativeLookup(initial.issues, initial.repository.id, prepared.initiative.initiative_id);
|
|
1152
|
+
if (initialParent.kind === "ambiguous") return publicationResult("ambiguous_remote_state", initialParent.message);
|
|
1153
|
+
const parentForPreflight = initialParent.kind === "found" ? initialParent.issue : {
|
|
1154
|
+
id: Number.MAX_SAFE_INTEGER,
|
|
1155
|
+
number: Number.MAX_SAFE_INTEGER,
|
|
1156
|
+
url: `https://github.com/${initial.repository.name_with_owner}/issues/${Number.MAX_SAFE_INTEGER}`,
|
|
1157
|
+
title: "",
|
|
1158
|
+
body: "",
|
|
1159
|
+
state: "open" as const,
|
|
1160
|
+
state_reason: null,
|
|
1161
|
+
};
|
|
1162
|
+
const parentBodyFailure = bodyLimitFailure("create-initiative", createInitiativeBody(initial.repository, prepared.initiative));
|
|
1163
|
+
if (parentBodyFailure) return publicationResult(parentBodyFailure.status, parentBodyFailure.message, parentBodyFailure);
|
|
1164
|
+
for (const operation of prepared.order) {
|
|
1165
|
+
const childFailure = bodyLimitFailure("upsert-task", childBody(initial.repository, operation, parentForPreflight), MAX_TERMINAL_SUFFIX_BYTES);
|
|
1166
|
+
if (childFailure) return publicationResult(childFailure.status, childFailure.message, childFailure);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const beforeParentWrite = publicationIntentDrift(absoluteRoot, prepared.intent_bindings);
|
|
1170
|
+
if (beforeParentWrite) return publicationResult("ambiguous_remote_state", beforeParentWrite);
|
|
1171
|
+
const parentResult = await runGithubTrackerOperation(absoluteRoot, prepared.initiative, gh);
|
|
1172
|
+
if (!isSuccessfulTrackerStatus(parentResult.status))
|
|
1173
|
+
return publicationResult(parentResult.status, parentResult.message, parentResult);
|
|
1174
|
+
const taskResults: GithubInitiativePublicationResult["tasks"] = [];
|
|
1175
|
+
for (const operation of prepared.order) {
|
|
1176
|
+
const intentDrift = publicationIntentDrift(absoluteRoot, prepared.intent_bindings, [operation.task_id]);
|
|
1177
|
+
if (intentDrift) return publicationResult("ambiguous_remote_state", intentDrift, parentResult, taskResults);
|
|
1178
|
+
const taskResult = await runGithubTrackerOperation(absoluteRoot, operation, gh);
|
|
1179
|
+
taskResults.push({
|
|
1180
|
+
task_id: operation.task_id,
|
|
1181
|
+
slice_id: operation.slice_id,
|
|
1182
|
+
status: taskResult.status,
|
|
1183
|
+
...(taskResult.issue_number === undefined ? {} : { issue_number: taskResult.issue_number }),
|
|
1184
|
+
...(taskResult.issue_url === undefined ? {} : { issue_url: taskResult.issue_url }),
|
|
1185
|
+
...(taskResult.node_id === undefined ? {} : { node_id: taskResult.node_id }),
|
|
1186
|
+
});
|
|
1187
|
+
if (!isSuccessfulTrackerStatus(taskResult.status))
|
|
1188
|
+
return publicationResult(taskResult.status, taskResult.message, parentResult, taskResults);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const finalIntentDrift = publicationIntentDrift(absoluteRoot, prepared.intent_bindings);
|
|
1192
|
+
if (finalIntentDrift) return publicationResult("ambiguous_remote_state", finalIntentDrift, parentResult, taskResults);
|
|
1193
|
+
const finalSource = await snapshot(absoluteRoot, gh, "upsert-task");
|
|
1194
|
+
if ("contract" in finalSource) return publicationResult(finalSource.status, finalSource.message, parentResult, taskResults);
|
|
1195
|
+
const parent = initiativeLookup(finalSource.issues, finalSource.repository.id, prepared.initiative.initiative_id);
|
|
1196
|
+
if (parent.kind !== "found") return publicationResult("ambiguous_remote_state", parent.kind === "ambiguous" ? parent.message : "published Initiative Parent disappeared", parentResult, taskResults);
|
|
1197
|
+
const parentDrift = publicationIssueDrift(
|
|
1198
|
+
parentResult,
|
|
1199
|
+
parent.issue,
|
|
1200
|
+
issueTitle(prepared.initiative.initiative_id, prepared.initiative.projection?.result ?? prepared.initiative.goal),
|
|
1201
|
+
createInitiativeBody(finalSource.repository, prepared.initiative),
|
|
1202
|
+
"Initiative Parent",
|
|
1203
|
+
);
|
|
1204
|
+
if (parentDrift) return publicationResult("ambiguous_remote_state", parentDrift, parentResult, taskResults);
|
|
1205
|
+
const resultByTask = new Map(taskResults.map((task) => [task.task_id, task]));
|
|
1206
|
+
const expectedNumbers: number[] = [];
|
|
1207
|
+
for (const operation of prepared.order) {
|
|
1208
|
+
const child = ownedTaskLookup(finalSource.issues, finalSource.repository.id, operation.task_id, operation.initiative_id, operation.slice_id);
|
|
1209
|
+
if (child.kind !== "found") return publicationResult("ambiguous_remote_state", child.kind === "ambiguous" ? child.message : `published Task ${operation.task_id} disappeared`, parentResult, taskResults);
|
|
1210
|
+
const childResult = resultByTask.get(operation.task_id);
|
|
1211
|
+
if (!childResult) return publicationResult("ambiguous_remote_state", `published Task ${operation.task_id} has no batch result`, parentResult, taskResults);
|
|
1212
|
+
const childDrift = publicationIssueDrift(
|
|
1213
|
+
childResult,
|
|
1214
|
+
child.issue,
|
|
1215
|
+
issueTitle(`${operation.initiative_id}/${operation.slice_id}`, operation.projection?.result ?? operation.goal),
|
|
1216
|
+
childBody(finalSource.repository, operation, parent.issue),
|
|
1217
|
+
`Task ${operation.task_id}`,
|
|
1218
|
+
);
|
|
1219
|
+
if (childDrift) return publicationResult("ambiguous_remote_state", childDrift, parentResult, taskResults);
|
|
1220
|
+
expectedNumbers.push(child.issue.number);
|
|
1221
|
+
const ownership = await confirmTerminalOwnership(absoluteRoot, gh, "upsert-task", finalSource, child.issue);
|
|
1222
|
+
if (!("owned" in ownership)) return publicationResult(ownership.status, ownership.message, parentResult, taskResults);
|
|
1223
|
+
const blockers: GithubIssue[] = [];
|
|
1224
|
+
for (const blockerId of operation.projection?.blocked_by ?? []) {
|
|
1225
|
+
const blocker = taskLookup(finalSource.issues, finalSource.repository.id, blockerId);
|
|
1226
|
+
if (blocker.kind !== "found") return publicationResult("ambiguous_remote_state", `published blocker ${blockerId} disappeared`, parentResult, taskResults);
|
|
1227
|
+
blockers.push(blocker.issue);
|
|
1228
|
+
}
|
|
1229
|
+
const dependencies = await confirmBlockedBy(absoluteRoot, gh, "upsert-task", finalSource.repository, child.issue.number, blockers);
|
|
1230
|
+
if (!("complete" in dependencies)) return publicationResult(dependencies.status, dependencies.message, parentResult, taskResults);
|
|
1231
|
+
if (!dependencies.complete) return publicationResult("ambiguous_remote_state", `Task ${operation.task_id} has incomplete blocking relations`, parentResult, taskResults);
|
|
1232
|
+
}
|
|
1233
|
+
const attached = await readSubIssueNumbers(absoluteRoot, gh, "upsert-task", finalSource.repository, parent.issue.number);
|
|
1234
|
+
if (!Array.isArray(attached)) return publicationResult(attached.status, attached.message, parentResult, taskResults);
|
|
1235
|
+
const sortedAttached = [...attached].sort((left, right) => left - right);
|
|
1236
|
+
const sortedExpected = [...expectedNumbers].sort((left, right) => left - right);
|
|
1237
|
+
if (sortedAttached.length !== sortedExpected.length || sortedAttached.some((number, index) => number !== sortedExpected[index]))
|
|
1238
|
+
return publicationResult("ambiguous_remote_state", "Initiative Parent Sub-issues do not match the complete publication batch", parentResult, taskResults);
|
|
1239
|
+
const statuses = [parentResult.status, ...taskResults.map((task) => task.status)];
|
|
1240
|
+
const status: TrackerStatus = statuses.every((item) => item === "created")
|
|
1241
|
+
? "created"
|
|
1242
|
+
: statuses.every((item) => item === "already_current") ? "already_current" : "updated";
|
|
1243
|
+
const issueByTask = new Map(taskResults.map((task) => [task.task_id, task.issue_number]));
|
|
1244
|
+
if ([...issueByTask.values()].some((number) => number === undefined))
|
|
1245
|
+
return publicationResult("ambiguous_remote_state", "published Task result is missing an Issue number", parentResult, taskResults);
|
|
1246
|
+
const issueNumber = (taskId: string): number => issueByTask.get(taskId)!;
|
|
1247
|
+
const firstTaskId = prepared.order[0].task_id;
|
|
1248
|
+
return publicationResult(status, "complete Initiative Parent, Children, and dependency graph published", parentResult, taskResults, {
|
|
1249
|
+
recommended_first_task_id: firstTaskId,
|
|
1250
|
+
recommended_first_issue_number: issueNumber(firstTaskId),
|
|
1251
|
+
order: prepared.order.map((operation) => operation.task_id),
|
|
1252
|
+
issue_order: prepared.order.map((operation) => issueNumber(operation.task_id)),
|
|
1253
|
+
parallel_groups: prepared.parallel_groups,
|
|
1254
|
+
parallel_issue_groups: prepared.parallel_groups.map((group) => group.map(issueNumber)),
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function isSuccessfulTrackerStatus(status: TrackerStatus): boolean {
|
|
1259
|
+
return status === "created" || status === "updated" || status === "already_current";
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function taskPublication(root: string, initiativeId: string, sliceId: string, intentPath: string, projection?: TaskProjection): PreparedPublicationTask {
|
|
958
1263
|
const absoluteRoot = resolve(root);
|
|
959
1264
|
const absolutePath = resolve(absoluteRoot, intentPath);
|
|
960
1265
|
const rel = relative(absoluteRoot, absolutePath);
|
|
@@ -966,14 +1271,18 @@ function taskPublication(root: string, initiativeId: string, sliceId: string, in
|
|
|
966
1271
|
if (read.intent_ref.path !== rel) throw new Error("TaskIntent path must match its canonical sidecar path");
|
|
967
1272
|
const intent = read.intent;
|
|
968
1273
|
return {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1274
|
+
operation: validateOperation({
|
|
1275
|
+
op: "upsert-task",
|
|
1276
|
+
initiative_id: initiativeId,
|
|
1277
|
+
task_id: intent.task_id,
|
|
1278
|
+
slice_id: sliceId,
|
|
1279
|
+
goal: intent.goal,
|
|
1280
|
+
risk: intent.risk,
|
|
1281
|
+
acceptance: intent.acceptance.map((item) => ({ id: item.id, summary: item.assertion })),
|
|
1282
|
+
projection,
|
|
1283
|
+
}) as Extract<TrackerOperation, { op: "upsert-task" }>,
|
|
1284
|
+
intent_path: read.intent_ref.path,
|
|
1285
|
+
intent_content_hash: read.content_hash,
|
|
977
1286
|
};
|
|
978
1287
|
}
|
|
979
1288
|
|
|
@@ -985,29 +1294,16 @@ export async function runGithubTrackerCli(
|
|
|
985
1294
|
const op = args[0];
|
|
986
1295
|
if (!args.includes("--json"))
|
|
987
1296
|
return { stdout: "", stderr: "invalid_tracker_command: --json is required\n", returncode: 2 };
|
|
988
|
-
let operation: TrackerOperation;
|
|
989
1297
|
try {
|
|
990
|
-
if (op
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
} else if (op === "upsert-task" && (args.length === 8 || args.length === 10)) {
|
|
1000
|
-
const initiative = valueAfter(args, "--initiative-id");
|
|
1001
|
-
const slice = valueAfter(args, "--slice-id");
|
|
1002
|
-
const intent = valueAfter(args, "--intent");
|
|
1003
|
-
if (!initiative || !slice || !intent)
|
|
1004
|
-
throw new Error("upsert-task requires --initiative-id, --slice-id, and --intent");
|
|
1005
|
-
const rawProjection = valueAfter(args, "--projection-json");
|
|
1006
|
-
const projection = rawProjection ? JSON.parse(rawProjection) as TaskProjection : undefined;
|
|
1007
|
-
operation = taskPublication(root, initiative, slice, intent, projection);
|
|
1008
|
-
} else {
|
|
1009
|
-
throw new Error("use create-initiative --stdin --json or upsert-task --initiative-id <id> --slice-id <id> --intent <path> [--projection-json <json>] --json");
|
|
1010
|
-
}
|
|
1298
|
+
if (op !== "publish-initiative" || args.length !== 3 || args[1] !== "--stdin")
|
|
1299
|
+
throw new Error("use publish-initiative --stdin --json");
|
|
1300
|
+
const raw = JSON.parse((options.stdin ?? (() => readFileSync(0, "utf8")))()) as InitiativePublicationInput;
|
|
1301
|
+
const published = await runGithubInitiativePublication(root, raw, options.gh);
|
|
1302
|
+
return {
|
|
1303
|
+
stdout: `${JSON.stringify(published, null, 2)}\n`,
|
|
1304
|
+
stderr: "",
|
|
1305
|
+
returncode: isSuccessfulTrackerStatus(published.status) ? 0 : 1,
|
|
1306
|
+
};
|
|
1011
1307
|
} catch (error) {
|
|
1012
1308
|
return {
|
|
1013
1309
|
stdout: "",
|
|
@@ -1015,10 +1311,4 @@ export async function runGithubTrackerCli(
|
|
|
1015
1311
|
returncode: 2,
|
|
1016
1312
|
};
|
|
1017
1313
|
}
|
|
1018
|
-
const projected = await runGithubTrackerOperation(root, operation, options.gh);
|
|
1019
|
-
return {
|
|
1020
|
-
stdout: `${JSON.stringify(projected, null, 2)}\n`,
|
|
1021
|
-
stderr: "",
|
|
1022
|
-
returncode: projected.status === "permanent_failure" || projected.status === "ambiguous_remote_state" ? 1 : 0,
|
|
1023
|
-
};
|
|
1024
1314
|
}
|
|
@@ -261,11 +261,10 @@ async function main(argv: string[]): Promise<number> {
|
|
|
261
261
|
{
|
|
262
262
|
name: "imm-tracker",
|
|
263
263
|
description:
|
|
264
|
-
"Opt-in, one-way GitHub Issue projection.
|
|
264
|
+
"Opt-in, one-way GitHub Issue projection. Publishes one complete Parent/Child graph and execution recommendation. Never grants or consumes Kernel authority.",
|
|
265
265
|
json_output: true,
|
|
266
266
|
examples: [
|
|
267
|
-
"imm-tracker
|
|
268
|
-
"imm-tracker upsert-task --initiative-id <id> --slice-id <id> --intent docs/plans/<task-id>.intent.json --json",
|
|
267
|
+
"imm-tracker publish-initiative --stdin --json",
|
|
269
268
|
],
|
|
270
269
|
},
|
|
271
270
|
],
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: imm-agent-doc-maintain
|
|
3
|
+
description: Use to minimize tracked AGENTS.md, CLAUDE.md, and GEMINI.md files to necessary non-discoverable context after an explicit, hash-bound, user-approved manifest; never installs or validates project contracts.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Immune-Brain: Agent Doc Maintain
|
|
7
|
+
|
|
8
|
+
Load [`../../dist/imm-agent-doc-maintain.md`](../../dist/imm-agent-doc-maintain.md),
|
|
9
|
+
then minimize tracked agent instruction files to necessary non-discoverable
|
|
10
|
+
context. This is a standalone host-native maintenance entry, not a Managed Path
|
|
11
|
+
continuation and not an `imm-loop` internal-role dispatch.
|
|
@@ -14,7 +14,7 @@ Agent envelope exactly. Brainstorm and Planner use the same Tool for bounded
|
|
|
14
14
|
`arch-explorer` and explicit-lens `advisory-reviewer` dispatches. Loop may
|
|
15
15
|
dispatch `compounder` only when a closed Step supplies structured evidence for
|
|
16
16
|
a reusable Learning; routine work without that evidence returns `next: none`
|
|
17
|
-
and creates no Learning. Do not discover or load a Pi Skill for these roles. The Managed Path public entries remain `imm-brainstorm`, `imm-planner`, and `imm-loop`; standalone `imm-pr-fix` and `imm-doc-
|
|
17
|
+
and creates no Learning. Do not discover or load a Pi Skill for these roles. The Managed Path public entries remain `imm-brainstorm`, `imm-planner`, and `imm-loop`; standalone `imm-pr-fix`, `imm-doc-prune`, and `imm-agent-doc-maintain` are host-native and are never dispatched as the Loop role.
|
|
18
18
|
Subagent Dispatch Protocol](../../dist/docs/reference/subagent-dispatch-protocol.md#authorization-authority).
|
|
19
19
|
All internal Agent dispatch envelopes use `run_in_background: false` and
|
|
20
20
|
return a direct result to the Parent before any workflow mutation.
|