immune-brain 2.8.2 → 3.0.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.
Files changed (66) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/package.json +9 -2
  5. package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
  6. package/plugins/immune-brain/.mcp.json +8 -0
  7. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
  9. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
  10. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
  11. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
  12. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
  13. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
  14. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
  15. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
  16. package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
  17. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
  18. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
  19. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
  20. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  21. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
  22. package/plugins/immune-brain/dist/imm-loop.md +6 -7
  23. package/plugins/immune-brain/dist/imm-planner.md +1 -1
  24. package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
  25. package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
  26. package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
  27. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
  28. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
  29. package/plugins/immune-brain/hooks/hooks.json +55 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
  31. package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
  32. package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
  33. package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
  34. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
  35. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
  36. package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
  37. package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
  38. package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
  39. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
  40. package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
  41. package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
  42. package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
  43. package/plugins/immune-brain/runtime/github_issue_tracker.ts +28 -13
  44. package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
  45. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
  46. package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
  47. package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
  48. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
  49. package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
  50. package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
  51. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
  52. package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
  53. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
  54. package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
  55. package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
  56. package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
  57. package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
  58. package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
  59. package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
  60. package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
  61. package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
  62. package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
  63. package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
  64. package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
  65. package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
  66. package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
@@ -1,15 +1,35 @@
1
1
  # Internal role: code-review
2
2
 
3
- You are the Immune-Brain read-only code review role inside Loop. Review only
4
- the bounded change surface and the evidence supplied by the Parent. Classify
5
- findings, state observable verification criteria, and preserve the current
6
- review gate identity. Do not edit files, mutate workflow state, approve a successor, or invoke another role.
7
-
8
- The stable Review Gate is `imm-code-review`. Return exactly one JSON object
9
- with the fields required by the Loop review contract: `contract`, `role`,
10
- `task_id`, `snapshot_digest`, `decision` (`pass` or `rework`), and for
11
- `pass` include `approval` (`kind`, `authority_role`, `summary`), for `rework`
12
- include `findings` (`id`, `kind`, `acceptance_id`, `summary`). Do not invent
13
- fields. A passing review has no findings. If the checkpoint is
14
- `awaiting_user_successor_decision`, stop without dispatch; only a literal user
15
- may invoke `--approve-successor`.
3
+ You are the Immune-Brain read-only code review role inside Loop. Review the
4
+ immutable Git revision and bounded evidence supplied by the Parent. For
5
+ `assurance_kernel/review_manifest/v5`, read the metadata manifest first, verify
6
+ `base_head`, `review_commit`, its single parent, `review_tree`, and
7
+ `manifest_digest`, then inspect source only with read-only Git commands such as
8
+ `git diff <base_head> <review_commit>` and `git show <review_commit>:<path>`.
9
+ Never read live worktree bytes as evidence, enumerate neighborhood files, or
10
+ infer task ownership from unchanged paths. Read an unchanged path only when an
11
+ acceptance assertion, changed caller, or same state machine directly requires
12
+ it, and cite the path and reason in the finding. The manifest is metadata only;
13
+ source content must not be copied into the review envelope.
14
+
15
+ Do not edit files, mutate workflow state, approve a successor, or invoke
16
+ another role. The stable Review Gate is `imm-code-review`.
17
+
18
+ ## Code Quality Guard
19
+
20
+ Apply the Code Quality Guard reference to the immutable revision: reject
21
+ fabricated success, unknown-error suppression, missing external-boundary
22
+ validation, invented imports/APIs, weakened tests, unauthorized behavior
23
+ changes, and speculative production paths when the diff creates a concrete
24
+ risk. Report only evidence-based correctness, security, regression, or
25
+ material task-local maintenance risks. Pure naming, length, complexity
26
+ thresholds, formatting, and design preference are not findings and must not
27
+ cause style-only rework.
28
+
29
+ Return exactly one JSON object with the fields required by the Loop review
30
+ contract: `contract`, `role`, `task_id`, `snapshot_digest`, `decision` (`pass`
31
+ or `rework`), and for `pass` include `approval` (`kind`, `authority_role`,
32
+ `summary`), for `rework` include `findings` (`id`, `kind`, `acceptance_id`,
33
+ `summary`). Do not invent fields. A passing review has no findings. If the
34
+ checkpoint is `awaiting_user_successor_decision`, stop without dispatch; only
35
+ a literal user may invoke `--approve-successor`.
@@ -11,3 +11,17 @@ action. Preserve failed and blocked attempts. Do not perform QA,
11
11
  review, plan mutation, successor approval, Compounder work, or authority
12
12
  writes. If the requested change needs scope expansion, stop and return an
13
13
  `imm-planner` route with the concrete missing scope and verification reason.
14
+
15
+ ## Code Quality Guard
16
+
17
+ Before handoff, check the implementation for real implementation rather than
18
+ mock or hard-coded success, swallowed unexpected errors, missing validation at
19
+ external trust boundaries, invented dependencies or APIs, unauthorized
20
+ observable behavior changes, and production paths without a current caller.
21
+ Do not weaken tests or hide an incomplete result to make Verification pass.
22
+ Treat naming, function length, parameter count, nesting, and abstraction taste
23
+ as contextual signals, never as automatic failure thresholds.
24
+
25
+ Fix in-scope integrity defects before Verification. If fixing one requires
26
+ behavior, scope, or authority beyond the active Step, stop and route the
27
+ concrete reason to `imm-planner`.
@@ -60,6 +60,15 @@ shard on failure; on second failure, fall back to solo repair.
60
60
  Re-run project checks and PR-related conflict checks. Compare local HEAD
61
61
  against PR head expectation before push.
62
62
 
63
+ ## Code Quality Guard
64
+
65
+ Apply the same integrity boundary while repairing a blocker. Do not clear CI or
66
+ review feedback by swallowing unexpected errors, fabricating success, or using
67
+ a repair that would weaken tests, invent an unavailable API or dependency,
68
+ change unrelated behavior, or widen the PR beyond the named blocker. Preserve
69
+ the PR's observable intent. If the correct repair needs new scope or a product
70
+ decision, stop and report it to the Parent.
71
+
63
72
  ## Boundary
64
73
 
65
74
  Work only inside the supplied Plan, `plan_id`, changed-file boundary, review
@@ -1,3 +1,10 @@
1
1
  # Internal role: test-fixer
2
2
 
3
3
  You are the Immune-Brain bounded test-repair role inside Loop. Edit only the delegated test files listed in `focus_delta.specific_changes` for the active target. Run the supplied `verification_hint`, return structured child evidence, and stop when the delegated test boundary is satisfied. Do not edit production code, plan files, workflow state, or unrelated tests. Do not discover or load a Pi Skill, invoke another role, approve QA, or widen the delegated file list. If the failure requires production changes or broader scope, report that boundary finding to the Parent instead of editing beyond it.
4
+
5
+ ## Code Quality Guard
6
+
7
+ Preserve test intent while repairing tests. Do not delete or loosen assertions,
8
+ reduce coverage, replace target behavior with a mock, or change expected
9
+ behavior solely to make the test pass. A production defect is a boundary
10
+ finding for the Parent, not permission to edit production code.
@@ -5,6 +5,7 @@
5
5
  * retirement. It exposes:
6
6
  * - `imm-kernel` intent author/validate (host-neutral TaskIntent drafts)
7
7
  * - `imm-kernel status --json` (read-only v3 legacy shadow status)
8
+ * - `imm-kernel inspect --json` (read-only Inspect Projection)
8
9
  * - `imm-kernel audit --legacy` (explicit read-only legacy audit)
9
10
  * - `imm-plan --routing-status --json` (strict Git-owned route projection)
10
11
  * - `imm-plan <plan-path> [--json]` (read-only Plan validation)
@@ -183,6 +184,7 @@ async function runKernelCli(args: string[], root: string): Promise<{
183
184
  const sub = args[0] ?? "";
184
185
  if (sub === "intent") return runKernelCommand(args, root);
185
186
  if (sub === "status" && args.includes("--json")) return runKernelCommand(args, root);
187
+ if (sub === "inspect" && args.includes("--json")) return runKernelCommand(args, root);
186
188
  if (sub === "audit") {
187
189
  // Explicit read-only legacy audit: bounded, no symlink, deterministic
188
190
  // redacted projection. Never writes journal or workflow state.
@@ -204,7 +206,7 @@ async function runKernelCli(args: string[], root: string): Promise<{
204
206
  }
205
207
  return {
206
208
  stdout: "",
207
- stderr: "invalid_kernel_command: imm-kernel supports intent author|validate, status --json, and audit --legacy only\n",
209
+ stderr: "invalid_kernel_command: imm-kernel supports intent author|validate, status --json, inspect --json, and audit --legacy only\n",
208
210
  returncode: 2,
209
211
  };
210
212
  }
@@ -236,12 +238,13 @@ async function main(argv: string[]): Promise<number> {
236
238
  {
237
239
  name: "imm-kernel",
238
240
  description:
239
- "v4-only Kernel surface: intent author/validate, status, and explicit legacy audit.",
241
+ "v4-only Kernel surface: intent author/validate, status, inspect, and explicit legacy audit.",
240
242
  json_output: true,
241
243
  examples: [
242
244
  "imm-kernel intent author docs/plans/<task-id>.intent.json --stdin --json",
243
245
  "imm-kernel intent validate docs/plans/<task-id>.intent.json --json",
244
246
  "imm-kernel status --json",
247
+ "imm-kernel inspect --json",
245
248
  "imm-kernel audit --legacy",
246
249
  ],
247
250
  },
@@ -126,7 +126,7 @@ export interface GitTaskSnapshot {
126
126
  staged_files: Record<string, GitTaskIndexEntry>;
127
127
  }
128
128
 
129
- const GIT_OBJECT_ID = /^[a-f0-9]{40,64}$/;
129
+ const GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
130
130
  const TASK_GIT_MODES = new Set(["100644", "100755", "120000"] as const);
131
131
  const fatalUtf8 = new TextDecoder("utf-8", { fatal: true });
132
132
  const portablePathCollator = new Intl.Collator("und", {
@@ -195,6 +195,34 @@ function decodeNullPaths(bytes: Buffer, label: string): string[] {
195
195
  return paths;
196
196
  }
197
197
 
198
+ function decodeIndexFlaggedPaths(bytes: Buffer, label: string): Array<{ path: string; flag: "assume-unchanged" | "skip-worktree" }> {
199
+ if (bytes.length === 0) return [];
200
+ if (bytes[bytes.length - 1] !== 0) throw new Error(`${label} is not NUL-terminated`);
201
+ const flagged: Array<{ path: string; flag: "assume-unchanged" | "skip-worktree" }> = [];
202
+ let start = 0;
203
+ for (let index = 0; index < bytes.length; index += 1) {
204
+ if (bytes[index] !== 0) continue;
205
+ const record = bytes.subarray(start, index);
206
+ if (record.length < 3 || record[1] !== 0x20) throw new Error(`${label} contains a malformed entry`);
207
+ const tag = String.fromCharCode(record[0]);
208
+ if (tag === "h" || tag === "S") {
209
+ flagged.push({
210
+ path: decodeCanonicalGitPath(record.subarray(2), label),
211
+ flag: tag === "h" ? "assume-unchanged" : "skip-worktree",
212
+ });
213
+ }
214
+ start = index + 1;
215
+ }
216
+ return flagged;
217
+ }
218
+
219
+ function assertNoScopedIndexFlags(root: string, scope: string[], label: string): void {
220
+ const flagged = decodeIndexFlaggedPaths(gitBytes(root, ["ls-files", "-v", "-z", "--"]), "Git index flags")
221
+ .filter(({ path }) => taskPathMatchesScope(path, scope));
222
+ if (flagged.length > 0)
223
+ throw new Error(`${label} contains unsupported index flags: ${flagged.map(({ path, flag }) => `${path} (${flag})`).join(", ")}`);
224
+ }
225
+
198
226
  function assertNoCaseFoldCollisions(paths: string[], label: string): void {
199
227
  const prefixes: string[] = [];
200
228
  for (const path of paths) {
@@ -300,6 +328,7 @@ function taskSnapshotOnce(root: string, scope: string[]): GitTaskSnapshot {
300
328
  throw new Error("task snapshot does not support sparse checkout or sparse index");
301
329
  if (gitBytes(root, ["ls-files", "--unmerged", "-z"]).length > 0)
302
330
  throw new Error("task snapshot does not support unmerged index entries");
331
+ assertNoScopedIndexFlags(root, scope, "task snapshot");
303
332
 
304
333
  const stagedPaths = decodeNullPaths(
305
334
  gitBytes(root, ["diff", "--cached", "--no-renames", "--name-only", "-z", head, "--"]),
@@ -363,11 +392,168 @@ export function captureGitTaskSnapshot(
363
392
  return before;
364
393
  }
365
394
 
366
- export function taskDiffHash(projectRoot: string, scopeHint: unknown): string {
395
+ export interface GitTaskDiffIdentity {
396
+ diff_hash: string;
397
+ changed_paths: string[];
398
+ }
399
+
400
+ function hashTaskSnapshot(snapshot: object): string {
401
+ return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
402
+ }
403
+
404
+ export function taskDiffIdentity(
405
+ projectRoot: string,
406
+ scopeHint: unknown,
407
+ ): GitTaskDiffIdentity {
367
408
  const snapshot = captureGitTaskSnapshot(projectRoot, scopeHint);
368
- return `sha256:${createHash("sha256")
369
- .update(JSON.stringify(snapshot))
370
- .digest("hex")}`;
409
+ return {
410
+ diff_hash: hashTaskSnapshot(snapshot),
411
+ changed_paths: Object.keys(snapshot.staged_files).sort(comparePaths),
412
+ };
413
+ }
414
+
415
+ export function taskDiffHash(projectRoot: string, scopeHint: unknown): string {
416
+ return taskDiffIdentity(projectRoot, scopeHint).diff_hash;
417
+ }
418
+
419
+ function gitRequired(root: string, args: string[], failure: string): string {
420
+ const output = git(root, args);
421
+ if (output === null) throw new Error(failure);
422
+ return output.trim();
423
+ }
424
+
425
+ /**
426
+ * The v4 scoped revision snapshot: the exact delta between the Enrollment base
427
+ * commit and the current index, restricted to the TaskIntent mutation envelope.
428
+ * It deliberately omits the current HEAD so an out-of-scope commit never
429
+ * invalidates task identity, and it never reads unchanged scope matches.
430
+ */
431
+ export interface GitTaskRevisionSnapshot {
432
+ kind: "git-task-revision-v1";
433
+ repository_root: string;
434
+ base_head: string;
435
+ base_tree: string;
436
+ scope: string[];
437
+ changed_paths: Record<string, GitTaskIndexEntry>;
438
+ }
439
+
440
+ function taskRevisionSnapshotOnce(
441
+ root: string,
442
+ scope: string[],
443
+ baseHead: string,
444
+ ): GitTaskRevisionSnapshot {
445
+ const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
446
+ const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
447
+ if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
448
+ throw new Error("cannot derive a task revision outside a committed Git workspace");
449
+ if (realpathSync(resolve(repositoryRoot)) !== root)
450
+ throw new Error("task revision repository root does not match the project root");
451
+ if (gitRequired(root, ["cat-file", "-t", baseHead], `task revision base is unreadable: ${baseHead}`) !== "commit")
452
+ throw new Error(`task revision base is not a commit: ${baseHead}`);
453
+ if (git(root, ["merge-base", "--is-ancestor", baseHead, head]) === null)
454
+ throw new Error(
455
+ `task revision base ${baseHead} is no longer an ancestor of HEAD; rewrite the task history or re-enroll`,
456
+ );
457
+ const baseTree = gitRequired(root, ["rev-parse", `${baseHead}^{tree}`], "task revision base tree is unreadable");
458
+ if (!GIT_OBJECT_ID.test(baseTree)) throw new Error("task revision base tree has invalid identity");
459
+ const sparseCheckout = git(root, ["config", "--bool", "core.sparseCheckout"])?.trim();
460
+ const sparseIndex = git(root, ["config", "--bool", "index.sparse"])?.trim();
461
+ if (sparseCheckout === "true" || sparseIndex === "true")
462
+ throw new Error("task revision does not support sparse checkout or sparse index");
463
+ if (gitBytes(root, ["ls-files", "--unmerged", "-z"]).length > 0)
464
+ throw new Error("task revision does not support unmerged index entries");
465
+ assertNoScopedIndexFlags(root, scope, "task revision");
466
+
467
+ const stagedPaths = decodeNullPaths(
468
+ gitBytes(root, ["diff", "--cached", "--no-renames", "--name-only", "-z", baseHead, "--"]),
469
+ "task revision paths",
470
+ );
471
+ const unstagedPaths = decodeNullPaths(
472
+ gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", "--"]),
473
+ "unstaged task revision paths",
474
+ );
475
+ const untrackedPaths = decodeNullPaths(
476
+ gitBytes(root, ["ls-files", "--others", "--exclude-standard", "-z", "--"]),
477
+ "untracked task revision paths",
478
+ );
479
+ const scopedStagedPaths = stagedPaths.filter((path) => taskPathMatchesScope(path, scope));
480
+ const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
481
+ const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
482
+ assertNoCaseFoldCollisions(
483
+ [...scopedStagedPaths, ...scopedUnstagedPaths, ...scopedUntrackedPaths],
484
+ "Git task revision paths",
485
+ );
486
+ const drift = [...new Set([...scopedUnstagedPaths, ...scopedUntrackedPaths])].sort(comparePaths);
487
+ if (drift.length > 0)
488
+ throw new Error(`task scope contains unstaged or untracked changes: ${drift.join(", ")}`);
489
+
490
+ const changed = [...new Set(scopedStagedPaths)].sort(comparePaths);
491
+ const changedPaths: Record<string, GitTaskIndexEntry> = {};
492
+ for (const path of changed) {
493
+ const current = indexEntry(root, path);
494
+ const base = headEntry(root, baseHead, path);
495
+ if (!current && !base) throw new Error(`task revision path has no index or base identity: ${path}`);
496
+ if (current && base && current.oid === base.oid && current.mode === base.mode)
497
+ throw new Error(`task revision path is not actually changed: ${path}`);
498
+ changedPaths[path] = {
499
+ status: !base ? "added" : !current ? "deleted" : "modified",
500
+ mode: current?.mode ?? null,
501
+ oid: current?.oid ?? null,
502
+ base_mode: base?.mode ?? null,
503
+ base_oid: base?.oid ?? null,
504
+ };
505
+ }
506
+ return {
507
+ kind: "git-task-revision-v1",
508
+ repository_root: root,
509
+ base_head: baseHead,
510
+ base_tree: baseTree,
511
+ scope,
512
+ changed_paths: changedPaths,
513
+ };
514
+ }
515
+
516
+ export function captureGitTaskRevisionSnapshot(
517
+ projectRoot: string,
518
+ scopeHint: unknown,
519
+ baseHead: unknown,
520
+ ): GitTaskRevisionSnapshot {
521
+ const requestedRoot = resolve(projectRoot);
522
+ const requestedStat = lstatSync(requestedRoot);
523
+ if (requestedStat.isSymbolicLink() || !requestedStat.isDirectory())
524
+ throw new Error("task revision root must be a real directory");
525
+ const root = realpathSync(requestedRoot);
526
+ if (typeof baseHead !== "string" || !GIT_OBJECT_ID.test(baseHead.toLowerCase()))
527
+ throw new Error("task revision base must be a Git commit id");
528
+ const scope = assertCanonicalTaskScope(scopeHint);
529
+ const normalizedBase = baseHead.toLowerCase();
530
+ const before = taskRevisionSnapshotOnce(root, scope, normalizedBase);
531
+ gitTaskSnapshotTestHook?.();
532
+ const after = taskRevisionSnapshotOnce(root, scope, normalizedBase);
533
+ if (JSON.stringify(after) !== JSON.stringify(before))
534
+ throw new Error("Git task revision changed while being captured");
535
+ return before;
536
+ }
537
+
538
+ export function taskRevisionIdentity(
539
+ projectRoot: string,
540
+ scopeHint: unknown,
541
+ baseHead: string,
542
+ ): GitTaskDiffIdentity {
543
+ const snapshot = captureGitTaskRevisionSnapshot(projectRoot, scopeHint, baseHead);
544
+ return {
545
+ diff_hash: hashTaskSnapshot(snapshot),
546
+ changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths),
547
+ };
548
+ }
549
+
550
+ /** The single v4 freshness identity shared by QA, Review, authorization, and completion. */
551
+ export function taskRevisionDiffHash(
552
+ projectRoot: string,
553
+ scopeHint: unknown,
554
+ baseHead: string,
555
+ ): string {
556
+ return taskRevisionIdentity(projectRoot, scopeHint, baseHead).diff_hash;
371
557
  }
372
558
 
373
559
  function isGitWorkspaceSnapshot(value: unknown): value is GitWorkspaceSnapshot {
@@ -17,8 +17,7 @@ a reusable Learning; routine work without that evidence returns `next: none`
17
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-prune` 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
- return `tool_call`, `tool_result`, and `tool_execution_end` evidence to the
21
- Parent before any workflow mutation.
20
+ return a direct result to the Parent before any workflow mutation.
22
21
 
23
22
  At `terminal_plan_complete`, stop with no next skill, authority, or action. At `awaiting_user_successor_decision`, stop with `recommended_authority: user`. This boundary follows the explicit internal Compounder handoff and runtime terminal settlement; it must not dispatch Planner, Compounder, transition, or a new Pi session/subagent. Only a literal user may approve a successor through the native authority gate; the internal runtime token is `--approve-successor`, never a public Skill or user-facing entry.
24
23
 
@@ -39,8 +38,8 @@ asking the user for chat pre-confirmation; the native host interaction is the
39
38
  single authority decision. Do not invoke the removed `imm-canary-work` Skill as
40
39
  a separate entry point. Invalid or contradictory projections fail closed. After
41
40
  implementation and focused verification, freeze the artifacts and call
42
- `advance_assurance`. If it returns `review_ready`, invoke the exact reserved
43
- foreground Agent and call `submit_review`; `request_authorization` remains the
41
+ `advance_assurance`. If it returns `review_ready`, invoke the foreground
42
+ reviewer and pass its structured verdict to `submit_review`; `request_authorization` remains the
44
43
  critical-risk user authorization boundary. Every QA/Review operation stays
45
44
  foreground and returns its next projected obligation directly to the Parent. The host performs any opted-in GitHub Issue projection only after the
46
45
  corresponding authority mutation: only a fresh claimless `done`/`stopped`
@@ -46,6 +46,8 @@ For Medium and High Design Risk, select every materially relevant technical-desi
46
46
 
47
47
  Use the selected design boundaries as one TaskIntent decomposition dimension alongside outcome, Verification, dependency, risk, rollback, compatibility, and authority. Keep one TaskIntent when the selected views describe one coherent executable slice with shared acceptance, risk treatment, rollback, and authority. Split a successor TaskIntent only when a service, state-machine owner, migration, independently promotable layer, or sequence dependency needs independent verification, rollback, authorization, or settlement. Do not split merely because the design names several layers, files, or services.
48
48
 
49
+ Treat trust-boundary changes as the same kind of decomposition evidence. A TaskIntent should normally change one primary trust-boundary invariant; merely traversing several boundaries or updating both sides of one end-to-end authority chain does not require a split. Split separate trust invariants when they can be independently verified, rolled back, authorized, migrated, or settled. Keep multiple trust-boundary changes together only when they form one atomic security outcome and splitting would create an unsafe or unusable intermediate state; record that reason in the Spec. This is Planner judgment, not a TaskIntent schema field or an Enrollment counting rule.
50
+
49
51
  ## Reference Closure Preflight
50
52
 
51
53
  Before authoring a TaskIntent, trace each expected behavior from its public or