immune-brain 3.6.4 → 3.6.5
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/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +45 -18
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +147 -48
- package/plugins/immune-brain/dist/imm-loop.md +8 -2
- package/plugins/immune-brain/dist/imm-planner.md +9 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +18 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +31 -9
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +14 -1
- package/plugins/immune-brain/runtime/commands/kernel.ts +15 -13
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +94 -3
- package/plugins/immune-brain/runtime/kernel/application.ts +1 -0
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +407 -0
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +17 -8
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +72 -13
- package/plugins/immune-brain/runtime/kernel/intent.ts +67 -23
- package/plugins/immune-brain/runtime/kernel/reducer.ts +34 -8
- package/plugins/immune-brain/runtime/kernel/types.ts +1 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +10 -6
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
mintToken,
|
|
29
29
|
type TaskIntentIdentityToken,
|
|
30
|
+
type TokenIdentity,
|
|
30
31
|
} from "./intent_token_registry";
|
|
31
32
|
|
|
32
33
|
export const INTENT_MAX_BYTES = 64 * 1024;
|
|
@@ -406,6 +407,26 @@ export interface ReadTaskIntentResult {
|
|
|
406
407
|
token: TaskIntentIdentityToken;
|
|
407
408
|
}
|
|
408
409
|
|
|
410
|
+
export interface ObservedTaskIntent {
|
|
411
|
+
intent: TaskIntentV1;
|
|
412
|
+
content_hash: string;
|
|
413
|
+
intent_ref: TaskIntentRefV1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export class TaskIntentObservationError extends Error {
|
|
417
|
+
readonly code: "missing" | "invalid";
|
|
418
|
+
|
|
419
|
+
constructor(code: "missing" | "invalid", message: string) {
|
|
420
|
+
super(message);
|
|
421
|
+
this.name = "TaskIntentObservationError";
|
|
422
|
+
this.code = code;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
interface TaskIntentReadSource extends ObservedTaskIntent {
|
|
427
|
+
identity: TokenIdentity;
|
|
428
|
+
}
|
|
429
|
+
|
|
409
430
|
export function setIntentReaderTestHook(
|
|
410
431
|
hook: { onBeforeDescriptorRead?: () => void } | null,
|
|
411
432
|
): void {
|
|
@@ -523,11 +544,11 @@ function assertIdentitiesUnchanged(
|
|
|
523
544
|
}
|
|
524
545
|
}
|
|
525
546
|
|
|
526
|
-
|
|
547
|
+
function readTaskIntentSource(
|
|
527
548
|
root: string,
|
|
528
549
|
taskId: string,
|
|
529
550
|
requestedPath?: string,
|
|
530
|
-
):
|
|
551
|
+
): TaskIntentReadSource {
|
|
531
552
|
validateTaskId(taskId);
|
|
532
553
|
|
|
533
554
|
const canonicalRoot = resolveCanonicalRoot(root);
|
|
@@ -545,7 +566,7 @@ export function readTaskIntent(
|
|
|
545
566
|
if (!target.startsWith(canonicalRoot + sep))
|
|
546
567
|
throw new Error("intent sidecar escapes project root");
|
|
547
568
|
if (!sidecarPresent(canonicalRoot, sidecarPath))
|
|
548
|
-
throw new
|
|
569
|
+
throw new TaskIntentObservationError("missing", `TaskIntent sidecar is missing at ${sidecarPath}`);
|
|
549
570
|
|
|
550
571
|
const pathIdentities = collectPathIdentities(canonicalRoot, sidecarPath);
|
|
551
572
|
const fileIdentity = pathIdentities[pathIdentities.length - 1];
|
|
@@ -557,13 +578,19 @@ export function readTaskIntent(
|
|
|
557
578
|
["ls-files", "--error-unmatch", "--", sidecarPath],
|
|
558
579
|
{ cwd: canonicalRoot, stdio: ["ignore", "pipe", "pipe"] },
|
|
559
580
|
);
|
|
560
|
-
} catch {
|
|
561
|
-
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (
|
|
583
|
+
typeof error === "object"
|
|
584
|
+
&& error !== null
|
|
585
|
+
&& "status" in error
|
|
586
|
+
&& (error as { status?: unknown }).status === 1
|
|
587
|
+
) throw new TaskIntentObservationError("invalid", "TaskIntent sidecar is not Git-tracked");
|
|
588
|
+
throw error;
|
|
562
589
|
}
|
|
563
590
|
|
|
564
591
|
const before = lstatSync(target);
|
|
565
592
|
if (!before.isFile() || before.size > INTENT_MAX_BYTES)
|
|
566
|
-
throw new
|
|
593
|
+
throw new TaskIntentObservationError("invalid", "TaskIntent sidecar must be a regular file no larger than 64 KiB");
|
|
567
594
|
|
|
568
595
|
const fd = openSync(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
569
596
|
let bytes: Buffer;
|
|
@@ -576,7 +603,7 @@ export function readTaskIntent(
|
|
|
576
603
|
closeSync(fd);
|
|
577
604
|
}
|
|
578
605
|
if (bytes.byteLength > INTENT_MAX_BYTES)
|
|
579
|
-
throw new
|
|
606
|
+
throw new TaskIntentObservationError("invalid", "TaskIntent sidecar exceeds 64 KiB");
|
|
580
607
|
|
|
581
608
|
// Post-read identity re-verification without a second path read as the
|
|
582
609
|
// source of bytes.
|
|
@@ -594,27 +621,15 @@ export function readTaskIntent(
|
|
|
594
621
|
try {
|
|
595
622
|
intent = parseTaskIntentV1(JSON.parse(bytes.toString("utf8")));
|
|
596
623
|
} catch (error) {
|
|
597
|
-
throw new
|
|
624
|
+
throw new TaskIntentObservationError(
|
|
625
|
+
"invalid",
|
|
598
626
|
`TaskIntent sidecar is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
599
627
|
);
|
|
600
628
|
}
|
|
601
629
|
if (intent.task_id !== taskId)
|
|
602
|
-
throw new
|
|
630
|
+
throw new TaskIntentObservationError("invalid", "intent.task_id does not match the sidecar filename task id");
|
|
603
631
|
|
|
604
632
|
const contentHash = canonicalIntentHash(intent);
|
|
605
|
-
const token = mintToken({
|
|
606
|
-
canonical_root: canonicalRoot,
|
|
607
|
-
sidecar_path: sidecarPath,
|
|
608
|
-
path_dev: fileIdentity.dev,
|
|
609
|
-
path_ino: fileIdentity.ino,
|
|
610
|
-
fd_dev: before.dev,
|
|
611
|
-
fd_ino: before.ino,
|
|
612
|
-
fd_size: before.size,
|
|
613
|
-
fd_mtime_ms: before.mtimeMs,
|
|
614
|
-
source_bytes_sha256: sourceBytesSha256,
|
|
615
|
-
intent_content_hash: contentHash,
|
|
616
|
-
});
|
|
617
|
-
|
|
618
633
|
return {
|
|
619
634
|
intent,
|
|
620
635
|
content_hash: contentHash,
|
|
@@ -623,6 +638,35 @@ export function readTaskIntent(
|
|
|
623
638
|
revision: intent.revision,
|
|
624
639
|
content_hash: contentHash,
|
|
625
640
|
},
|
|
626
|
-
|
|
641
|
+
identity: {
|
|
642
|
+
canonical_root: canonicalRoot,
|
|
643
|
+
sidecar_path: sidecarPath,
|
|
644
|
+
path_dev: fileIdentity.dev,
|
|
645
|
+
path_ino: fileIdentity.ino,
|
|
646
|
+
fd_dev: before.dev,
|
|
647
|
+
fd_ino: before.ino,
|
|
648
|
+
fd_size: before.size,
|
|
649
|
+
fd_mtime_ms: before.mtimeMs,
|
|
650
|
+
source_bytes_sha256: sourceBytesSha256,
|
|
651
|
+
intent_content_hash: contentHash,
|
|
652
|
+
},
|
|
627
653
|
};
|
|
628
654
|
}
|
|
655
|
+
|
|
656
|
+
export function observeTaskIntent(
|
|
657
|
+
root: string,
|
|
658
|
+
taskId: string,
|
|
659
|
+
requestedPath?: string,
|
|
660
|
+
): ObservedTaskIntent {
|
|
661
|
+
const { identity: _identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
|
|
662
|
+
return observed;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export function readTaskIntent(
|
|
666
|
+
root: string,
|
|
667
|
+
taskId: string,
|
|
668
|
+
requestedPath?: string,
|
|
669
|
+
): ReadTaskIntentResult {
|
|
670
|
+
const { identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
|
|
671
|
+
return { ...observed, token: mintToken(identity) };
|
|
672
|
+
}
|
|
@@ -178,6 +178,7 @@ function hasPrivilegedKind(action: TaskAction): boolean {
|
|
|
178
178
|
action.type === "record_approval" ||
|
|
179
179
|
action.type === "approve_breaking_intent_revision" ||
|
|
180
180
|
action.type === "request_rework" ||
|
|
181
|
+
action.type === "authorize_rework" ||
|
|
181
182
|
action.type === "stop" ||
|
|
182
183
|
action.type === "resolve_user_decision"
|
|
183
184
|
);
|
|
@@ -449,13 +450,16 @@ export function reduceTask(
|
|
|
449
450
|
"request_rework requires review, qa, or user authority",
|
|
450
451
|
]);
|
|
451
452
|
const round = reviewRound(record);
|
|
452
|
-
const
|
|
453
|
-
(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
453
|
+
const hasPriorBlockingReviewRework = record.findings.some(
|
|
454
|
+
(finding) =>
|
|
455
|
+
finding.source === "review" &&
|
|
456
|
+
finding.kind === "blocking" &&
|
|
457
|
+
finding.review_round !== null,
|
|
458
|
+
);
|
|
457
459
|
const parkForReplan =
|
|
458
|
-
authorityAudit.authority_kind === "review" &&
|
|
460
|
+
authorityAudit.authority_kind === "review" &&
|
|
461
|
+
hasPriorBlockingReviewRework &&
|
|
462
|
+
action.findings.some((finding) => finding.kind === "blocking");
|
|
459
463
|
if (!parkForReplan) {
|
|
460
464
|
record.artifact_state = "active";
|
|
461
465
|
record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
|
|
@@ -470,8 +474,8 @@ export function reduceTask(
|
|
|
470
474
|
record.findings.push({
|
|
471
475
|
...finding,
|
|
472
476
|
status: "open",
|
|
473
|
-
source: "review",
|
|
474
|
-
review_round: round,
|
|
477
|
+
source: authorityAudit.authority_kind === "review" ? "review" : "execution",
|
|
478
|
+
review_round: authorityAudit.authority_kind === "review" ? round : null,
|
|
475
479
|
});
|
|
476
480
|
}
|
|
477
481
|
if (
|
|
@@ -502,6 +506,28 @@ export function reduceTask(
|
|
|
502
506
|
appendHistory(record, action, from, `review_round_${round}`, authorityAudit);
|
|
503
507
|
break;
|
|
504
508
|
}
|
|
509
|
+
case "authorize_rework": {
|
|
510
|
+
if (record.lifecycle !== "active")
|
|
511
|
+
throw new KernelInvariantError([
|
|
512
|
+
`cannot authorize rework while lifecycle is ${record.lifecycle}`,
|
|
513
|
+
]);
|
|
514
|
+
if (authorityAudit?.authority_kind !== "user")
|
|
515
|
+
throw new KernelInvariantError([
|
|
516
|
+
"authorize_rework requires literal-user authority",
|
|
517
|
+
]);
|
|
518
|
+
const open = record.findings.filter(
|
|
519
|
+
(finding) => finding.kind === "replan_required" && finding.status === "open",
|
|
520
|
+
);
|
|
521
|
+
if (open.length === 0)
|
|
522
|
+
throw new KernelInvariantError([
|
|
523
|
+
"authorize_rework requires an open replan boundary",
|
|
524
|
+
]);
|
|
525
|
+
for (const finding of open) finding.status = "resolved";
|
|
526
|
+
record.artifact_state = "active";
|
|
527
|
+
record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
|
|
528
|
+
appendHistory(record, action, from, open.map((finding) => finding.id).join(","), authorityAudit);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
505
531
|
case "complete": {
|
|
506
532
|
if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
|
|
507
533
|
throw new KernelInvariantError([
|
|
@@ -297,6 +297,7 @@ export type TaskAction =
|
|
|
297
297
|
| (TaskActionBase & { type: "request_rework"; findings: TaskFinding[] })
|
|
298
298
|
| (TaskActionBase & { type: "complete" })
|
|
299
299
|
| (TaskActionBase & { type: "stop"; reason: string })
|
|
300
|
+
| (TaskActionBase & { type: "authorize_rework" })
|
|
300
301
|
| (TaskActionBase & { type: "resolve_user_decision"; finding_id: string; resolution: string });
|
|
301
302
|
|
|
302
303
|
|
|
@@ -735,6 +735,7 @@ const ACTION_V2_TYPES = [
|
|
|
735
735
|
"request_rework",
|
|
736
736
|
"complete",
|
|
737
737
|
"stop",
|
|
738
|
+
"authorize_rework",
|
|
738
739
|
"resolve_user_decision",
|
|
739
740
|
] as const;
|
|
740
741
|
|
|
@@ -897,7 +898,8 @@ export function parseTaskAction(raw: unknown): TaskAction {
|
|
|
897
898
|
};
|
|
898
899
|
break;
|
|
899
900
|
}
|
|
900
|
-
case "complete":
|
|
901
|
+
case "complete":
|
|
902
|
+
case "authorize_rework": {
|
|
901
903
|
rejectUnknown(value, [...ACTION_BASE_FIELDS], "action", violations);
|
|
902
904
|
action = { ...base, type: base.type };
|
|
903
905
|
break;
|
|
@@ -1002,6 +1004,7 @@ export function assertTaskRecordUpdateV3(
|
|
|
1002
1004
|
if (
|
|
1003
1005
|
next.intent_ref.path !== previous.intent_ref.path &&
|
|
1004
1006
|
action.type !== "request_rework" &&
|
|
1007
|
+
action.type !== "authorize_rework" &&
|
|
1005
1008
|
action.type !== "stop"
|
|
1006
1009
|
)
|
|
1007
1010
|
violations.push("only artifact transitions may change intent_ref path");
|
|
@@ -1023,11 +1026,12 @@ export function assertTaskRecordUpdateV3(
|
|
|
1023
1026
|
? [action.finding_id]
|
|
1024
1027
|
: action.type === "resolve_user_decision"
|
|
1025
1028
|
? [action.finding_id]
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1029
|
+
: action.type === "authorize_rework" ||
|
|
1030
|
+
action.type === "approve_breaking_intent_revision"
|
|
1031
|
+
? previous.findings
|
|
1032
|
+
.filter((item) => item.kind === "replan_required" && item.status === "open")
|
|
1033
|
+
.map((item) => item.id)
|
|
1034
|
+
: [];
|
|
1031
1035
|
const reworkFindingIds =
|
|
1032
1036
|
action.type === "request_rework"
|
|
1033
1037
|
? new Set(action.findings.map((item) => item.id))
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "3.6.
|
|
2
|
+
export const PLUGIN_VERSION = "3.6.5" as const;
|