immune-brain 5.2.0 → 5.5.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 +4 -4
- package/README.zh-CN.md +4 -4
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
- package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
- package/plugins/immune-brain/dist/BASELINE.md +17 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16145 -12745
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +63 -19
- package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
- package/plugins/immune-brain/dist/imm-run.md +89 -344
- package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
- package/plugins/immune-brain/hooks/hooks.json +22 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
- package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +10 -26
- package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
- package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
- package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
- package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +30 -20
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
- package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
- package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
- package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
- package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
- package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
- package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
- package/plugins/immune-brain/runtime/plan_core.ts +15 -8
- package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
- package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
- package/plugins/immune-brain/skills/BASELINE.md +17 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -3
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -2
- package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { projectTaskStatus } from "../assurance/coordinator";
|
|
6
|
+
import { readTaskIntent } from "../kernel/intent";
|
|
7
|
+
import { readAuditTaskPair, readSecureProjectFile, readSettledTaskEvidence, revisionForContent } from "../kernel/storage";
|
|
8
|
+
import { auditRunRecordPath, auditRunTerminalProofPath } from "../kernel/storage_paths";
|
|
9
|
+
import { captureGitTaskRevisionSnapshot, pathMatchesScope } from "../workspace_scope";
|
|
10
|
+
import { readParentConsolidationReceipt } from "./batch_state";
|
|
11
|
+
import { parseFinalVerification } from "./batch_final_verification";
|
|
12
|
+
|
|
13
|
+
export interface DeliveryRun { task_id: string; run_id: string }
|
|
14
|
+
export interface DeliveryConsolidation extends DeliveryRun { source_runs: DeliveryRun[] }
|
|
15
|
+
export const consolidationSchema = {
|
|
16
|
+
type: "object", additionalProperties: false, required: ["task_id", "run_id", "source_runs"],
|
|
17
|
+
properties: {
|
|
18
|
+
task_id: { type: "string" }, run_id: { type: "string" },
|
|
19
|
+
source_runs: { type: "array", minItems: 1, maxItems: 16, items: {
|
|
20
|
+
type: "object", additionalProperties: false, required: ["task_id", "run_id"],
|
|
21
|
+
properties: { task_id: { type: "string" }, run_id: { type: "string" } },
|
|
22
|
+
} },
|
|
23
|
+
},
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
function object(value: unknown, keys: string[]): Record<string, unknown> {
|
|
27
|
+
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
28
|
+
Object.keys(value).sort().join() !== [...keys].sort().join()) throw new Error("invalid delivery_consolidation shape");
|
|
29
|
+
return value as Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
function run(value: unknown): DeliveryRun {
|
|
32
|
+
const raw = object(value, ["task_id", "run_id"]);
|
|
33
|
+
if (typeof raw.task_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(raw.task_id) ||
|
|
34
|
+
typeof raw.run_id !== "string" || !/^run-[A-Za-z0-9][A-Za-z0-9._-]{0,123}$/.test(raw.run_id))
|
|
35
|
+
throw new Error("invalid consolidation task/run identity");
|
|
36
|
+
return { task_id: raw.task_id, run_id: raw.run_id };
|
|
37
|
+
}
|
|
38
|
+
export function parseDeliveryConsolidation(value: unknown): DeliveryConsolidation | undefined {
|
|
39
|
+
if (value === undefined) return undefined;
|
|
40
|
+
const raw = object(value, ["task_id", "run_id", "source_runs"]);
|
|
41
|
+
const final = run({ task_id: raw.task_id, run_id: raw.run_id });
|
|
42
|
+
if (!Array.isArray(raw.source_runs) || !raw.source_runs.length || raw.source_runs.length > 16)
|
|
43
|
+
throw new Error("consolidation requires 1–16 source runs");
|
|
44
|
+
const sources = raw.source_runs.map(run).sort((a, b) => a.task_id.localeCompare(b.task_id));
|
|
45
|
+
if (new Set([final.task_id, ...sources.map(source => source.task_id)]).size !== sources.length + 1)
|
|
46
|
+
throw new Error("duplicate/self consolidation selection");
|
|
47
|
+
return { ...final, source_runs: sources };
|
|
48
|
+
}
|
|
49
|
+
export function consolidationDigest(value: unknown): string {
|
|
50
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
|
|
51
|
+
}
|
|
52
|
+
function git(root: string, argv: string[]): string {
|
|
53
|
+
const result = spawnSync("git", ["-C", root, ...argv], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
|
54
|
+
if (result.status !== 0) throw new Error(`consolidation Git inspection failed: ${argv[0]}`);
|
|
55
|
+
return result.stdout;
|
|
56
|
+
}
|
|
57
|
+
export interface ConsolidationEnvelope {
|
|
58
|
+
initiative_slug: string;
|
|
59
|
+
plan_digest: string;
|
|
60
|
+
selection: DeliveryConsolidation;
|
|
61
|
+
parent_tasks: Array<{ task_id: string; intent_hash: string }>;
|
|
62
|
+
branch: string;
|
|
63
|
+
head: string;
|
|
64
|
+
final_commands: string[];
|
|
65
|
+
/** All source approvals are historical; only the final run approves the composed bytes. */
|
|
66
|
+
runs: Array<{ task_id: string; run_id: string; record_hash: string; proof_hash: string;
|
|
67
|
+
intent_hash: string; review_revision: unknown }>;
|
|
68
|
+
delivery_diff_hash: string;
|
|
69
|
+
manifest: Array<{ path: string; mode: string | null; oid: string | null }>;
|
|
70
|
+
}
|
|
71
|
+
export interface ConsolidationReceipt {
|
|
72
|
+
contract: "assurance_kernel/parent_delivery_consolidation/v1";
|
|
73
|
+
envelope: ConsolidationEnvelope;
|
|
74
|
+
digest: string;
|
|
75
|
+
commit: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseConsolidationReceipt(value: unknown): ConsolidationReceipt {
|
|
79
|
+
const raw = object(value, ["contract", "envelope", "digest", "commit"]);
|
|
80
|
+
if (raw.contract !== "assurance_kernel/parent_delivery_consolidation/v1" ||
|
|
81
|
+
(typeof raw.commit !== "string" && raw.commit !== null) ||
|
|
82
|
+
(typeof raw.commit === "string" && !/^[a-f0-9]{40,64}$/.test(raw.commit))) throw new Error("invalid consolidation receipt");
|
|
83
|
+
const e = object(raw.envelope, ["initiative_slug", "plan_digest", "selection", "parent_tasks", "branch", "head",
|
|
84
|
+
"final_commands", "runs", "delivery_diff_hash", "manifest"]);
|
|
85
|
+
const selection = parseDeliveryConsolidation(e.selection)!;
|
|
86
|
+
if (JSON.stringify(selection) !== JSON.stringify(e.selection) || typeof e.initiative_slug !== "string" ||
|
|
87
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(e.initiative_slug) || typeof e.branch !== "string" || !e.branch ||
|
|
88
|
+
typeof e.head !== "string" || !/^[a-f0-9]{40,64}$/.test(e.head) ||
|
|
89
|
+
!Array.isArray(e.parent_tasks) || !e.parent_tasks.length || !Array.isArray(e.runs) ||
|
|
90
|
+
!Array.isArray(e.manifest) || !e.manifest.length || !Array.isArray(e.final_commands) ||
|
|
91
|
+
e.final_commands.some(c => typeof c !== "string") || typeof raw.digest !== "string" ||
|
|
92
|
+
raw.digest !== consolidationDigest(e)) throw new Error("consolidation receipt digest/shape mismatch");
|
|
93
|
+
const hash = (value: unknown) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
94
|
+
if (!hash(e.plan_digest) || !hash(e.delivery_diff_hash) || !parseFinalVerification(e.final_commands)?.length)
|
|
95
|
+
throw new Error("invalid consolidation proof/commands");
|
|
96
|
+
const expectedRuns = [selection, ...selection.source_runs].map(({ task_id, run_id }) => ({ task_id, run_id }));
|
|
97
|
+
if (e.runs.length !== expectedRuns.length) throw new Error("consolidation run contributions missing");
|
|
98
|
+
for (let index = 0; index < e.runs.length; index++) {
|
|
99
|
+
const binding = object(e.runs[index], ["task_id", "run_id", "record_hash", "proof_hash", "intent_hash", "review_revision"]);
|
|
100
|
+
if (JSON.stringify(run({ task_id: binding.task_id, run_id: binding.run_id })) !== JSON.stringify(expectedRuns[index]) ||
|
|
101
|
+
!hash(binding.record_hash) || !hash(binding.proof_hash) || !hash(binding.intent_hash))
|
|
102
|
+
throw new Error("consolidation run contribution binding mismatch");
|
|
103
|
+
}
|
|
104
|
+
const parentIds = e.parent_tasks.map(value => {
|
|
105
|
+
const task = object(value, ["task_id", "intent_hash"]);
|
|
106
|
+
if (typeof task.task_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(task.task_id) || !hash(task.intent_hash))
|
|
107
|
+
throw new Error("invalid consolidation Parent binding");
|
|
108
|
+
return task.task_id;
|
|
109
|
+
});
|
|
110
|
+
if (new Set(parentIds).size !== parentIds.length) throw new Error("duplicate consolidation Parent contribution");
|
|
111
|
+
const paths = e.manifest.map(value => {
|
|
112
|
+
const entry = object(value, ["path", "mode", "oid"]);
|
|
113
|
+
if (typeof entry.path !== "string" || !entry.path || entry.path.startsWith("/") || entry.path.includes("\\") ||
|
|
114
|
+
entry.path.split("/").some(part => !part || part === "." || part === "..") ||
|
|
115
|
+
(entry.mode !== "100644" && entry.mode !== "100755" && entry.mode !== null) ||
|
|
116
|
+
(entry.oid !== null && (typeof entry.oid !== "string" || !/^[a-f0-9]{40,64}$/.test(entry.oid))) ||
|
|
117
|
+
((entry.mode === null) !== (entry.oid === null))) throw new Error("invalid consolidation file identity");
|
|
118
|
+
return entry.path;
|
|
119
|
+
});
|
|
120
|
+
if (new Set(paths).size !== paths.length || JSON.stringify(paths) !== JSON.stringify([...paths].sort()))
|
|
121
|
+
throw new Error("consolidation manifest is not canonical");
|
|
122
|
+
return raw as unknown as ConsolidationReceipt;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runEvidence(root: string, identity: DeliveryRun) {
|
|
126
|
+
const settled = readSettledTaskEvidence(root, identity.task_id);
|
|
127
|
+
if (!settled || settled.runId !== identity.run_id || settled.proof.terminal_lifecycle !== "done")
|
|
128
|
+
throw new Error(`consolidation requires the exact settled done run: ${identity.task_id}`);
|
|
129
|
+
const record = settled.record;
|
|
130
|
+
if (record.contract !== "assurance_kernel/task_record/v4") throw new Error("consolidation requires v4 immutable Git delivery records");
|
|
131
|
+
const pair = readAuditTaskPair(root, identity.task_id, identity.run_id);
|
|
132
|
+
if (!pair || JSON.stringify(pair.record) !== JSON.stringify(settled.record) || JSON.stringify(pair.proof) !== JSON.stringify(settled.proof))
|
|
133
|
+
throw new Error(`consolidation source audit mismatch: ${identity.task_id}`);
|
|
134
|
+
const current = readTaskIntent(root, identity.task_id, settled.record.intent_ref.path);
|
|
135
|
+
if (current.content_hash !== settled.record.intent_ref.content_hash) throw new Error("consolidation Intent changed");
|
|
136
|
+
const status = await projectTaskStatus(root, identity.task_id);
|
|
137
|
+
if (status.error || status.projection.run_id !== identity.run_id || !status.projection.completion_ready)
|
|
138
|
+
throw new Error(`consolidation run has no completion proof: ${identity.task_id}`);
|
|
139
|
+
const review = [...record.attestations].reverse().find(a => a.kind === "review")?.review_revision ?? null;
|
|
140
|
+
if (review) {
|
|
141
|
+
if (git(root, ["rev-parse", `${review.review_commit}^{tree}`]).trim() !== review.review_tree ||
|
|
142
|
+
git(root, ["rev-parse", `${review.review_commit}^@`]).trim() !== review.base_head)
|
|
143
|
+
throw new Error("consolidation immutable Review revision mismatch");
|
|
144
|
+
}
|
|
145
|
+
return { settled: { ...settled, record }, binding: { task_id: identity.task_id, run_id: identity.run_id,
|
|
146
|
+
record_hash: revisionForContent(readSecureProjectFile(root, auditRunRecordPath(identity.task_id, identity.run_id))),
|
|
147
|
+
proof_hash: revisionForContent(readSecureProjectFile(root, auditRunTerminalProofPath(identity.task_id, identity.run_id))),
|
|
148
|
+
intent_hash: current.content_hash, review_revision: review } };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Read-only preparation: no index, association, receipt, or Git ref writes before the native gate. */
|
|
152
|
+
export async function prepareDeliveryConsolidation(root: string, initiativeSlug: string, planDigest: string,
|
|
153
|
+
parentTaskIds: string[], selection: DeliveryConsolidation, commands: string[]): Promise<ConsolidationEnvelope> {
|
|
154
|
+
selection = parseDeliveryConsolidation(selection)!;
|
|
155
|
+
const all = [selection, ...selection.source_runs];
|
|
156
|
+
if (!selection.source_runs.some(source => parentTaskIds.includes(source.task_id)))
|
|
157
|
+
throw new Error("consolidation must select an original Parent contribution");
|
|
158
|
+
const sources = [];
|
|
159
|
+
for (const identity of all) sources.push(await runEvidence(root, identity));
|
|
160
|
+
const final = sources[0]!.settled.record;
|
|
161
|
+
if (final.intent_snapshot.risk === "routine" || !sources[0]!.binding.review_revision || !final.git_base_head)
|
|
162
|
+
throw new Error("final consolidation requires fresh QA and independent material/critical Review");
|
|
163
|
+
const snapshot = captureGitTaskRevisionSnapshot(root, final.intent_snapshot.scope_hint, final.git_base_head, selection.task_id);
|
|
164
|
+
const diffHash = consolidationDigest(snapshot);
|
|
165
|
+
if (!final.attestations.some(a => a.kind === "qa" && a.diff_hash === diffHash && a.task_revision === final.intent_snapshot.revision &&
|
|
166
|
+
a.intent_content_hash === final.intent_ref.content_hash && a.acceptance_results.length === final.intent_snapshot.acceptance.length &&
|
|
167
|
+
a.acceptance_results.every(r => r.status === "passed")) ||
|
|
168
|
+
!final.attestations.some(a => a.kind === "review" && a.diff_hash === diffHash && a.review_revision &&
|
|
169
|
+
a.task_revision === final.intent_snapshot.revision && a.intent_content_hash === final.intent_ref.content_hash))
|
|
170
|
+
throw new Error("final composed bytes do not match fresh QA/Review");
|
|
171
|
+
const finalScope = final.intent_snapshot.scope_hint;
|
|
172
|
+
for (const source of sources.slice(1)) {
|
|
173
|
+
const record = source.settled.record;
|
|
174
|
+
if (!record.git_base_head) throw new Error("source run has no Git delivery base");
|
|
175
|
+
const review = source.binding.review_revision;
|
|
176
|
+
if (!review) throw new Error("source run has no immutable Review delivery");
|
|
177
|
+
for (const path of git(root, ["diff", "--name-only", "--no-renames", "-z", record.git_base_head, review.review_commit]).split("\0").filter(Boolean))
|
|
178
|
+
if (!finalScope.some(scope => pathMatchesScope(path, scope))) throw new Error(`source contribution is not covered by final Review scope: ${path}`);
|
|
179
|
+
}
|
|
180
|
+
const auditPaths = new Set(all.flatMap(identity => [auditRunRecordPath(identity.task_id, identity.run_id), auditRunTerminalProofPath(identity.task_id, identity.run_id)]));
|
|
181
|
+
const dirty = [...new Set([...git(root, ["diff", "--name-only", "--no-renames", "-z", "HEAD"]).split("\0"),
|
|
182
|
+
...git(root, ["ls-files", "--others", "--exclude-standard", "-z"]).split("\0")].filter(Boolean))].sort();
|
|
183
|
+
const unstaged = git(root, ["diff", "--name-only", "-z"]).split("\0").filter(Boolean);
|
|
184
|
+
if (unstaged.some(path => !auditPaths.has(path))) throw new Error("consolidation has unstaged delivery drift");
|
|
185
|
+
const manifest = dirty.map(path => {
|
|
186
|
+
if (!finalScope.some(scope => pathMatchesScope(path, scope)) && !auditPaths.has(path)) throw new Error(`dirty_outside_scope: ${path}`);
|
|
187
|
+
if (!existsSync(join(root, path))) return { path, mode: null, oid: null };
|
|
188
|
+
const stat = lstatSync(join(root, path));
|
|
189
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("consolidation only accepts regular delivery files");
|
|
190
|
+
const oid = git(root, ["hash-object", "--", path]).trim();
|
|
191
|
+
const staged = git(root, ["ls-files", "--stage", "--", path]);
|
|
192
|
+
const mode = staged ? staged.split(" ")[0]! : stat.mode & 0o111 ? "100755" : "100644";
|
|
193
|
+
return { path, mode, oid };
|
|
194
|
+
});
|
|
195
|
+
const parentTasks = parentTaskIds.map(task_id => ({ task_id, intent_hash: readTaskIntent(root, task_id).content_hash }));
|
|
196
|
+
return { initiative_slug: initiativeSlug, plan_digest: planDigest, selection, parent_tasks: parentTasks,
|
|
197
|
+
branch: git(root, ["symbolic-ref", "--short", "HEAD"]).trim(), head: git(root, ["rev-parse", "HEAD"]).trim(),
|
|
198
|
+
final_commands: commands, runs: sources.map(source => source.binding), delivery_diff_hash: diffHash, manifest };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Verify committed content against the accepted manifest, not against a historical source bundle. */
|
|
202
|
+
export async function verifyConsolidatedCommit(root: string, receipt: ConsolidationReceipt): Promise<void> {
|
|
203
|
+
parseConsolidationReceipt(receipt);
|
|
204
|
+
const { envelope: e, commit } = receipt;
|
|
205
|
+
if (!commit) throw new Error("consolidation commit receipt is pending");
|
|
206
|
+
// Recovery must preserve the whole checkout, not just the reviewed scope snapshot.
|
|
207
|
+
if (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]).length)
|
|
208
|
+
throw new Error("consolidation recovery requires a clean workspace");
|
|
209
|
+
if (git(root, ["ls-files", "-v", "-z"]).split("\0").some(entry => entry && !entry.startsWith("H ")))
|
|
210
|
+
throw new Error("consolidation recovery rejects hidden index flags");
|
|
211
|
+
const message = git(root, ["show", "-s", "--format=%B", commit]);
|
|
212
|
+
if (!message.startsWith(`imm(${e.selection.task_id}): `) ||
|
|
213
|
+
!message.split("\n").includes(`Immune-Brain-Task: ${e.selection.task_id}`))
|
|
214
|
+
throw new Error("consolidation commit lacks the selected final task identity");
|
|
215
|
+
if (git(root, ["rev-parse", `${commit}^@`]).trim() !== e.head ||
|
|
216
|
+
git(root, ["symbolic-ref", "--short", "HEAD"]).trim() !== e.branch ||
|
|
217
|
+
spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", commit, "HEAD"], { stdio: "ignore" }).status !== 0)
|
|
218
|
+
throw new Error("consolidation commit lineage mismatch");
|
|
219
|
+
const paths = git(root, ["diff-tree", "--no-commit-id", "--no-renames", "--name-only", "-r", "-z", e.head, commit]).split("\0").filter(Boolean).sort();
|
|
220
|
+
if (JSON.stringify(paths) !== JSON.stringify(e.manifest.map(entry => entry.path))) throw new Error("consolidation commit manifest differs");
|
|
221
|
+
for (const entry of e.manifest) {
|
|
222
|
+
const tree = git(root, ["ls-tree", "-z", commit, "--", entry.path]);
|
|
223
|
+
const identity = tree ? tree.split("\t")[0]!.split(" ") : null;
|
|
224
|
+
if ((identity?.[0] ?? null) !== entry.mode || (identity?.[2] ?? null) !== entry.oid)
|
|
225
|
+
throw new Error("consolidation committed bytes/mode mismatch");
|
|
226
|
+
}
|
|
227
|
+
for (const identity of e.runs) {
|
|
228
|
+
const current = (await runEvidence(root, identity)).binding;
|
|
229
|
+
if (JSON.stringify(current) !== JSON.stringify(identity)) throw new Error("consolidation source proof changed");
|
|
230
|
+
for (const path of [auditRunRecordPath(identity.task_id, identity.run_id), auditRunTerminalProofPath(identity.task_id, identity.run_id)])
|
|
231
|
+
if (git(root, ["show", `${commit}:${path}`]) !== readSecureProjectFile(root, path)) throw new Error("committed source audit mismatch");
|
|
232
|
+
}
|
|
233
|
+
for (const parent of e.parent_tasks)
|
|
234
|
+
if (readTaskIntent(root, parent.task_id).content_hash !== parent.intent_hash) throw new Error("consolidated Parent Intent changed");
|
|
235
|
+
const final = readSettledTaskEvidence(root, e.selection.task_id)!.record;
|
|
236
|
+
if (final.contract !== "assurance_kernel/task_record/v4") throw new Error("final consolidation record changed contract");
|
|
237
|
+
const live = captureGitTaskRevisionSnapshot(root, final.intent_snapshot.scope_hint, final.git_base_head, e.selection.task_id);
|
|
238
|
+
if (consolidationDigest(live) !== e.delivery_diff_hash) throw new Error("consolidated delivery changed after commit");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function observeConsolidatedDeliveries(root: string, slug: string, requiredIds: readonly string[]) {
|
|
242
|
+
const receipt = readParentConsolidationReceipt(root, slug);
|
|
243
|
+
if (!receipt || !receipt.commit) return null;
|
|
244
|
+
if (JSON.stringify(receipt.envelope.parent_tasks.map(task => task.task_id).sort()) !== JSON.stringify([...requiredIds].sort()))
|
|
245
|
+
throw new Error("consolidation Parent contribution set changed");
|
|
246
|
+
await verifyConsolidatedCommit(root, receipt);
|
|
247
|
+
return receipt;
|
|
248
|
+
}
|
|
@@ -62,9 +62,15 @@ function enrollmentBaselinePath(root: string): string {
|
|
|
62
62
|
return join(root, ".imm/state/enrollment-baseline.json");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Records the dirt that predates the task. `claimedPaths` are the in-scope
|
|
67
|
+
* paths the confirmed Enrollment gate listed: they are left out, so later
|
|
68
|
+
* snapshots treat them as task work instead of withholding them.
|
|
69
|
+
*/
|
|
70
|
+
export function writeEnrollmentBaseline(root: string, claimedPaths: readonly string[] = []): void {
|
|
66
71
|
const snapshot = captureGitWorkspaceSnapshot(root);
|
|
67
72
|
if (!snapshot) return;
|
|
73
|
+
for (const path of claimedPaths) delete snapshot.dirty_files[path];
|
|
68
74
|
const path = enrollmentBaselinePath(root);
|
|
69
75
|
mkdirSync(dirname(path), { recursive: true });
|
|
70
76
|
writeFileSync(path, `${JSON.stringify(snapshot)}\n`);
|
|
@@ -220,13 +226,14 @@ export function setGitTaskSnapshotTestHook(hook: () => void): () => void {
|
|
|
220
226
|
};
|
|
221
227
|
}
|
|
222
228
|
|
|
223
|
-
function gitBytes(root: string, args: string[]): Buffer {
|
|
229
|
+
function gitBytes(root: string, args: string[], env?: NodeJS.ProcessEnv): Buffer {
|
|
224
230
|
const diff = args[0] === "diff";
|
|
225
231
|
const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
|
|
226
232
|
const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
|
|
227
233
|
encoding: null,
|
|
228
234
|
stdio: ["ignore", "pipe", "pipe"],
|
|
229
235
|
maxBuffer: 8 * 1024 * 1024,
|
|
236
|
+
...(env ? { env } : {}),
|
|
230
237
|
});
|
|
231
238
|
if ((result.status !== 0 && !(diff && result.status === 1)) || !Buffer.isBuffer(result.stdout)) {
|
|
232
239
|
throw new Error(`Git task snapshot command failed: git ${args.join(" ")}`);
|
|
@@ -282,7 +289,7 @@ function decodeIndexFlaggedPaths(bytes: Buffer, label: string): Array<{ path: st
|
|
|
282
289
|
const record = bytes.subarray(start, index);
|
|
283
290
|
if (record.length < 3 || record[1] !== 0x20) throw new Error(`${label} contains a malformed entry`);
|
|
284
291
|
const tag = String.fromCharCode(record[0]);
|
|
285
|
-
if (tag === "h" || tag === "S") {
|
|
292
|
+
if (tag === "h" || tag === "S" || tag === "s") {
|
|
286
293
|
flagged.push({
|
|
287
294
|
path: decodeCanonicalGitPath(record.subarray(2), label),
|
|
288
295
|
flag: tag === "h" ? "assume-unchanged" : "skip-worktree",
|
|
@@ -394,9 +401,10 @@ function taskPathMatchesScope(path: string, scope: string[]): boolean {
|
|
|
394
401
|
|
|
395
402
|
/**
|
|
396
403
|
* A staged in-scope path that still equals the Enrollment baseline predates the
|
|
397
|
-
* task
|
|
398
|
-
* revision did not carry, so it is
|
|
399
|
-
* yields the same bytes, and only
|
|
404
|
+
* task and was not listed by the confirmed Enrollment gate. Dropping it silently
|
|
405
|
+
* let QA attest workspace bytes that the Review revision did not carry, so it is
|
|
406
|
+
* a hard stop instead: reverting and reapplying yields the same bytes, and only
|
|
407
|
+
* a fresh Enrollment that lists the path, or starts from a clean scope, recovers.
|
|
400
408
|
* The task's own planning sidecar is exempt, as it is in dirtyScopePaths:
|
|
401
409
|
* Enrollment requires it tracked, so it is routinely staged in the baseline.
|
|
402
410
|
*/
|
|
@@ -411,14 +419,15 @@ function assertNoPreEnrollmentScopeChanges(root: string, scopedStagedPaths: read
|
|
|
411
419
|
.sort(comparePaths);
|
|
412
420
|
if (withheld.length > 0)
|
|
413
421
|
throw new Error(
|
|
414
|
-
`task scope contains staged changes that predate Enrollment and cannot become task work: ${withheld.join(", ")}; stop the task and
|
|
422
|
+
`task scope contains staged changes that predate Enrollment and cannot become task work: ${withheld.join(", ")}; stop the task and enroll again so the Enrollment gate lists them, or from a clean scope`,
|
|
415
423
|
);
|
|
416
424
|
}
|
|
417
425
|
|
|
418
426
|
/**
|
|
419
427
|
* In-scope paths that are already staged, modified or untracked relative to
|
|
420
|
-
* HEAD. Enrollment
|
|
421
|
-
* and
|
|
428
|
+
* HEAD. The Enrollment gate lists them; the ones it listed are claimed as task
|
|
429
|
+
* work, and any other one refuses Enrollment, because the baseline would record
|
|
430
|
+
* it as user dirt and the task could never deliver it.
|
|
422
431
|
*/
|
|
423
432
|
export function dirtyScopePaths(projectRoot: string, scopeHint: unknown, taskId?: string): string[] {
|
|
424
433
|
const root = realpathSync(resolve(projectRoot));
|
|
@@ -430,7 +439,7 @@ export function dirtyScopePaths(projectRoot: string, scopeHint: unknown, taskId?
|
|
|
430
439
|
);
|
|
431
440
|
}
|
|
432
441
|
|
|
433
|
-
function taskSnapshotOnce(root: string, scope: string[], taskId?: string): GitTaskSnapshot {
|
|
442
|
+
function taskSnapshotOnce(root: string, scope: string[], taskId?: string, tolerated?: string[]): GitTaskSnapshot {
|
|
434
443
|
const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
435
444
|
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
436
445
|
if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
|
|
@@ -462,8 +471,10 @@ function taskSnapshotOnce(root: string, scope: string[], taskId?: string): GitTa
|
|
|
462
471
|
const uncommittedInScope = [...new Set([...unstagedPaths, ...untrackedPaths])]
|
|
463
472
|
.filter((path) => taskPathMatchesScope(path, scope))
|
|
464
473
|
.sort(comparePaths);
|
|
465
|
-
if (uncommittedInScope.length > 0)
|
|
466
|
-
throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
|
|
474
|
+
if (uncommittedInScope.length > 0) {
|
|
475
|
+
if (!tolerated) throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
|
|
476
|
+
tolerated.splice(0, tolerated.length, ...uncommittedInScope);
|
|
477
|
+
}
|
|
467
478
|
|
|
468
479
|
const taskPaths = [...new Set(stagedPaths)]
|
|
469
480
|
.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope))
|
|
@@ -495,6 +506,7 @@ export function captureGitTaskSnapshot(
|
|
|
495
506
|
projectRoot: string,
|
|
496
507
|
scopeHint: unknown,
|
|
497
508
|
taskId?: string,
|
|
509
|
+
tolerated?: string[],
|
|
498
510
|
): GitTaskSnapshot {
|
|
499
511
|
const requestedRoot = resolve(projectRoot);
|
|
500
512
|
const requestedStat = lstatSync(requestedRoot);
|
|
@@ -502,9 +514,9 @@ export function captureGitTaskSnapshot(
|
|
|
502
514
|
throw new Error("task snapshot root must be a real directory");
|
|
503
515
|
const root = realpathSync(requestedRoot);
|
|
504
516
|
const scope = assertCanonicalTaskScope(scopeHint);
|
|
505
|
-
const before = taskSnapshotOnce(root, scope, taskId);
|
|
517
|
+
const before = taskSnapshotOnce(root, scope, taskId, tolerated);
|
|
506
518
|
gitTaskSnapshotTestHook?.();
|
|
507
|
-
const after = taskSnapshotOnce(root, scope, taskId);
|
|
519
|
+
const after = taskSnapshotOnce(root, scope, taskId, tolerated);
|
|
508
520
|
if (JSON.stringify(after) !== JSON.stringify(before))
|
|
509
521
|
throw new Error("Git task snapshot changed while being captured");
|
|
510
522
|
return before;
|
|
@@ -561,6 +573,7 @@ function taskRevisionSnapshotOnce(
|
|
|
561
573
|
scope: string[],
|
|
562
574
|
baseHead: string,
|
|
563
575
|
taskId?: string,
|
|
576
|
+
tolerated?: string[],
|
|
564
577
|
): GitTaskRevisionSnapshot {
|
|
565
578
|
const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
566
579
|
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
@@ -606,8 +619,10 @@ function taskRevisionSnapshotOnce(
|
|
|
606
619
|
"Git task revision paths",
|
|
607
620
|
);
|
|
608
621
|
const drift = [...new Set([...scopedUnstagedPaths, ...scopedUntrackedPaths])].sort(comparePaths);
|
|
609
|
-
if (drift.length > 0)
|
|
610
|
-
throw new Error(`task scope contains unstaged or untracked changes: ${drift.join(", ")}`);
|
|
622
|
+
if (drift.length > 0) {
|
|
623
|
+
if (!tolerated) throw new Error(`task scope contains unstaged or untracked changes: ${drift.join(", ")}`);
|
|
624
|
+
tolerated.splice(0, tolerated.length, ...drift);
|
|
625
|
+
}
|
|
611
626
|
|
|
612
627
|
const changed = [...new Set(scopedStagedPaths)].sort(comparePaths);
|
|
613
628
|
const changedPaths: Record<string, GitTaskIndexEntry> = {};
|
|
@@ -640,6 +655,7 @@ export function captureGitTaskRevisionSnapshot(
|
|
|
640
655
|
scopeHint: unknown,
|
|
641
656
|
baseHead: unknown,
|
|
642
657
|
taskId?: string,
|
|
658
|
+
tolerated?: string[],
|
|
643
659
|
): GitTaskRevisionSnapshot {
|
|
644
660
|
const requestedRoot = resolve(projectRoot);
|
|
645
661
|
const requestedStat = lstatSync(requestedRoot);
|
|
@@ -650,9 +666,9 @@ export function captureGitTaskRevisionSnapshot(
|
|
|
650
666
|
throw new Error("task revision base must be a Git commit id");
|
|
651
667
|
const scope = assertCanonicalTaskScope(scopeHint);
|
|
652
668
|
const normalizedBase = baseHead.toLowerCase();
|
|
653
|
-
const before = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId);
|
|
669
|
+
const before = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId, tolerated);
|
|
654
670
|
gitTaskSnapshotTestHook?.();
|
|
655
|
-
const after = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId);
|
|
671
|
+
const after = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId, tolerated);
|
|
656
672
|
if (JSON.stringify(after) !== JSON.stringify(before))
|
|
657
673
|
throw new Error("Git task revision changed while being captured");
|
|
658
674
|
return before;
|
|
@@ -705,6 +721,36 @@ export function taskDeliveryIdentity(
|
|
|
705
721
|
return taskDiffIdentity(projectRoot, record.intent_snapshot.scope_hint, record.task_id);
|
|
706
722
|
}
|
|
707
723
|
|
|
724
|
+
/**
|
|
725
|
+
* The delivery identity for a read-only status projection. Unstaged or
|
|
726
|
+
* untracked in-scope paths do not refuse it: the identity is the staged one and
|
|
727
|
+
* the paths are returned for a warning. Nothing may be frozen from this value;
|
|
728
|
+
* every snapshot that is frozen goes through `taskDeliveryIdentity`, which
|
|
729
|
+
* still refuses those paths.
|
|
730
|
+
*/
|
|
731
|
+
export function taskStatusIdentity(
|
|
732
|
+
projectRoot: string,
|
|
733
|
+
record: TaskDeliveryRecord,
|
|
734
|
+
): GitTaskDiffIdentity & { unstaged_scope_paths: string[] } {
|
|
735
|
+
const unstaged: string[] = [];
|
|
736
|
+
const scope = record.intent_snapshot.scope_hint;
|
|
737
|
+
if (record.contract === "assurance_kernel/task_record/v4") {
|
|
738
|
+
if (!record.git_base_head) throw new Error("TaskRecord v4 is missing git_base_head");
|
|
739
|
+
const snapshot = captureGitTaskRevisionSnapshot(projectRoot, scope, record.git_base_head, record.task_id, unstaged);
|
|
740
|
+
return {
|
|
741
|
+
diff_hash: hashTaskSnapshot(snapshot),
|
|
742
|
+
changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths),
|
|
743
|
+
unstaged_scope_paths: unstaged,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const snapshot = captureGitTaskSnapshot(projectRoot, scope, record.task_id, unstaged);
|
|
747
|
+
return {
|
|
748
|
+
diff_hash: hashTaskSnapshot(snapshot),
|
|
749
|
+
changed_paths: Object.keys(snapshot.staged_files).sort(comparePaths),
|
|
750
|
+
unstaged_scope_paths: unstaged,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
708
754
|
/** The same v4 identity for an immutable delivered commit, without touching the index. */
|
|
709
755
|
export function taskCommitRevisionIdentity(projectRoot: string, scopeHint: unknown, baseHead: string, commit: string): GitTaskDiffIdentity {
|
|
710
756
|
const requested = resolve(projectRoot);
|
|
@@ -989,3 +1035,65 @@ export function assertChangedFilesWithinScope(
|
|
|
989
1035
|
);
|
|
990
1036
|
}
|
|
991
1037
|
}
|
|
1038
|
+
|
|
1039
|
+
function gitBlobId(body: Buffer, length: number): string {
|
|
1040
|
+
const hash = createHash(length === 64 ? "sha256" : "sha1");
|
|
1041
|
+
hash.update(`blob ${body.length}\0`);
|
|
1042
|
+
hash.update(body);
|
|
1043
|
+
return hash.digest("hex");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* In-scope tracked paths whose working-directory content differs from `tree`.
|
|
1048
|
+
* Bytes are compared directly, never through the index stat cache, so a file a
|
|
1049
|
+
* command rewrote with identical content is not a difference. Paths outside the
|
|
1050
|
+
* scope are the user's environment and are not read.
|
|
1051
|
+
*/
|
|
1052
|
+
// A hosting process that carries one of these would compare another repository
|
|
1053
|
+
// or index instead of the task's working directory.
|
|
1054
|
+
const GIT_REPOSITORY_BINDINGS = [
|
|
1055
|
+
"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY",
|
|
1056
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_COMMON_DIR", "GIT_NAMESPACE", "GIT_PREFIX",
|
|
1057
|
+
];
|
|
1058
|
+
|
|
1059
|
+
export function inScopeTrackedDrift(projectRoot: string, tree: string, scopeHint: unknown): string[] {
|
|
1060
|
+
const root = resolve(projectRoot);
|
|
1061
|
+
const env = { ...process.env };
|
|
1062
|
+
for (const name of GIT_REPOSITORY_BINDINGS) delete env[name];
|
|
1063
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(tree)) throw new Error("frozen tree has invalid identity");
|
|
1064
|
+
const scope = Array.isArray(scopeHint) ? scopeHint.filter((entry): entry is string => typeof entry === "string") : [];
|
|
1065
|
+
const inScope = (path: string) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope);
|
|
1066
|
+
const expected = new Map<string, { mode: string; oid: string }>();
|
|
1067
|
+
for (const entry of gitBytes(root, ["ls-tree", "-r", "-z", tree], env).toString("utf8").split("\0")) {
|
|
1068
|
+
const tab = entry.indexOf("\t");
|
|
1069
|
+
if (tab < 0) continue;
|
|
1070
|
+
const [mode, , oid] = entry.slice(0, tab).split(" ");
|
|
1071
|
+
const path = entry.slice(tab + 1);
|
|
1072
|
+
if (mode && oid && inScope(path)) expected.set(path, { mode, oid });
|
|
1073
|
+
}
|
|
1074
|
+
const tracked = splitNull(gitBytes(root, ["ls-files", "-z"], env).toString("utf8")).filter(inScope);
|
|
1075
|
+
const regular: string[] = [];
|
|
1076
|
+
const drift = new Set<string>();
|
|
1077
|
+
for (const path of new Set([...expected.keys(), ...tracked])) {
|
|
1078
|
+
const want = expected.get(path);
|
|
1079
|
+
let stat;
|
|
1080
|
+
try { stat = lstatSync(join(root, path)); } catch { stat = null; }
|
|
1081
|
+
if (!want || !stat) { if (want || stat) drift.add(path); continue; }
|
|
1082
|
+
// A submodule is pinned by the index, not by working-directory bytes.
|
|
1083
|
+
if (want.mode === "160000") continue;
|
|
1084
|
+
if (stat.isSymbolicLink()) {
|
|
1085
|
+
if (want.mode !== "120000" || gitBlobId(Buffer.from(readlinkSync(join(root, path))), want.oid.length) !== want.oid) drift.add(path);
|
|
1086
|
+
} else if (!stat.isFile() || want.mode === "120000" || (want.mode === "100755") !== ((stat.mode & 0o100) !== 0)) drift.add(path);
|
|
1087
|
+
else regular.push(path);
|
|
1088
|
+
}
|
|
1089
|
+
if (regular.length) {
|
|
1090
|
+
// `--stdin-paths` applies the same clean filters as `git add` would.
|
|
1091
|
+
const hashed = spawnSync("git", ["--no-optional-locks", "-C", root, "hash-object", "--stdin-paths"], {
|
|
1092
|
+
input: `${regular.join("\n")}\n`, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 8 * 1024 * 1024, env,
|
|
1093
|
+
});
|
|
1094
|
+
const ids = hashed.status === 0 ? hashed.stdout.split("\n").filter(Boolean) : [];
|
|
1095
|
+
if (ids.length !== regular.length) throw new Error("Git task snapshot command failed: git hash-object");
|
|
1096
|
+
regular.forEach((path, index) => { if (ids[index] !== expected.get(path)!.oid) drift.add(path); });
|
|
1097
|
+
}
|
|
1098
|
+
return [...drift].sort(comparePaths);
|
|
1099
|
+
}
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
their own existing grant. An explicit approval already covering the same
|
|
16
16
|
operation, target, and impact — including a valid batch capability — stays
|
|
17
17
|
usable without another gate.
|
|
18
|
+
- A precondition commit is the one commit the runtime makes without a separate
|
|
19
|
+
grant. When an `imm-run` precondition would fail only because paths are
|
|
20
|
+
uncommitted, the runtime commits that precondition's committable set, an
|
|
21
|
+
explicit path list, silently and continues. It runs Git hooks normally and
|
|
22
|
+
grants no push, publication, history rewrite, or commit of any other path. A
|
|
23
|
+
model never makes this commit itself and never asks the user to make it.
|
|
18
24
|
- Before a new outbound effect, check the project's existing research channel and
|
|
19
25
|
data scope. A public article, a mock, a documentation example, or a local
|
|
20
26
|
read-only database gives no authority to call an arbitrary provider or send new
|
|
@@ -55,11 +61,19 @@ framing does not load that execution guidance.
|
|
|
55
61
|
|
|
56
62
|
Ordinary host input stays host-native and does not run natural-language Managed
|
|
57
63
|
routing. A new Managed workflow starts only from explicit `imm-brainstorm`,
|
|
58
|
-
`imm-planner`, or `imm-run` Skill entry.
|
|
64
|
+
`imm-planner`, or `imm-run` Skill entry. For `imm-planner` and `imm-brainstorm`
|
|
65
|
+
the user's request for that planning or clarification, by slash command or in
|
|
66
|
+
natural language, is such an entry; a request that asks for neither stays
|
|
67
|
+
host-native. `imm-run` starts only from a literal-user entry. A model-entered
|
|
68
|
+
Planner or Brainstorm leaves the session's run entry unchanged.
|
|
59
69
|
|
|
60
70
|
1. **Continue an existing Managed owner explicitly**: an active Assurance
|
|
61
71
|
projection, TaskIntent, TaskRecord, reviewer `follow_up`, or other nonterminal
|
|
62
72
|
Managed owner remains authoritative; the user resumes it with `imm-run`.
|
|
73
|
+
Once that entry binds this session, ordinary status questions and in-scope
|
|
74
|
+
clarification retain automatic response continuation. Explicit cancel, planning
|
|
75
|
+
entry or session replacement releases the binding; Host continuation metadata
|
|
76
|
+
remains non-authoritative.
|
|
63
77
|
2. **Start explicitly**: the selected Immune-Brain Skill owns its planning or
|
|
64
78
|
coordination work. It creates only requested artifacts and their required
|
|
65
79
|
parent directories. Explicit Planner entry also owns absent routing-policy
|
|
@@ -112,6 +126,8 @@ Routine Managed enrollment is the first step of an explicit `imm-run` entry and
|
|
|
112
126
|
|
|
113
127
|
For an unborn symbolic Git HEAD, the same Enrollment confirmation discloses an empty root commit. After approval, the Host creates that commit without staging project content or changing the index/worktree, then revalidates ownership and rehearses Enrollment against the new base. Git identity uses existing environment/config values with an explicit automation fallback, never config writes. Decline or pre-initialization cancellation leaves HEAD unborn; if later Enrollment fails or is cancelled, report the remaining commit. Invalid Git refs are errors, not initialization candidates.
|
|
114
128
|
|
|
129
|
+
Before Enrollment prepares its binding, the Host commits the Enrollment committable set when an untracked TaskIntent would otherwise fail it. The set is the task's own TaskIntent and the Spec its binding names. In-scope changes that already exist are not committed: the Enrollment gate lists them and only the listed paths become task work on confirmation, so a dirty scope is never claimed without that list being shown. The commit is silent, needs no separate confirmation, and the native confirmation binds the HEAD that results. When no precondition fails, no commit is made. Dirty paths outside the set are left as they are and do not block Enrollment. With an unborn HEAD the empty root commit comes first and this commit follows it. A commit Git refuses fails Enrollment with Git's message and is never retried with hooks bypassed; if Enrollment is declined, cancelled or fails afterwards, report the remaining commit.
|
|
130
|
+
|
|
115
131
|
## Parallel Read-Only Dispatch
|
|
116
132
|
|
|
117
133
|
State mutations, step activations, QA decisions, and plan switches remain
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: imm-brainstorm
|
|
3
|
-
description: Use when the user
|
|
3
|
+
description: Use when the user asks to clarify or explore requirements before Immune-Brain planning.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Immune-Brain: Brainstorm
|
|
7
7
|
|
|
8
8
|
Use [`../../dist/imm-brainstorm.md`](../../dist/imm-brainstorm.md) as the
|
|
9
|
-
canonical contract index, not a whole-document read.
|
|
10
|
-
|
|
9
|
+
canonical contract index, not a whole-document read. Starts on the user's request
|
|
10
|
+
to clarify or explore requirements, by slash command or in natural language;
|
|
11
|
+
ordinary host questions do not start this workflow.
|
|
11
12
|
|
|
12
13
|
Mandatory constraints before any action: Brainstorm is read-only — no code,
|
|
13
14
|
test, or runtime edits; no Spec, Plan, or workflow-state writes. All modes
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: imm-planner
|
|
3
|
-
description: Use when the user
|
|
3
|
+
description: Use when the user asks for an Immune-Brain Spec, Plan, or TaskIntent for a repository change.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Immune-Brain: Planner
|
|
7
7
|
|
|
8
8
|
Use [`../../dist/imm-planner.md`](../../dist/imm-planner.md) as the canonical contract
|
|
9
|
-
index, not a whole-document read.
|
|
9
|
+
index, not a whole-document read. Starts on the user's request for a Spec, Plan
|
|
10
|
+
or TaskIntent, by slash command or in natural language; other host requests stay
|
|
10
11
|
host-native.
|
|
11
12
|
|
|
12
13
|
Mandatory constraints before any action: Planner writes a TaskIntent, and a Spec
|
|
@@ -42,6 +43,11 @@ own routes instead of these stages.
|
|
|
42
43
|
- retirement design: [Retirement Completion Contract](../../dist/imm-planner.md#retirement-completion-contract)
|
|
43
44
|
- optional research dispatch: [Research Dispatch](../../dist/imm-planner.md#research-dispatch)
|
|
44
45
|
|
|
46
|
+
A Spec with external input declares its threat model and supported environment,
|
|
47
|
+
acceptance enumerates concrete negative cases rather than absolute
|
|
48
|
+
any-malformed-input clauses, and descriptors name focused test files, never whole
|
|
49
|
+
suites.
|
|
50
|
+
|
|
45
51
|
Every request stops after candidate TaskIntent validation, plus Spec validation when the work is complex; Plan-only requests stop there too. The Planner
|
|
46
52
|
never invokes the current Host's native Enrollment gate, even when the request also asks for
|
|
47
53
|
execution: report the candidate and name a literal-user `imm-run` entry as the
|
|
@@ -7,12 +7,16 @@ description: Use when the user explicitly requests execution or resumption of an
|
|
|
7
7
|
|
|
8
8
|
Use [`../../dist/imm-run.md`](../../dist/imm-run.md) as the canonical contract
|
|
9
9
|
index, not a whole-document read, in the current host conversation. Explicit entry only:
|
|
10
|
-
ordinary host input never
|
|
10
|
+
ordinary host input never creates a Managed execution binding. Status questions
|
|
11
|
+
and in-scope clarification retain an already-bound run.
|
|
11
12
|
|
|
12
13
|
Mandatory constraints before any action: verify the active backend claim,
|
|
13
14
|
TaskIntent, and TaskRecord via `imm_kernel_canary` `status` first; invalid or
|
|
14
|
-
contradictory projections fail closed.
|
|
15
|
-
|
|
15
|
+
contradictory projections fail closed. A Parent Issue selects its whole planned
|
|
16
|
+
batch; load the batch route and open native Batch Authorization directly. A Child
|
|
17
|
+
Issue selects only its TaskIntent, verifies predecessor settlement and integration,
|
|
18
|
+
then opens single-task Enrollment. With no active claim, a standalone validated
|
|
19
|
+
candidate opens native Enrollment once. Resume only from those authority facts:
|
|
16
20
|
a compaction heading, HANDOFF summary, or Issue state is prose, never authority.
|
|
17
21
|
All Managed authority gates use the
|
|
18
22
|
current Host's native interaction. A failed gate stays fail-closed and
|
|
@@ -25,11 +29,11 @@ references load only under their own condition. Never read the whole contract
|
|
|
25
29
|
or all references as an entry prerequisite.
|
|
26
30
|
|
|
27
31
|
- common: [Shared Guards](../../dist/BASELINE.md#shared-guards), [Workflow Activation](../../dist/BASELINE.md#workflow-activation), [Host Confirmation Boundary](../../dist/BASELINE.md#host-confirmation-boundary), [Kernel Canary Routing and Authority](../../dist/imm-run.md#kernel-canary-routing-and-authority)
|
|
28
|
-
- unattended batch run, or any question about whether `imm-run` starts one: [Unattended Batch Opt-In](../../dist/imm-run.md#unattended-batch-opt-in)
|
|
29
|
-
- parallel batch (`max_parallel`, `lane_offers`, Lane admission or integration): [Parallel Batch Opt-In](../../dist/imm-run.md#parallel-batch-opt-in)
|
|
30
|
-
- a lane-mode report carries an `executor` handoff, or a Lane's Executor Host session has ended: [Lane Executor Supervision](../../dist/imm-run.md#lane-executor-supervision)
|
|
31
|
-
- that Parent runs inside Herdr (`HERDR_ENV=1`): [Herdr Lane Tabs](../../dist/imm-run.md#herdr-lane-tabs)
|
|
32
|
-
- a lane-mode batch is about to start, to resolve the repository's parallelism, Executor Host, model and effort: [Lane Preferences](../../dist/imm-run.md#lane-preferences)
|
|
32
|
+
- Parent Issue, unattended batch run, or any question about whether `imm-run` starts one: [Unattended Batch Opt-In](../../dist/imm-run-batch.md#unattended-batch-opt-in)
|
|
33
|
+
- parallel batch (`max_parallel`, `lane_offers`, Lane admission or integration): [Parallel Batch Opt-In](../../dist/imm-run-batch.md#parallel-batch-opt-in)
|
|
34
|
+
- a lane-mode report carries an `executor` handoff, or a Lane's Executor Host session has ended: [Lane Executor Supervision](../../dist/imm-run-batch.md#lane-executor-supervision)
|
|
35
|
+
- that Parent runs inside Herdr (`HERDR_ENV=1`): [Herdr Lane Tabs](../../dist/imm-run-batch.md#herdr-lane-tabs)
|
|
36
|
+
- a lane-mode batch is about to start, to resolve the repository's parallelism, Executor Host, model and effort: [Lane Preferences](../../dist/imm-run-batch.md#lane-preferences)
|
|
33
37
|
- steady execution: [Verification and Local Recovery](../../dist/BASELINE.md#verification-and-local-recovery), [Execution Loop](../../dist/imm-run.md#execution-loop), [Observable Output](../../dist/imm-run.md#observable-output)
|
|
34
38
|
- rework, scope expansion, breaking revision, user decision, stop, interruption or unknown state before any action: [Decisions and Recovery](../../dist/imm-run.md#decisions-and-recovery), [Failure Output](../../dist/imm-run.md#failure-output); re-read `status`, then the pending obligation. Rework submits the verdict before editing, and an uncertain interruption resumes from exact task/run authority facts rather than a summary.
|
|
35
39
|
- review or post-settlement learning: [Review and Learning](../../dist/imm-run.md#review-and-learning)
|