immune-brain 3.6.8 → 4.0.0
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 +11 -4
- package/README.zh-CN.md +10 -3
- package/package.json +3 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +18 -2
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -121
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +106 -600
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +1 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +3 -3
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -43
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7581 -5042
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +11 -12
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +27 -25
- package/plugins/immune-brain/dist/imm-planner.md +53 -32
- package/plugins/immune-brain/dist/imm-review-retro.md +123 -0
- package/plugins/immune-brain/dist/registry.yaml +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +3 -1
- package/plugins/immune-brain/dist/role-prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +183 -40
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +240 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +132 -58
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +15 -7
- package/plugins/immune-brain/runtime/assurance/verification.ts +246 -206
- package/plugins/immune-brain/runtime/authorization_operation.ts +20 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +288 -721
- package/plugins/immune-brain/runtime/commands/kernel.ts +158 -67
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +254 -29
- package/plugins/immune-brain/runtime/kernel/actor_identity.ts +33 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +22 -6
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +94 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +27 -6
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +43 -16
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +10 -6
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +50 -63
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +13 -4
- package/plugins/immune-brain/runtime/kernel/completion.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +124 -34
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +13 -5
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/intent.ts +7 -11
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/legacy_task_record.ts +323 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +10 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +32 -31
- package/plugins/immune-brain/runtime/kernel/run_identity.ts +121 -0
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +100 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_migration.ts +950 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_store.ts +1193 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1254 -1206
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +129 -755
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +419 -46
- package/plugins/immune-brain/runtime/kernel/types.ts +12 -43
- package/plugins/immune-brain/runtime/kernel/validation.ts +60 -274
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +0 -1
- package/plugins/immune-brain/runtime/plan_core.ts +27 -65
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +3 -1
- package/plugins/immune-brain/runtime/prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/staged_intent.ts +58 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +37 -7
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +42 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +771 -0
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +189 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +35 -0
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +33 -0
- package/plugins/immune-brain/runtime/unattended/types.ts +14 -1
- package/plugins/immune-brain/runtime/v4_runtime.ts +19 -23
- package/plugins/immune-brain/runtime/verification_descriptor.ts +92 -136
- package/plugins/immune-brain/runtime/workspace_scope.ts +98 -13
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -3
- package/plugins/immune-brain/skills/imm-review-retro/SKILL.md +23 -0
- package/plugins/immune-brain/skills/imm-review-retro/scripts/review_retro.ts +355 -0
- package/plugins/immune-brain/skills/registry.yaml +9 -0
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +0 -4
- package/plugins/immune-brain/bin/imm-retired +0 -4
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +0 -716
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +0 -451
- package/plugins/immune-brain/runtime/kernel/legacy.ts +0 -299
- package/plugins/immune-brain/runtime/kernel/observation.ts +0 -397
- package/plugins/immune-brain/runtime/kernel/readiness.ts +0 -282
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +0 -132
|
@@ -3,11 +3,13 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import {
|
|
4
4
|
existsSync,
|
|
5
5
|
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
6
7
|
readFileSync,
|
|
7
8
|
readlinkSync,
|
|
8
9
|
realpathSync,
|
|
10
|
+
writeFileSync,
|
|
9
11
|
} from "node:fs";
|
|
10
|
-
import { resolve } from "node:path";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
13
|
|
|
12
14
|
export interface GitWorkspaceSnapshot {
|
|
13
15
|
kind: "git-workspace-v1";
|
|
@@ -34,6 +36,75 @@ function comparePaths(left: string, right: string): number {
|
|
|
34
36
|
return 0;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
function isDeliveryAttachment(path: string): boolean {
|
|
40
|
+
return path === ".imm/audit" || path.startsWith(".imm/audit/");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isNonDeliveryPath(path: string): boolean {
|
|
44
|
+
return isDeliveryAttachment(path) || isRuntimeAuthorityPath(path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isOwnPlanningSidecar(path: string, taskId?: string): boolean {
|
|
48
|
+
return Boolean(taskId) && (path === `docs/plans/${taskId}.intent.json` || path === `docs/plans/archive/${taskId}.intent.json`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isOtherTaskSidecar(path: string, taskId?: string): boolean {
|
|
52
|
+
return /^docs\/plans\/(?:archive\/)?[^/]+\.intent\.json$/.test(path) && !isOwnPlanningSidecar(path, taskId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isPlanningNoise(path: string, taskId?: string): boolean {
|
|
56
|
+
return path.startsWith("docs/plans/") && !isOtherTaskSidecar(path, taskId) && !isOwnPlanningSidecar(path, taskId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function enrollmentBaselinePath(root: string): string {
|
|
60
|
+
return join(root, ".imm/state/enrollment-baseline.json");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function writeEnrollmentBaseline(root: string): void {
|
|
64
|
+
const snapshot = captureGitWorkspaceSnapshot(root);
|
|
65
|
+
if (!snapshot) return;
|
|
66
|
+
const path = enrollmentBaselinePath(root);
|
|
67
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
68
|
+
writeFileSync(path, `${JSON.stringify(snapshot)}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readEnrollmentBaseline(root: string): GitWorkspaceSnapshot | null {
|
|
72
|
+
const baselinePath = enrollmentBaselinePath(root);
|
|
73
|
+
if (!existsSync(baselinePath)) return null;
|
|
74
|
+
try {
|
|
75
|
+
const baseline = JSON.parse(readFileSync(baselinePath, "utf8")) as GitWorkspaceSnapshot;
|
|
76
|
+
return isGitWorkspaceSnapshot(baseline) ? baseline : null;
|
|
77
|
+
} catch {
|
|
78
|
+
throw new Error("enrollment baseline is unreadable");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertNoEnvelopeEscape(
|
|
83
|
+
root: string,
|
|
84
|
+
stagedPaths: readonly string[],
|
|
85
|
+
scope: string[],
|
|
86
|
+
taskId?: string,
|
|
87
|
+
): void {
|
|
88
|
+
const baseline = readEnrollmentBaseline(root);
|
|
89
|
+
const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
|
|
90
|
+
if (baseline && !current) throw new Error("enrollment baseline cannot be compared because Git is unavailable");
|
|
91
|
+
const escaped = [...new Set(stagedPaths)]
|
|
92
|
+
.filter((path) => !isNonDeliveryPath(path) && !isOwnPlanningSidecar(path, taskId) && !isPlanningNoise(path, taskId) && !taskPathMatchesScope(path, scope))
|
|
93
|
+
.filter((path) => !baseline || baseline.dirty_files[path] !== current?.dirty_files[path])
|
|
94
|
+
.sort(comparePaths);
|
|
95
|
+
if (escaped.length > 0)
|
|
96
|
+
throw new Error(`task delivery contains paths outside the authorization envelope: ${escaped.join(", ")}`);
|
|
97
|
+
if (!baseline || !current) return;
|
|
98
|
+
const staged = new Set(stagedPaths);
|
|
99
|
+
const mixed = [...new Set([...Object.keys(baseline.dirty_files), ...Object.keys(current.dirty_files)])]
|
|
100
|
+
.filter((path) => !isNonDeliveryPath(path) && !isOwnPlanningSidecar(path, taskId) && !isPlanningNoise(path, taskId) && !taskPathMatchesScope(path, scope))
|
|
101
|
+
.filter((path) => !isOtherTaskSidecar(path, taskId) || staged.has(path))
|
|
102
|
+
.filter((path) => baseline.dirty_files[path] !== current.dirty_files[path])
|
|
103
|
+
.sort(comparePaths);
|
|
104
|
+
if (mixed.length > 0)
|
|
105
|
+
throw new Error(`task delivery contains paths outside the authorization envelope: ${mixed.join(", ")}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
37
108
|
function isRuntimeAuthorityPath(path: string): boolean {
|
|
38
109
|
return (
|
|
39
110
|
// Kernel v2 task state, workspace coordination, and authority journals are
|
|
@@ -315,7 +386,7 @@ function taskPathMatchesScope(path: string, scope: string[]): boolean {
|
|
|
315
386
|
return scope.some((scopePath) => pathMatchesScope(path, scopePath));
|
|
316
387
|
}
|
|
317
388
|
|
|
318
|
-
function taskSnapshotOnce(root: string, scope: string[]): GitTaskSnapshot {
|
|
389
|
+
function taskSnapshotOnce(root: string, scope: string[], taskId?: string): GitTaskSnapshot {
|
|
319
390
|
const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
320
391
|
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
321
392
|
if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
|
|
@@ -343,14 +414,18 @@ function taskSnapshotOnce(root: string, scope: string[]): GitTaskSnapshot {
|
|
|
343
414
|
"untracked task paths",
|
|
344
415
|
);
|
|
345
416
|
assertNoCaseFoldCollisions([...stagedPaths, ...unstagedPaths, ...untrackedPaths], "Git task paths");
|
|
417
|
+
assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
|
|
346
418
|
const uncommittedInScope = [...new Set([...unstagedPaths, ...untrackedPaths])]
|
|
347
419
|
.filter((path) => taskPathMatchesScope(path, scope))
|
|
348
420
|
.sort(comparePaths);
|
|
349
421
|
if (uncommittedInScope.length > 0)
|
|
350
422
|
throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
|
|
351
423
|
|
|
424
|
+
const baseline = readEnrollmentBaseline(root);
|
|
425
|
+
const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
|
|
352
426
|
const taskPaths = [...new Set(stagedPaths)]
|
|
353
|
-
.filter((path) => taskPathMatchesScope(path, scope))
|
|
427
|
+
.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope))
|
|
428
|
+
.filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined))
|
|
354
429
|
.sort(comparePaths);
|
|
355
430
|
const stagedFiles: Record<string, GitTaskIndexEntry> = {};
|
|
356
431
|
for (const path of taskPaths) {
|
|
@@ -377,6 +452,7 @@ function taskSnapshotOnce(root: string, scope: string[]): GitTaskSnapshot {
|
|
|
377
452
|
export function captureGitTaskSnapshot(
|
|
378
453
|
projectRoot: string,
|
|
379
454
|
scopeHint: unknown,
|
|
455
|
+
taskId?: string,
|
|
380
456
|
): GitTaskSnapshot {
|
|
381
457
|
const requestedRoot = resolve(projectRoot);
|
|
382
458
|
const requestedStat = lstatSync(requestedRoot);
|
|
@@ -384,9 +460,9 @@ export function captureGitTaskSnapshot(
|
|
|
384
460
|
throw new Error("task snapshot root must be a real directory");
|
|
385
461
|
const root = realpathSync(requestedRoot);
|
|
386
462
|
const scope = assertCanonicalTaskScope(scopeHint);
|
|
387
|
-
const before = taskSnapshotOnce(root, scope);
|
|
463
|
+
const before = taskSnapshotOnce(root, scope, taskId);
|
|
388
464
|
gitTaskSnapshotTestHook?.();
|
|
389
|
-
const after = taskSnapshotOnce(root, scope);
|
|
465
|
+
const after = taskSnapshotOnce(root, scope, taskId);
|
|
390
466
|
if (JSON.stringify(after) !== JSON.stringify(before))
|
|
391
467
|
throw new Error("Git task snapshot changed while being captured");
|
|
392
468
|
return before;
|
|
@@ -404,16 +480,17 @@ function hashTaskSnapshot(snapshot: object): string {
|
|
|
404
480
|
export function taskDiffIdentity(
|
|
405
481
|
projectRoot: string,
|
|
406
482
|
scopeHint: unknown,
|
|
483
|
+
taskId?: string,
|
|
407
484
|
): GitTaskDiffIdentity {
|
|
408
|
-
const snapshot = captureGitTaskSnapshot(projectRoot, scopeHint);
|
|
485
|
+
const snapshot = captureGitTaskSnapshot(projectRoot, scopeHint, taskId);
|
|
409
486
|
return {
|
|
410
487
|
diff_hash: hashTaskSnapshot(snapshot),
|
|
411
488
|
changed_paths: Object.keys(snapshot.staged_files).sort(comparePaths),
|
|
412
489
|
};
|
|
413
490
|
}
|
|
414
491
|
|
|
415
|
-
export function taskDiffHash(projectRoot: string, scopeHint: unknown): string {
|
|
416
|
-
return taskDiffIdentity(projectRoot, scopeHint).diff_hash;
|
|
492
|
+
export function taskDiffHash(projectRoot: string, scopeHint: unknown, taskId?: string): string {
|
|
493
|
+
return taskDiffIdentity(projectRoot, scopeHint, taskId).diff_hash;
|
|
417
494
|
}
|
|
418
495
|
|
|
419
496
|
function gitRequired(root: string, args: string[], failure: string): string {
|
|
@@ -441,6 +518,7 @@ function taskRevisionSnapshotOnce(
|
|
|
441
518
|
root: string,
|
|
442
519
|
scope: string[],
|
|
443
520
|
baseHead: string,
|
|
521
|
+
taskId?: string,
|
|
444
522
|
): GitTaskRevisionSnapshot {
|
|
445
523
|
const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
446
524
|
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
@@ -476,7 +554,11 @@ function taskRevisionSnapshotOnce(
|
|
|
476
554
|
gitBytes(root, ["ls-files", "--others", "--exclude-standard", "-z", "--"]),
|
|
477
555
|
"untracked task revision paths",
|
|
478
556
|
);
|
|
479
|
-
|
|
557
|
+
assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
|
|
558
|
+
const baseline = readEnrollmentBaseline(root);
|
|
559
|
+
const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
|
|
560
|
+
const scopedStagedPaths = stagedPaths.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope))
|
|
561
|
+
.filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined));
|
|
480
562
|
const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
481
563
|
const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
482
564
|
assertNoCaseFoldCollisions(
|
|
@@ -517,6 +599,7 @@ export function captureGitTaskRevisionSnapshot(
|
|
|
517
599
|
projectRoot: string,
|
|
518
600
|
scopeHint: unknown,
|
|
519
601
|
baseHead: unknown,
|
|
602
|
+
taskId?: string,
|
|
520
603
|
): GitTaskRevisionSnapshot {
|
|
521
604
|
const requestedRoot = resolve(projectRoot);
|
|
522
605
|
const requestedStat = lstatSync(requestedRoot);
|
|
@@ -527,9 +610,9 @@ export function captureGitTaskRevisionSnapshot(
|
|
|
527
610
|
throw new Error("task revision base must be a Git commit id");
|
|
528
611
|
const scope = assertCanonicalTaskScope(scopeHint);
|
|
529
612
|
const normalizedBase = baseHead.toLowerCase();
|
|
530
|
-
const before = taskRevisionSnapshotOnce(root, scope, normalizedBase);
|
|
613
|
+
const before = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId);
|
|
531
614
|
gitTaskSnapshotTestHook?.();
|
|
532
|
-
const after = taskRevisionSnapshotOnce(root, scope, normalizedBase);
|
|
615
|
+
const after = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId);
|
|
533
616
|
if (JSON.stringify(after) !== JSON.stringify(before))
|
|
534
617
|
throw new Error("Git task revision changed while being captured");
|
|
535
618
|
return before;
|
|
@@ -539,8 +622,9 @@ export function taskRevisionIdentity(
|
|
|
539
622
|
projectRoot: string,
|
|
540
623
|
scopeHint: unknown,
|
|
541
624
|
baseHead: string,
|
|
625
|
+
taskId?: string,
|
|
542
626
|
): GitTaskDiffIdentity {
|
|
543
|
-
const snapshot = captureGitTaskRevisionSnapshot(projectRoot, scopeHint, baseHead);
|
|
627
|
+
const snapshot = captureGitTaskRevisionSnapshot(projectRoot, scopeHint, baseHead, taskId);
|
|
544
628
|
return {
|
|
545
629
|
diff_hash: hashTaskSnapshot(snapshot),
|
|
546
630
|
changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths),
|
|
@@ -552,8 +636,9 @@ export function taskRevisionDiffHash(
|
|
|
552
636
|
projectRoot: string,
|
|
553
637
|
scopeHint: unknown,
|
|
554
638
|
baseHead: string,
|
|
639
|
+
taskId?: string,
|
|
555
640
|
): string {
|
|
556
|
-
return taskRevisionIdentity(projectRoot, scopeHint, baseHead).diff_hash;
|
|
641
|
+
return taskRevisionIdentity(projectRoot, scopeHint, baseHead, taskId).diff_hash;
|
|
557
642
|
}
|
|
558
643
|
|
|
559
644
|
function isGitWorkspaceSnapshot(value: unknown): value is GitWorkspaceSnapshot {
|
|
@@ -9,8 +9,8 @@ Use [`../../dist/imm-planner.md`](../../dist/imm-planner.md) as the canonical co
|
|
|
9
9
|
index, not a whole-document read. Explicit entry only: ordinary host requests stay
|
|
10
10
|
host-native.
|
|
11
11
|
|
|
12
|
-
Mandatory constraints before any action: Planner writes
|
|
13
|
-
|
|
12
|
+
Mandatory constraints before any action: Planner writes a TaskIntent, and a Spec
|
|
13
|
+
only for complex work; it never implements, overwrites an enrolled TaskIntent, or
|
|
14
14
|
grants execution authority — only the native Enrollment gate can.
|
|
15
15
|
|
|
16
16
|
Section routes - load a section's instructions only when its branch applies.
|
|
@@ -35,7 +35,7 @@ own routes instead of these stages.
|
|
|
35
35
|
- retirement design: [Retirement Completion Contract](../../dist/imm-planner.md#retirement-completion-contract)
|
|
36
36
|
- optional research dispatch: [Research Dispatch](../../dist/imm-planner.md#research-dispatch)
|
|
37
37
|
|
|
38
|
-
Plan-only requests stop after candidate
|
|
38
|
+
Plan-only requests stop after candidate TaskIntent validation, plus Spec validation when the work is complex. Requests
|
|
39
39
|
that include execution invoke the current Host's native Enrollment gate
|
|
40
40
|
directly, without chat pre-confirmation. Native-gate failure stays fail-closed
|
|
41
41
|
in that Host: report its reason and one retry action only; never suggest
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: imm-review-retro
|
|
3
|
+
description: Use when the user explicitly requests Immune-Brain ranking of models by cross-model review load or a project usage retro.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Immune-Brain: Review Retro
|
|
7
|
+
|
|
8
|
+
Use [`../../dist/imm-review-retro.md`](../../dist/imm-review-retro.md) as the
|
|
9
|
+
canonical contract index, not a whole-document read. This is a
|
|
10
|
+
standalone host-native analysis entry, not a Managed Path continuation
|
|
11
|
+
and not an `imm-loop` internal-role dispatch.
|
|
12
|
+
|
|
13
|
+
Mandatory constraints: read-only on pi session logs. Do not edit code, tests,
|
|
14
|
+
Specs, or workflow state. Do not write session logs or `.imm/` files.
|
|
15
|
+
|
|
16
|
+
Section routes - load a section's instructions only when its branch applies.
|
|
17
|
+
Read each linked heading body up to the next heading; nested sections and
|
|
18
|
+
references load only under their own condition. Never read the whole contract
|
|
19
|
+
or all references as an entry prerequisite.
|
|
20
|
+
|
|
21
|
+
- common: [Boundary](../../dist/imm-review-retro.md#boundary), [Invocation](../../dist/imm-review-retro.md#invocation)
|
|
22
|
+
- running the analyzer: [Counting rules](../../dist/imm-review-retro.md#counting-rules), [CLI](../../dist/imm-review-retro.md#cli)
|
|
23
|
+
- interpreting the report: [Report](../../dist/imm-review-retro.md#report), [Caveats](../../dist/imm-review-retro.md#caveats)
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Retro: how many code reviews did each model's own code trigger, across recent pi sessions.
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun review_retro.ts <days> [--root <sessions-dir>] [--project <substr>] [--top N]
|
|
6
|
+
*
|
|
7
|
+
* Counting rules (the 口径 that keeps the numbers honest):
|
|
8
|
+
* review executed = Agent(subagent_type="Review") tool call
|
|
9
|
+
* avgSc / pass% = average score (0-10) and PASS rate from [SCORE: ...] tags in Review toolResult
|
|
10
|
+
* kernel:submit_review = the *registration* of that same review, reported separately (never added)
|
|
11
|
+
* attribution = the model behind the most recent edit/write before the review (the code's author)
|
|
12
|
+
* findings = imm_kernel_canary record_finding, deduped per session, harness bookkeeping split out
|
|
13
|
+
*/
|
|
14
|
+
import { createReadStream, existsSync, readdirSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { createInterface } from "node:readline";
|
|
18
|
+
import { pathToFileURL } from "node:url";
|
|
19
|
+
|
|
20
|
+
const BOOKKEEPING = /recorded cleanly|receipt recorded|round recorded|no findings?\b/i;
|
|
21
|
+
const EDIT_TOOLS = new Set(["edit", "write", "multiedit"]);
|
|
22
|
+
const SCORE_RE = /\[SCORE:\s*([\d.]+)\s*(?:\/\s*10)?\]/i;
|
|
23
|
+
const VERDICT_RE = /\[VERDICT:\s*(\w+)\]/i;
|
|
24
|
+
const RISK_RE = /\[RISK:\s*(\w+)\]/i;
|
|
25
|
+
const BLOCK_RE = /\[BLOCKING:\s*(\d+)\]/i;
|
|
26
|
+
const ADVIS_RE = /\[ADVISORY:\s*(\d+)\]/i;
|
|
27
|
+
|
|
28
|
+
type ReviewTag = {
|
|
29
|
+
score: number;
|
|
30
|
+
verdict: string;
|
|
31
|
+
risk: string;
|
|
32
|
+
blocking: number;
|
|
33
|
+
advisory: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function parseReviewTag(text: string): ReviewTag | null {
|
|
37
|
+
if (!text.includes("[SCORE:")) return null;
|
|
38
|
+
const sc = SCORE_RE.exec(text);
|
|
39
|
+
if (!sc) return null;
|
|
40
|
+
const score = Number(sc[1]);
|
|
41
|
+
if (!Number.isFinite(score)) return null;
|
|
42
|
+
return {
|
|
43
|
+
score,
|
|
44
|
+
verdict: (VERDICT_RE.exec(text)?.[1] ?? "UNKNOWN").toUpperCase(),
|
|
45
|
+
risk: (RISK_RE.exec(text)?.[1] ?? "UNKNOWN").toUpperCase(),
|
|
46
|
+
blocking: Number(BLOCK_RE.exec(text)?.[1] ?? 0),
|
|
47
|
+
advisory: Number(ADVIS_RE.exec(text)?.[1] ?? 0),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractText(content: unknown): string {
|
|
52
|
+
if (typeof content === "string") return content;
|
|
53
|
+
if (Array.isArray(content)) {
|
|
54
|
+
return content
|
|
55
|
+
.map((item) => {
|
|
56
|
+
if (typeof item === "string") return item;
|
|
57
|
+
if (item && typeof item === "object") {
|
|
58
|
+
const rec = item as Record<string, unknown>;
|
|
59
|
+
if ("text" in rec) return String(rec.text);
|
|
60
|
+
if ("result" in rec) return String(rec.result);
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
})
|
|
64
|
+
.join("\n");
|
|
65
|
+
}
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function norm(a: unknown): Record<string, unknown> {
|
|
70
|
+
if (typeof a === "string") {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(a) as Record<string, unknown>;
|
|
73
|
+
} catch {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return a && typeof a === "object" ? (a as Record<string, unknown>) : {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function bump(map: Map<string, number>, key: string, n = 1): void {
|
|
81
|
+
map.set(key, (map.get(key) ?? 0) + n);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type Args = { days: number; root: string; project: string; top: number };
|
|
85
|
+
|
|
86
|
+
function parseArgs(argv: string[]): Args {
|
|
87
|
+
const out: Args = {
|
|
88
|
+
days: NaN,
|
|
89
|
+
root: join(homedir(), ".pi/agent/sessions"),
|
|
90
|
+
project: "",
|
|
91
|
+
top: 15,
|
|
92
|
+
};
|
|
93
|
+
const rest: string[] = [];
|
|
94
|
+
for (let i = 0; i < argv.length; i++) {
|
|
95
|
+
const a = argv[i]!;
|
|
96
|
+
if (a === "--root") out.root = argv[++i] ?? "";
|
|
97
|
+
else if (a === "--project") out.project = argv[++i] ?? "";
|
|
98
|
+
else if (a === "--top") out.top = Number(argv[++i]);
|
|
99
|
+
else if (a.startsWith("-")) throw new Error(`unknown flag: ${a}`);
|
|
100
|
+
else rest.push(a);
|
|
101
|
+
}
|
|
102
|
+
out.days = Number(rest[0]);
|
|
103
|
+
if (!(out.days > 0) || rest[1] !== undefined) {
|
|
104
|
+
throw new Error("usage: review_retro.ts <days> [--root <dir>] [--project <substr>] [--top N]");
|
|
105
|
+
}
|
|
106
|
+
if (!Number.isFinite(out.top) || out.top <= 0) throw new Error("--top must be > 0");
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function listJsonl(root: string): string[] {
|
|
111
|
+
if (!existsSync(root)) return [];
|
|
112
|
+
const paths: string[] = [];
|
|
113
|
+
for (const dir of readdirSync(root, { withFileTypes: true })) {
|
|
114
|
+
if (!dir.isDirectory()) continue;
|
|
115
|
+
const folder = join(root, dir.name);
|
|
116
|
+
for (const name of readdirSync(folder)) {
|
|
117
|
+
if (name.endsWith(".jsonl")) paths.push(join(folder, name));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return paths;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function run(argv: string[]): Promise<string> {
|
|
124
|
+
const args = parseArgs(argv);
|
|
125
|
+
const cut = new Date(Date.now() - args.days * 86400000).toISOString().slice(0, 19);
|
|
126
|
+
const home = homedir();
|
|
127
|
+
const dev = new Map<string, number>();
|
|
128
|
+
const turns = new Map<string, number>();
|
|
129
|
+
const rev = new Map<string, number>();
|
|
130
|
+
const sub = new Map<string, number>();
|
|
131
|
+
const tools = new Map<string, number>();
|
|
132
|
+
const proj = new Map<string, number>();
|
|
133
|
+
const episode = new Map<string, Set<string>>();
|
|
134
|
+
const tasks = new Map<string, Set<string>>();
|
|
135
|
+
const findUniq = new Map<string, Set<string>>();
|
|
136
|
+
const findingsRaw = new Map<string, number>();
|
|
137
|
+
const scores = new Map<string, number[]>();
|
|
138
|
+
const verdicts = new Map<string, number>();
|
|
139
|
+
const risks = new Map<string, number>();
|
|
140
|
+
let files = 0;
|
|
141
|
+
|
|
142
|
+
for (const path of listJsonl(args.root)) {
|
|
143
|
+
let editor: string | null = null;
|
|
144
|
+
let cwd = "";
|
|
145
|
+
let used = false;
|
|
146
|
+
const pending = new Map<string, string>();
|
|
147
|
+
const rl = createInterface({ input: createReadStream(path, { encoding: "utf8" }) });
|
|
148
|
+
for await (const raw of rl) {
|
|
149
|
+
const line = raw.trim();
|
|
150
|
+
if (!line) continue;
|
|
151
|
+
let o: Record<string, unknown>;
|
|
152
|
+
try {
|
|
153
|
+
o = JSON.parse(line) as Record<string, unknown>;
|
|
154
|
+
} catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (o.type === "session") {
|
|
158
|
+
cwd = String(o.cwd ?? "");
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const m = (o.message && typeof o.message === "object" ? o.message : {}) as Record<string, unknown>;
|
|
162
|
+
const role = m.role;
|
|
163
|
+
if (String(o.timestamp ?? "") < cut) continue;
|
|
164
|
+
if (args.project && !cwd.includes(args.project)) continue;
|
|
165
|
+
|
|
166
|
+
if (role === "assistant") {
|
|
167
|
+
used = true;
|
|
168
|
+
const mo = `${m.provider ?? "?"}/${m.model ?? "?"}`;
|
|
169
|
+
bump(turns, mo);
|
|
170
|
+
const content = Array.isArray(m.content) ? m.content : [];
|
|
171
|
+
for (const c of content) {
|
|
172
|
+
if (!c || typeof c !== "object") continue;
|
|
173
|
+
const call = c as Record<string, unknown>;
|
|
174
|
+
if (call.type !== "toolCall") continue;
|
|
175
|
+
const name = String(call.name ?? "");
|
|
176
|
+
const a = norm(call.arguments);
|
|
177
|
+
const cid = typeof call.id === "string" ? call.id : "";
|
|
178
|
+
bump(tools, name || "?");
|
|
179
|
+
if (EDIT_TOOLS.has(name)) {
|
|
180
|
+
bump(dev, mo);
|
|
181
|
+
editor = mo;
|
|
182
|
+
} else if (name === "Agent" && a.subagent_type === "Review") {
|
|
183
|
+
const owner = editor ?? "no-edit (review-only)";
|
|
184
|
+
bump(rev, owner);
|
|
185
|
+
const ep = episode.get(owner) ?? new Set();
|
|
186
|
+
ep.add(`${path}\0${String(a.description ?? "")}${String(a.prompt ?? "").slice(0, 240)}`);
|
|
187
|
+
episode.set(owner, ep);
|
|
188
|
+
const projKey = `${cwd.replace(home, "~")}\0${owner}`;
|
|
189
|
+
bump(proj, projKey);
|
|
190
|
+
if (cid) pending.set(cid, owner);
|
|
191
|
+
} else if (name === "imm_kernel_canary") {
|
|
192
|
+
const act = norm(a.action);
|
|
193
|
+
const op = act.op;
|
|
194
|
+
const owner = editor ?? "no-edit (review-only)";
|
|
195
|
+
if (op === "submit_review") {
|
|
196
|
+
bump(sub, owner);
|
|
197
|
+
const t = tasks.get(owner) ?? new Set();
|
|
198
|
+
t.add(`${cwd}\0${String(a.task_id ?? "")}`);
|
|
199
|
+
tasks.set(owner, t);
|
|
200
|
+
} else if (op === "record_finding") {
|
|
201
|
+
const f = norm(act.finding);
|
|
202
|
+
const summ = String(f.summary ?? "");
|
|
203
|
+
const kind = BOOKKEEPING.test(summ) ? "bookkeeping" : String(f.kind ?? "");
|
|
204
|
+
const rawKey = `${owner}\0${kind}`;
|
|
205
|
+
bump(findingsRaw, rawKey);
|
|
206
|
+
const uniq = findUniq.get(rawKey) ?? new Set();
|
|
207
|
+
uniq.add(`${path}\0${summ.slice(0, 160)}`);
|
|
208
|
+
findUniq.set(rawKey, uniq);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else if (role === "toolResult") {
|
|
213
|
+
const tcid = String(m.toolCallId ?? "");
|
|
214
|
+
const owner = pending.get(tcid);
|
|
215
|
+
if (!owner) continue;
|
|
216
|
+
pending.delete(tcid);
|
|
217
|
+
const tag = parseReviewTag(extractText(m.content));
|
|
218
|
+
if (!tag) continue;
|
|
219
|
+
const sl = scores.get(owner) ?? [];
|
|
220
|
+
sl.push(tag.score);
|
|
221
|
+
scores.set(owner, sl);
|
|
222
|
+
bump(verdicts, `${owner}\0${tag.verdict}`);
|
|
223
|
+
bump(risks, `${owner}\0${tag.risk}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (used) files += 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const lines: string[] = [];
|
|
230
|
+
const emit = (s = "") => lines.push(s);
|
|
231
|
+
emit(`window: last ${args.days}d (UTC >= ${cut}Z) | sessions with activity: ${files} | root: ${args.root}`);
|
|
232
|
+
emit("review = Agent(Review) executed; attributed to the model that last edited the code under review");
|
|
233
|
+
emit("avgSc/pass% = parsed from Review [SCORE: .../10] [VERDICT: ...] tags (shows '-' if untagged)");
|
|
234
|
+
emit("");
|
|
235
|
+
const hdr =
|
|
236
|
+
`${"model".padEnd(42)}${"devEdits".padStart(9)}${"turns".padStart(6)}${"reviews".padStart(8)}${"uniq".padStart(5)}${"rev/100ed".padStart(10)}${"avgSc".padStart(6)}${"pass%".padStart(6)}${"registr".padStart(8)}${"block".padStart(6)}${"advis".padStart(6)}${"noisy".padStart(6)}`;
|
|
237
|
+
emit(hdr);
|
|
238
|
+
emit("-".repeat(hdr.length));
|
|
239
|
+
|
|
240
|
+
const models = [...new Set([...rev.keys(), ...dev.keys()])].sort(
|
|
241
|
+
(a, b) => (rev.get(b) ?? 0) - (rev.get(a) ?? 0),
|
|
242
|
+
);
|
|
243
|
+
const shown: string[] = [];
|
|
244
|
+
for (const mo of models) {
|
|
245
|
+
if (!rev.get(mo) && (dev.get(mo) ?? 0) < 30) continue;
|
|
246
|
+
shown.push(mo);
|
|
247
|
+
const uq = episode.get(mo)?.size ?? 0;
|
|
248
|
+
const d = dev.get(mo) ?? 0;
|
|
249
|
+
const r = rev.get(mo) ?? 0;
|
|
250
|
+
const rate = d ? ((100 * r) / d).toFixed(1) : "-";
|
|
251
|
+
const sl = scores.get(mo) ?? [];
|
|
252
|
+
const avgSc = sl.length ? (sl.reduce((x, y) => x + y, 0) / sl.length).toFixed(1) : "-";
|
|
253
|
+
const passCnt = verdicts.get(`${mo}\0PASS`) ?? 0;
|
|
254
|
+
const passPct = sl.length ? `${Math.round((100 * passCnt) / sl.length)}%` : "-";
|
|
255
|
+
const block = findUniq.get(`${mo}\0blocking`)?.size ?? 0;
|
|
256
|
+
const advis = findUniq.get(`${mo}\0advisory`)?.size ?? 0;
|
|
257
|
+
const noisy = findUniq.get(`${mo}\0bookkeeping`)?.size ?? 0;
|
|
258
|
+
emit(
|
|
259
|
+
`${mo.padEnd(42)}${String(d).padStart(9)}${String(turns.get(mo) ?? 0).padStart(6)}${String(r).padStart(8)}${String(uq).padStart(5)}${rate.padStart(10)}${avgSc.padStart(6)}${passPct.padStart(6)}${String(sub.get(mo) ?? 0).padStart(8)}${String(block).padStart(6)}${String(advis).padStart(6)}${String(noisy).padStart(6)}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
emit("-".repeat(hdr.length));
|
|
263
|
+
const totalScores = [...scores.values()].flat();
|
|
264
|
+
const totAvg = totalScores.length
|
|
265
|
+
? (totalScores.reduce((x, y) => x + y, 0) / totalScores.length).toFixed(1)
|
|
266
|
+
: "-";
|
|
267
|
+
const totPass = shown.reduce((n, mo) => n + (verdicts.get(`${mo}\0PASS`) ?? 0), 0);
|
|
268
|
+
const totPassPct = totalScores.length ? `${Math.round((100 * totPass) / totalScores.length)}%` : "-";
|
|
269
|
+
const totBlock = [...findUniq.entries()].reduce((n, [k, v]) => n + (k.endsWith("\0blocking") ? v.size : 0), 0);
|
|
270
|
+
const totAdvis = [...findUniq.entries()].reduce((n, [k, v]) => n + (k.endsWith("\0advisory") ? v.size : 0), 0);
|
|
271
|
+
const totNoisy = [...findUniq.entries()].reduce((n, [k, v]) => n + (k.endsWith("\0bookkeeping") ? v.size : 0), 0);
|
|
272
|
+
const totDev = [...dev.values()].reduce((a, b) => a + b, 0);
|
|
273
|
+
const totTurns = [...turns.values()].reduce((a, b) => a + b, 0);
|
|
274
|
+
const totRev = [...rev.values()].reduce((a, b) => a + b, 0);
|
|
275
|
+
const totUniq = [...episode.values()].reduce((n, s) => n + s.size, 0);
|
|
276
|
+
const totSub = [...sub.values()].reduce((a, b) => a + b, 0);
|
|
277
|
+
emit(
|
|
278
|
+
`${"TOTAL".padEnd(42)}${String(totDev).padStart(9)}${String(totTurns).padStart(6)}${String(totRev).padStart(8)}${String(totUniq).padStart(5)}${"".padStart(10)}${totAvg.padStart(6)}${totPassPct.padStart(6)}${String(totSub).padStart(8)}${String(totBlock).padStart(6)}${String(totAdvis).padStart(6)}${String(totNoisy).padStart(6)}`,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
emit("");
|
|
282
|
+
emit("=== usage (sessions / turns / edits / tools) ===");
|
|
283
|
+
emit(`sessions: ${files} | turns: ${totTurns} | edits: ${totDev}`);
|
|
284
|
+
const toolRows = [...tools.entries()].sort((a, b) => b[1] - a[1]);
|
|
285
|
+
if (toolRows.length === 0) emit("(no tool calls in this window)");
|
|
286
|
+
else for (const [name, n] of toolRows) emit(`${String(n).padStart(5)} ${name}`);
|
|
287
|
+
|
|
288
|
+
emit("");
|
|
289
|
+
emit("=== review quality & scores (new rubric) ===");
|
|
290
|
+
const scoredModels = [...scores.keys()]
|
|
291
|
+
.filter((m) => (scores.get(m) ?? []).length > 0)
|
|
292
|
+
.sort((a, b) => (scores.get(b)?.length ?? 0) - (scores.get(a)?.length ?? 0));
|
|
293
|
+
if (scoredModels.length) {
|
|
294
|
+
const qHdr = `${"model".padEnd(42)}${"scored".padStart(7)}${"avgScore".padStart(9)}${"PASS".padStart(6)}${"REVISE".padStart(7)}${"REJECT".padStart(7)}${"highRisk".padStart(9)}`;
|
|
295
|
+
emit(qHdr);
|
|
296
|
+
emit("-".repeat(qHdr.length));
|
|
297
|
+
for (const mo of scoredModels) {
|
|
298
|
+
const sl = scores.get(mo) ?? [];
|
|
299
|
+
const avgS = (sl.reduce((x, y) => x + y, 0) / sl.length).toFixed(2);
|
|
300
|
+
const pC = verdicts.get(`${mo}\0PASS`) ?? 0;
|
|
301
|
+
const revC = verdicts.get(`${mo}\0REVISE`) ?? 0;
|
|
302
|
+
const rejC = verdicts.get(`${mo}\0REJECT`) ?? 0;
|
|
303
|
+
const highR = (risks.get(`${mo}\0HIGH`) ?? 0) + (risks.get(`${mo}\0CRITICAL`) ?? 0);
|
|
304
|
+
emit(
|
|
305
|
+
`${mo.padEnd(42)}${String(sl.length).padStart(7)}${avgS.padStart(9)}${String(pC).padStart(6)}${String(revC).padStart(7)}${String(rejC).padStart(7)}${String(highR).padStart(9)}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
emit("(no scored reviews found in this window yet; reviews with [SCORE: .../10] will appear here)");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
emit("");
|
|
313
|
+
emit("=== where the reviews landed (project x author) ===");
|
|
314
|
+
const projRows = [...proj.entries()].sort((a, b) => b[1] - a[1]).slice(0, args.top);
|
|
315
|
+
for (const [key, v] of projRows) {
|
|
316
|
+
const [p, mo] = key.split("\0");
|
|
317
|
+
emit(`${String(v).padStart(5)} ${String(p).padEnd(58)} ${mo}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
emit("");
|
|
321
|
+
emit("=== review rounds per kernel task (rework signal) ===");
|
|
322
|
+
const taskModels = [...tasks.keys()].sort((a, b) => (sub.get(b) ?? 0) - (sub.get(a) ?? 0));
|
|
323
|
+
for (const mo of taskModels) {
|
|
324
|
+
const nt = tasks.get(mo)?.size ?? 0;
|
|
325
|
+
if (!nt) continue;
|
|
326
|
+
const s = sub.get(mo) ?? 0;
|
|
327
|
+
emit(
|
|
328
|
+
`${(s / nt).toFixed(1).padStart(6)} rounds/task ${String(s).padStart(4)} registrations / ${String(nt).padStart(3)} tasks ${mo}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const rawSum = [...findingsRaw.values()].reduce((a, b) => a + b, 0);
|
|
333
|
+
const uniqSum = [...findUniq.values()].reduce((n, s) => n + s.size, 0);
|
|
334
|
+
if (rawSum && rawSum !== uniqSum) {
|
|
335
|
+
emit("");
|
|
336
|
+
emit(
|
|
337
|
+
`(findings: ${rawSum} raw calls deduped to ${uniqSum} distinct — re-submits of the same finding are counted once)`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
emit("caveats: 'bookkeep' = kernel self-entries mislabelled as findings, excluded from blocking/advisory;");
|
|
341
|
+
emit(" high rounds/task can be canary/QA harness re-registration, not human-visible rework.");
|
|
342
|
+
return `${lines.join("\n")}\n`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const isMain = Boolean(process.argv[1]) && pathToFileURL(process.argv[1]!).href === import.meta.url;
|
|
346
|
+
if (isMain) {
|
|
347
|
+
run(process.argv.slice(2))
|
|
348
|
+
.then((text) => {
|
|
349
|
+
process.stdout.write(text);
|
|
350
|
+
})
|
|
351
|
+
.catch((err: unknown) => {
|
|
352
|
+
console.error(err);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
@@ -56,3 +56,12 @@ skills:
|
|
|
56
56
|
output_artifacts: [maintain_report]
|
|
57
57
|
next_actions: []
|
|
58
58
|
boundary: Minimize tracked agent-instruction context after explicit manifest approval; no Managed authority mutation, contract installation, or reference-document creation.
|
|
59
|
+
- name: imm-review-retro
|
|
60
|
+
path: skills/imm-review-retro/SKILL.md
|
|
61
|
+
role: execute
|
|
62
|
+
title: Review Retro
|
|
63
|
+
role_class: discovery
|
|
64
|
+
canonical: true
|
|
65
|
+
output_artifacts: [retro_report]
|
|
66
|
+
next_actions: []
|
|
67
|
+
boundary: Rank models by review load and report project usage from pi session logs. Read-only. No Managed Path mutation.
|