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.
- package/.claude-plugin/marketplace.json +16 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +9 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
- package/plugins/immune-brain/.mcp.json +8 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
- package/plugins/immune-brain/dist/imm-loop.md +6 -7
- package/plugins/immune-brain/dist/imm-planner.md +1 -1
- package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
- package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +55 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
- package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
- package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
- package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
- package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
- package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
- package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +28 -13
- package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
- package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
- package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
- package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
- package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
- package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
- package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
- package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
- 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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
|
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
|
|
369
|
-
|
|
370
|
-
.
|
|
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
|
|
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
|
|
43
|
-
|
|
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
|