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
package/package.json
CHANGED
|
@@ -177,6 +177,7 @@ export type { AssuranceRole } from "./pi-canary-assurance";
|
|
|
177
177
|
export type AuthorizeOperation =
|
|
178
178
|
| "approve-breaking-intent-revision"
|
|
179
179
|
| "resolve-user-decision"
|
|
180
|
+
| "authorize-rework"
|
|
180
181
|
| "stop";
|
|
181
182
|
|
|
182
183
|
export interface SnapshotDescriptorInput {
|
|
@@ -435,6 +436,7 @@ export default function (
|
|
|
435
436
|
Type.Object({ op: Type.Literal("advance_assurance") }),
|
|
436
437
|
Type.Object({ op: Type.Literal("submit_review"), verdict: Type.Unknown() }),
|
|
437
438
|
Type.Object({ op: Type.Literal("request_authorization") }),
|
|
439
|
+
Type.Object({ op: Type.Literal("request_stop") }),
|
|
438
440
|
Type.Object({ op: Type.Literal("repair_authority_state") }),
|
|
439
441
|
Type.Object({ op: Type.Literal("freeze_artifacts") }),
|
|
440
442
|
Type.Object({
|
|
@@ -530,8 +532,8 @@ export default function (
|
|
|
530
532
|
return failCanaryTool(taskId, action.op, "authority_conflict", "authority_repair_failed", message, blocked.next_action);
|
|
531
533
|
}
|
|
532
534
|
}
|
|
533
|
-
if (action.op === "advance_assurance" || action.op === "request_authorization" || action.op === "submit_review" || action.op === "approve_breaking_intent_revision") {
|
|
534
|
-
if ((action.op === "request_authorization" || action.op === "approve_breaking_intent_revision") && ctx.mode !== "tui")
|
|
535
|
+
if (action.op === "advance_assurance" || action.op === "request_stop" || action.op === "request_authorization" || action.op === "submit_review" || action.op === "approve_breaking_intent_revision") {
|
|
536
|
+
if ((action.op === "request_stop" || action.op === "request_authorization" || action.op === "approve_breaking_intent_revision") && ctx.mode !== "tui")
|
|
535
537
|
return failCanaryTool(taskId, action.op, "blocked", "tui_required", "literal-user authorization is TUI-only", "invoke the TUI Tool");
|
|
536
538
|
const result = action.op === "advance_assurance"
|
|
537
539
|
? await progression.advance(taskId, ctx, signal, (update) => {
|
|
@@ -547,7 +549,9 @@ export default function (
|
|
|
547
549
|
ctx,
|
|
548
550
|
(action as { next_intent?: unknown }).next_intent,
|
|
549
551
|
)
|
|
550
|
-
:
|
|
552
|
+
: action.op === "request_stop"
|
|
553
|
+
? await authorizeExactOperation(taskId, "stop", { ...ctx, signal: signal && ctx.signal ? AbortSignal.any([signal, ctx.signal]) : signal ?? ctx.signal })
|
|
554
|
+
: await requestAuthorization(taskId, ctx);
|
|
551
555
|
const enriched = await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>);
|
|
552
556
|
presentTaskRailResult(ctx, taskId, enriched);
|
|
553
557
|
throwIfCanaryToolFailure(taskId, action.op, enriched);
|
|
@@ -696,7 +700,7 @@ export default function (
|
|
|
696
700
|
});
|
|
697
701
|
|
|
698
702
|
type AuthorizationOutcome =
|
|
699
|
-
| { state: "applied"; operation: AuthorizeOperation; lifecycle?: string }
|
|
703
|
+
| { state: "applied"; operation: AuthorizeOperation; lifecycle?: string; delivery_error?: string }
|
|
700
704
|
| { state: "cancelled"; operation: AuthorizeOperation; reason: string }
|
|
701
705
|
| { state: "blocked"; reason: string };
|
|
702
706
|
|
|
@@ -726,12 +730,15 @@ export default function (
|
|
|
726
730
|
let invocation: InvocationToken;
|
|
727
731
|
const authorizationGeneration = progression.sessionGenerationValue();
|
|
728
732
|
try {
|
|
733
|
+
if (operation === "stop" && progression.active(taskId)?.state === "running")
|
|
734
|
+
throw new Error("assurance operation is already running");
|
|
729
735
|
invocation = progression.openInvocation(taskId);
|
|
730
736
|
} catch (error) {
|
|
731
737
|
const reason = error instanceof Error ? error.message : String(error);
|
|
732
738
|
notifyOnce(ctx, `authorization-open:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
|
|
733
739
|
return { state: "blocked", reason };
|
|
734
740
|
}
|
|
741
|
+
try {
|
|
735
742
|
const projection = await projectAssuranceState(ctx.cwd, taskId);
|
|
736
743
|
if (projection.error || !projection.claim) {
|
|
737
744
|
const reason = projection.error ?? "no active backend claim";
|
|
@@ -841,6 +848,9 @@ export default function (
|
|
|
841
848
|
`Resolution: ${userDecisionOperation.resolution}`,
|
|
842
849
|
]
|
|
843
850
|
: []),
|
|
851
|
+
...(operation === "authorize-rework"
|
|
852
|
+
? [`Replan boundaries: ${projection.projection.replan_required_ids.join(", ")}`]
|
|
853
|
+
: []),
|
|
844
854
|
...(nextIntent
|
|
845
855
|
? [
|
|
846
856
|
`Next Intent: rev ${nextIntent.revision} (${nextIntentHash})`,
|
|
@@ -888,16 +898,27 @@ export default function (
|
|
|
888
898
|
if (nextIntent) {
|
|
889
899
|
restoreStagedIntent();
|
|
890
900
|
}
|
|
891
|
-
if (
|
|
901
|
+
if (
|
|
902
|
+
operation !== "stop" &&
|
|
903
|
+
operation !== "approve-breaking-intent-revision" &&
|
|
904
|
+
operation !== "authorize-rework"
|
|
905
|
+
)
|
|
892
906
|
await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
|
|
893
907
|
progression.closeInvocation(invocation);
|
|
908
|
+
if (operation === "stop" && !ctx.signal?.aborted && !(error instanceof Error && error.name === "AbortError")) {
|
|
909
|
+
return { state: "blocked", reason: "native stop confirmation failed; retry request_stop in this Host" };
|
|
910
|
+
}
|
|
894
911
|
return { state: "cancelled", operation, reason: "confirmation aborted" };
|
|
895
912
|
}
|
|
896
|
-
if (!confirmed) {
|
|
913
|
+
if (!confirmed || ctx.signal?.aborted) {
|
|
897
914
|
if (nextIntent) {
|
|
898
915
|
restoreStagedIntent();
|
|
899
916
|
}
|
|
900
|
-
if (
|
|
917
|
+
if (
|
|
918
|
+
operation !== "stop" &&
|
|
919
|
+
operation !== "approve-breaking-intent-revision" &&
|
|
920
|
+
operation !== "authorize-rework"
|
|
921
|
+
)
|
|
901
922
|
await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
|
|
902
923
|
progression.closeInvocation(invocation);
|
|
903
924
|
return { state: "cancelled", operation, reason: "cancelled" };
|
|
@@ -910,7 +931,6 @@ export default function (
|
|
|
910
931
|
progression.closeInvocation(invocation);
|
|
911
932
|
return { state: "blocked", reason: "session changed; confirmation discarded" };
|
|
912
933
|
}
|
|
913
|
-
try {
|
|
914
934
|
// Linearization point: only this fresh affirmative continuation
|
|
915
935
|
// may mint/apply; timeout/cancel already won open -> cancelled.
|
|
916
936
|
try {
|
|
@@ -932,14 +952,16 @@ export default function (
|
|
|
932
952
|
};
|
|
933
953
|
}
|
|
934
954
|
const exactOperation = operation === "stop"
|
|
935
|
-
? { op: "stop" as const, reason: "literal user
|
|
955
|
+
? { op: "stop" as const, reason: "literal user requested task stop" }
|
|
936
956
|
: operation === "approve-breaking-intent-revision"
|
|
937
957
|
? {
|
|
938
958
|
op: "approve_breaking_intent_revision" as const,
|
|
939
959
|
next_intent: nextIntent!,
|
|
940
960
|
next_intent_ref: nextIntentRef!,
|
|
941
961
|
}
|
|
942
|
-
:
|
|
962
|
+
: operation === "authorize-rework"
|
|
963
|
+
? { op: "authorize_rework" as const }
|
|
964
|
+
: userDecisionOperation!;
|
|
943
965
|
// The exact host-built operation is shared by capability digest and
|
|
944
966
|
// application payload; command arguments cannot inject authority fields.
|
|
945
967
|
try {
|
|
@@ -980,9 +1002,11 @@ export default function (
|
|
|
980
1002
|
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
981
1003
|
now,
|
|
982
1004
|
})) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
|
|
1005
|
+
if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
|
|
983
1006
|
if (
|
|
984
|
-
exactOperation.op === "stop"
|
|
985
|
-
|
|
1007
|
+
exactOperation.op === "stop" ||
|
|
1008
|
+
exactOperation.op === "authorize_rework" ||
|
|
1009
|
+
exactOperation.op === "approve_breaking_intent_revision"
|
|
986
1010
|
) stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
987
1011
|
return { state: "applied", operation, lifecycle: result.record.lifecycle };
|
|
988
1012
|
} catch (error) {
|
|
@@ -994,6 +1018,13 @@ export default function (
|
|
|
994
1018
|
}
|
|
995
1019
|
} catch (error) {
|
|
996
1020
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1021
|
+
if (operation === "stop") {
|
|
1022
|
+
const terminal = await projectAssuranceState(ctx.cwd, taskId).catch(() => null);
|
|
1023
|
+
if (terminal && !terminal.error && terminal.projection.lifecycle === "stopped") {
|
|
1024
|
+
progression.releaseStoppedReview(taskId);
|
|
1025
|
+
return { state: "applied", operation, lifecycle: "stopped", delivery_error: reason };
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
997
1028
|
notifyOnce(ctx, `authorization-apply:${taskId}:${operation}:${reason}`, `authorize failed: ${reason}`, "error");
|
|
998
1029
|
return { state: "blocked", reason };
|
|
999
1030
|
} finally {
|
|
@@ -1015,9 +1046,6 @@ export default function (
|
|
|
1015
1046
|
return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
|
|
1016
1047
|
const derived = deriveAuthorizationOperation({
|
|
1017
1048
|
readiness: projection.projection.authorization,
|
|
1018
|
-
hasOpenReplanRequired: read.record.findings.some(
|
|
1019
|
-
(finding) => finding.kind === "replan_required" && finding.status === "open",
|
|
1020
|
-
),
|
|
1021
1049
|
});
|
|
1022
1050
|
if ("blocked" in derived) return { state: "blocked", reason: derived.blocked };
|
|
1023
1051
|
return authorizeExactOperation(taskId, derived.operation, ctx);
|
|
@@ -1030,15 +1058,14 @@ export default function (
|
|
|
1030
1058
|
|
|
1031
1059
|
export type DerivedAuthorizationOperation =
|
|
1032
1060
|
| "resolve-user-decision"
|
|
1033
|
-
| "
|
|
1061
|
+
| "authorize-rework";
|
|
1034
1062
|
|
|
1035
1063
|
// Kernel projection is the sole source of authorization readiness.
|
|
1036
1064
|
export function deriveAuthorizationOperation(input: {
|
|
1037
1065
|
readiness: AssuranceAuthorizationReadiness;
|
|
1038
|
-
hasOpenReplanRequired?: boolean;
|
|
1039
1066
|
}): { operation: DerivedAuthorizationOperation } | { blocked: string } {
|
|
1040
|
-
if (input.hasOpenReplanRequired) return { operation: "stop" };
|
|
1041
1067
|
if (input.readiness.state === "resolve_user_decision") return { operation: "resolve-user-decision" };
|
|
1068
|
+
if (input.readiness.state === "authorize_rework") return { operation: "authorize-rework" };
|
|
1042
1069
|
if (input.readiness.blocked) return { blocked: input.readiness.blocked };
|
|
1043
1070
|
return { blocked: "no unique host-derived authorization operation" };
|
|
1044
1071
|
}
|
|
@@ -42,7 +42,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// plugins/immune-brain/runtime/plugin_version.ts
|
|
45
|
-
var PLUGIN_VERSION = "3.6.
|
|
45
|
+
var PLUGIN_VERSION = "3.6.5";
|
|
46
46
|
|
|
47
47
|
// plugins/immune-brain/runtime/claude/interaction.ts
|
|
48
48
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -1351,6 +1351,8 @@ class AssuranceCoordinator {
|
|
|
1351
1351
|
return this.sessionGeneration;
|
|
1352
1352
|
}
|
|
1353
1353
|
async advance(taskId, ctx, signal, onUpdate) {
|
|
1354
|
+
if (this.isInvocationOpen(taskId))
|
|
1355
|
+
return { state: "blocked", reason: "an authority invocation is already open" };
|
|
1354
1356
|
const active = this.active(taskId);
|
|
1355
1357
|
if (active?.state === "review_ready") {
|
|
1356
1358
|
const reservation = this.reviewReservations.get(taskId);
|
|
@@ -1728,6 +1730,17 @@ class AssuranceCoordinator {
|
|
|
1728
1730
|
}
|
|
1729
1731
|
return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
|
|
1730
1732
|
}
|
|
1733
|
+
isReviewVerdictValid(taskId, verdictInput) {
|
|
1734
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
1735
|
+
if (!reservation)
|
|
1736
|
+
return false;
|
|
1737
|
+
try {
|
|
1738
|
+
parseAssuranceVerdict(verdictInput, reservation.snapshot);
|
|
1739
|
+
return true;
|
|
1740
|
+
} catch {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1731
1744
|
abandonReview(taskId, reason) {
|
|
1732
1745
|
const reservation = this.reviewReservations.get(taskId);
|
|
1733
1746
|
if (!reservation)
|
|
@@ -1743,6 +1756,12 @@ class AssuranceCoordinator {
|
|
|
1743
1756
|
return { state: "blocked", code: "verdict_invalid", reason: "Review verdict correction is required before advancing" };
|
|
1744
1757
|
return { state: "review_ready", operation: "review", operation_id: reservation.operationId, snapshot_digest: snapshotDigest(reservation.snapshot), review_bundle_digest: reservation.snapshot.review_bundle_digest ?? "", agent_params: reservation.hostReservation.dispatch };
|
|
1745
1758
|
}
|
|
1759
|
+
releaseStoppedReview(taskId) {
|
|
1760
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
1761
|
+
if (reservation)
|
|
1762
|
+
this.releaseReviewReservation(taskId, reservation);
|
|
1763
|
+
this.rejectedReviewOperations.delete(taskId);
|
|
1764
|
+
}
|
|
1746
1765
|
releaseReviewReservation(taskId, reservation, rejectionReason) {
|
|
1747
1766
|
if (this.reviewReservations.get(taskId) !== reservation)
|
|
1748
1767
|
return;
|
|
@@ -3001,6 +3020,15 @@ function classifyIntentRevision(previous, next) {
|
|
|
3001
3020
|
return "breaking";
|
|
3002
3021
|
return next.revision > previous.revision ? "compatible" : "breaking";
|
|
3003
3022
|
}
|
|
3023
|
+
|
|
3024
|
+
class TaskIntentObservationError extends Error {
|
|
3025
|
+
code;
|
|
3026
|
+
constructor(code, message) {
|
|
3027
|
+
super(message);
|
|
3028
|
+
this.name = "TaskIntentObservationError";
|
|
3029
|
+
this.code = code;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3004
3032
|
var intentReaderTestHook = null;
|
|
3005
3033
|
function validateTaskId3(taskId) {
|
|
3006
3034
|
if (!TASK_ID_PATTERN.test(taskId))
|
|
@@ -3065,7 +3093,7 @@ function assertIdentitiesUnchanged(expected, canonicalRoot, relativePath) {
|
|
|
3065
3093
|
throw new Error(`path component changed while being read: ${parts.slice(0, index + 1).join("/")}`);
|
|
3066
3094
|
}
|
|
3067
3095
|
}
|
|
3068
|
-
function
|
|
3096
|
+
function readTaskIntentSource(root, taskId, requestedPath) {
|
|
3069
3097
|
validateTaskId3(taskId);
|
|
3070
3098
|
const canonicalRoot = resolveCanonicalRoot(root);
|
|
3071
3099
|
const activePath = `${INTENT_SIDECAR_RELATIVE_PREFIX}${taskId}.intent.json`;
|
|
@@ -3077,17 +3105,19 @@ function readTaskIntent(root, taskId, requestedPath) {
|
|
|
3077
3105
|
if (!target.startsWith(canonicalRoot + sep3))
|
|
3078
3106
|
throw new Error("intent sidecar escapes project root");
|
|
3079
3107
|
if (!sidecarPresent(canonicalRoot, sidecarPath))
|
|
3080
|
-
throw new
|
|
3108
|
+
throw new TaskIntentObservationError("missing", `TaskIntent sidecar is missing at ${sidecarPath}`);
|
|
3081
3109
|
const pathIdentities = collectPathIdentities(canonicalRoot, sidecarPath);
|
|
3082
3110
|
const fileIdentity = pathIdentities[pathIdentities.length - 1];
|
|
3083
3111
|
try {
|
|
3084
3112
|
execFileSync3("git", ["ls-files", "--error-unmatch", "--", sidecarPath], { cwd: canonicalRoot, stdio: ["ignore", "pipe", "pipe"] });
|
|
3085
|
-
} catch {
|
|
3086
|
-
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
if (typeof error === "object" && error !== null && "status" in error && error.status === 1)
|
|
3115
|
+
throw new TaskIntentObservationError("invalid", "TaskIntent sidecar is not Git-tracked");
|
|
3116
|
+
throw error;
|
|
3087
3117
|
}
|
|
3088
3118
|
const before = lstatSync4(target);
|
|
3089
3119
|
if (!before.isFile() || before.size > INTENT_MAX_BYTES)
|
|
3090
|
-
throw new
|
|
3120
|
+
throw new TaskIntentObservationError("invalid", "TaskIntent sidecar must be a regular file no larger than 64 KiB");
|
|
3091
3121
|
const fd = openSync2(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
3092
3122
|
let bytes;
|
|
3093
3123
|
try {
|
|
@@ -3099,7 +3129,7 @@ function readTaskIntent(root, taskId, requestedPath) {
|
|
|
3099
3129
|
closeSync2(fd);
|
|
3100
3130
|
}
|
|
3101
3131
|
if (bytes.byteLength > INTENT_MAX_BYTES)
|
|
3102
|
-
throw new
|
|
3132
|
+
throw new TaskIntentObservationError("invalid", "TaskIntent sidecar exceeds 64 KiB");
|
|
3103
3133
|
const after = lstatSync4(target);
|
|
3104
3134
|
assertSameIdentity(statIdentity(before), after, "intent sidecar");
|
|
3105
3135
|
assertIdentitiesUnchanged(pathIdentities, canonicalRoot, sidecarPath);
|
|
@@ -3113,23 +3143,11 @@ function readTaskIntent(root, taskId, requestedPath) {
|
|
|
3113
3143
|
try {
|
|
3114
3144
|
intent = parseTaskIntentV1(JSON.parse(bytes.toString("utf8")));
|
|
3115
3145
|
} catch (error) {
|
|
3116
|
-
throw new
|
|
3146
|
+
throw new TaskIntentObservationError("invalid", `TaskIntent sidecar is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
3117
3147
|
}
|
|
3118
3148
|
if (intent.task_id !== taskId)
|
|
3119
|
-
throw new
|
|
3149
|
+
throw new TaskIntentObservationError("invalid", "intent.task_id does not match the sidecar filename task id");
|
|
3120
3150
|
const contentHash = canonicalIntentHash(intent);
|
|
3121
|
-
const token = mintToken({
|
|
3122
|
-
canonical_root: canonicalRoot,
|
|
3123
|
-
sidecar_path: sidecarPath,
|
|
3124
|
-
path_dev: fileIdentity.dev,
|
|
3125
|
-
path_ino: fileIdentity.ino,
|
|
3126
|
-
fd_dev: before.dev,
|
|
3127
|
-
fd_ino: before.ino,
|
|
3128
|
-
fd_size: before.size,
|
|
3129
|
-
fd_mtime_ms: before.mtimeMs,
|
|
3130
|
-
source_bytes_sha256: sourceBytesSha256,
|
|
3131
|
-
intent_content_hash: contentHash
|
|
3132
|
-
});
|
|
3133
3151
|
return {
|
|
3134
3152
|
intent,
|
|
3135
3153
|
content_hash: contentHash,
|
|
@@ -3138,9 +3156,24 @@ function readTaskIntent(root, taskId, requestedPath) {
|
|
|
3138
3156
|
revision: intent.revision,
|
|
3139
3157
|
content_hash: contentHash
|
|
3140
3158
|
},
|
|
3141
|
-
|
|
3159
|
+
identity: {
|
|
3160
|
+
canonical_root: canonicalRoot,
|
|
3161
|
+
sidecar_path: sidecarPath,
|
|
3162
|
+
path_dev: fileIdentity.dev,
|
|
3163
|
+
path_ino: fileIdentity.ino,
|
|
3164
|
+
fd_dev: before.dev,
|
|
3165
|
+
fd_ino: before.ino,
|
|
3166
|
+
fd_size: before.size,
|
|
3167
|
+
fd_mtime_ms: before.mtimeMs,
|
|
3168
|
+
source_bytes_sha256: sourceBytesSha256,
|
|
3169
|
+
intent_content_hash: contentHash
|
|
3170
|
+
}
|
|
3142
3171
|
};
|
|
3143
3172
|
}
|
|
3173
|
+
function readTaskIntent(root, taskId, requestedPath) {
|
|
3174
|
+
const { identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
|
|
3175
|
+
return { ...observed, token: mintToken(identity) };
|
|
3176
|
+
}
|
|
3144
3177
|
|
|
3145
3178
|
// plugins/immune-brain/runtime/kernel/validation.ts
|
|
3146
3179
|
var GIT_OBJECT_ID3 = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
@@ -3645,6 +3678,7 @@ var ACTION_V2_TYPES = [
|
|
|
3645
3678
|
"request_rework",
|
|
3646
3679
|
"complete",
|
|
3647
3680
|
"stop",
|
|
3681
|
+
"authorize_rework",
|
|
3648
3682
|
"resolve_user_decision"
|
|
3649
3683
|
];
|
|
3650
3684
|
var ACTION_BASE_FIELDS = [
|
|
@@ -3740,7 +3774,8 @@ function parseTaskAction(raw) {
|
|
|
3740
3774
|
};
|
|
3741
3775
|
break;
|
|
3742
3776
|
}
|
|
3743
|
-
case "complete":
|
|
3777
|
+
case "complete":
|
|
3778
|
+
case "authorize_rework": {
|
|
3744
3779
|
rejectUnknown2(value, [...ACTION_BASE_FIELDS], "action", violations);
|
|
3745
3780
|
action = { ...base, type: base.type };
|
|
3746
3781
|
break;
|
|
@@ -3798,7 +3833,7 @@ function assertTaskRecordUpdateV3(previousRaw, nextRaw, action) {
|
|
|
3798
3833
|
violations.push("non-intent action cannot change the intent snapshot");
|
|
3799
3834
|
if (next.intent_ref.content_hash !== previous.intent_ref.content_hash)
|
|
3800
3835
|
violations.push("non-intent action cannot change intent_ref content hash");
|
|
3801
|
-
if (next.intent_ref.path !== previous.intent_ref.path && action.type !== "request_rework" && action.type !== "stop")
|
|
3836
|
+
if (next.intent_ref.path !== previous.intent_ref.path && action.type !== "request_rework" && action.type !== "authorize_rework" && action.type !== "stop")
|
|
3802
3837
|
violations.push("only artifact transitions may change intent_ref path");
|
|
3803
3838
|
}
|
|
3804
3839
|
if (next.attestations.length < previous.attestations.length)
|
|
@@ -3810,7 +3845,7 @@ function assertTaskRecordUpdateV3(previousRaw, nextRaw, action) {
|
|
|
3810
3845
|
if (!current || JSON.stringify(current) !== JSON.stringify(prior))
|
|
3811
3846
|
violations.push(`attestation ${prior.id} was rewritten`);
|
|
3812
3847
|
}
|
|
3813
|
-
const resolvingFindingIds = action.type === "resolve_finding" ? [action.finding_id] : action.type === "resolve_user_decision" ? [action.finding_id] : action.type === "approve_breaking_intent_revision" ? previous.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id) : [];
|
|
3848
|
+
const resolvingFindingIds = action.type === "resolve_finding" ? [action.finding_id] : action.type === "resolve_user_decision" ? [action.finding_id] : action.type === "authorize_rework" || action.type === "approve_breaking_intent_revision" ? previous.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id) : [];
|
|
3814
3849
|
const reworkFindingIds = action.type === "request_rework" ? new Set(action.findings.map((item) => item.id)) : new Set;
|
|
3815
3850
|
for (const prior of previous.findings) {
|
|
3816
3851
|
const current = next.findings.find((item) => item.id === prior.id);
|
|
@@ -5039,6 +5074,8 @@ function deriveAssuranceAuthorization(input) {
|
|
|
5039
5074
|
state: "none",
|
|
5040
5075
|
blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`
|
|
5041
5076
|
};
|
|
5077
|
+
if (input.next_obligation === "revise_intent")
|
|
5078
|
+
return { state: "authorize_rework", blocked: null };
|
|
5042
5079
|
return { state: "none", blocked: null };
|
|
5043
5080
|
}
|
|
5044
5081
|
function emptyProjection() {
|
|
@@ -5289,7 +5326,7 @@ function intentRefMatches(intent, ref) {
|
|
|
5289
5326
|
return ref.path === `docs/plans/${intent.task_id}.intent.json` && ref.content_hash === canonicalIntentHash(intent);
|
|
5290
5327
|
}
|
|
5291
5328
|
function hasPrivilegedKind(action) {
|
|
5292
|
-
return action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "stop" || action.type === "resolve_user_decision";
|
|
5329
|
+
return action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "authorize_rework" || action.type === "stop" || action.type === "resolve_user_decision";
|
|
5293
5330
|
}
|
|
5294
5331
|
function findingsDigestV2(findings) {
|
|
5295
5332
|
const normalized = findings.map((finding) => ({
|
|
@@ -5507,8 +5544,8 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
|
|
|
5507
5544
|
"request_rework requires review, qa, or user authority"
|
|
5508
5545
|
]);
|
|
5509
5546
|
const round = reviewRound(record);
|
|
5510
|
-
const
|
|
5511
|
-
const parkForReplan = authorityAudit.authority_kind === "review" &&
|
|
5547
|
+
const hasPriorBlockingReviewRework = record.findings.some((finding) => finding.source === "review" && finding.kind === "blocking" && finding.review_round !== null);
|
|
5548
|
+
const parkForReplan = authorityAudit.authority_kind === "review" && hasPriorBlockingReviewRework && action.findings.some((finding) => finding.kind === "blocking");
|
|
5512
5549
|
if (!parkForReplan) {
|
|
5513
5550
|
record.artifact_state = "active";
|
|
5514
5551
|
record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
|
|
@@ -5523,8 +5560,8 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
|
|
|
5523
5560
|
record.findings.push({
|
|
5524
5561
|
...finding,
|
|
5525
5562
|
status: "open",
|
|
5526
|
-
source: "review",
|
|
5527
|
-
review_round: round
|
|
5563
|
+
source: authorityAudit.authority_kind === "review" ? "review" : "execution",
|
|
5564
|
+
review_round: authorityAudit.authority_kind === "review" ? round : null
|
|
5528
5565
|
});
|
|
5529
5566
|
}
|
|
5530
5567
|
if (parkForReplan && !record.findings.some((item) => item.status === "open" && item.kind === "replan_required")) {
|
|
@@ -5547,6 +5584,27 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
|
|
|
5547
5584
|
appendHistory(record, action, from, `review_round_${round}`, authorityAudit);
|
|
5548
5585
|
break;
|
|
5549
5586
|
}
|
|
5587
|
+
case "authorize_rework": {
|
|
5588
|
+
if (record.lifecycle !== "active")
|
|
5589
|
+
throw new KernelInvariantError([
|
|
5590
|
+
`cannot authorize rework while lifecycle is ${record.lifecycle}`
|
|
5591
|
+
]);
|
|
5592
|
+
if (authorityAudit?.authority_kind !== "user")
|
|
5593
|
+
throw new KernelInvariantError([
|
|
5594
|
+
"authorize_rework requires literal-user authority"
|
|
5595
|
+
]);
|
|
5596
|
+
const open = record.findings.filter((finding) => finding.kind === "replan_required" && finding.status === "open");
|
|
5597
|
+
if (open.length === 0)
|
|
5598
|
+
throw new KernelInvariantError([
|
|
5599
|
+
"authorize_rework requires an open replan boundary"
|
|
5600
|
+
]);
|
|
5601
|
+
for (const finding of open)
|
|
5602
|
+
finding.status = "resolved";
|
|
5603
|
+
record.artifact_state = "active";
|
|
5604
|
+
record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
|
|
5605
|
+
appendHistory(record, action, from, open.map((finding) => finding.id).join(","), authorityAudit);
|
|
5606
|
+
break;
|
|
5607
|
+
}
|
|
5550
5608
|
case "complete": {
|
|
5551
5609
|
if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
|
|
5552
5610
|
throw new KernelInvariantError([
|
|
@@ -5677,7 +5735,7 @@ function applyTaskAction(input) {
|
|
|
5677
5735
|
"intent token does not match the committed record intent"
|
|
5678
5736
|
]);
|
|
5679
5737
|
}
|
|
5680
|
-
const privileged = action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "stop" || action.type === "resolve_user_decision";
|
|
5738
|
+
const privileged = action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "authorize_rework" || action.type === "stop" || action.type === "resolve_user_decision";
|
|
5681
5739
|
const expectedAuthority = privileged ? {
|
|
5682
5740
|
task_id,
|
|
5683
5741
|
action,
|
|
@@ -5803,6 +5861,8 @@ function capabilityActionFor(input) {
|
|
|
5803
5861
|
return { ...base, approval: input.approval };
|
|
5804
5862
|
case "request_rework":
|
|
5805
5863
|
return { ...base, findings: input.findings };
|
|
5864
|
+
case "authorize_rework":
|
|
5865
|
+
return { ...base, type: "authorize_rework" };
|
|
5806
5866
|
case "stop":
|
|
5807
5867
|
return { ...base, reason: input.reason };
|
|
5808
5868
|
case "approve_breaking_intent_revision":
|
|
@@ -5949,7 +6009,7 @@ function createCanaryApplication(registry) {
|
|
|
5949
6009
|
const hasBoundSpec = snapshot.intent_snapshot.scope_hint.some((path) => /^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path) && snapshot.intent_snapshot.scope_hint.includes(archivePath(path)));
|
|
5950
6010
|
if (operation.op === "complete" && hasBoundSpec && snapshot.record.artifact_state !== "frozen")
|
|
5951
6011
|
throw new KernelInvariantError(["complete requires frozen planning artifacts"]);
|
|
5952
|
-
const artifactTransition = snapshot.record.artifact_state === "frozen" && (operation.op === "request_rework" || operation.op === "approve_breaking_intent_revision") ? transitionFor(input.root, snapshot.record, "restore") : operation.op === "stop" && snapshot.record.artifact_state !== "frozen" ? transitionFor(input.root, snapshot.record, "freeze", true) : undefined;
|
|
6012
|
+
const artifactTransition = snapshot.record.artifact_state === "frozen" && (operation.op === "request_rework" || operation.op === "authorize_rework" || operation.op === "approve_breaking_intent_revision") ? transitionFor(input.root, snapshot.record, "restore") : operation.op === "stop" && snapshot.record.artifact_state !== "frozen" ? transitionFor(input.root, snapshot.record, "freeze", true) : undefined;
|
|
5953
6013
|
const event_id = `${operation.op}:${input.task_id}:${at}`;
|
|
5954
6014
|
const base = {
|
|
5955
6015
|
event_id,
|
|
@@ -6015,6 +6075,10 @@ function createCanaryApplication(registry) {
|
|
|
6015
6075
|
capability = operation.capability;
|
|
6016
6076
|
action = { ...base, type: "stop", reason: operation.reason };
|
|
6017
6077
|
break;
|
|
6078
|
+
case "authorize_rework":
|
|
6079
|
+
capability = operation.capability;
|
|
6080
|
+
action = { ...base, type: "authorize_rework" };
|
|
6081
|
+
break;
|
|
6018
6082
|
case "resolve_user_decision":
|
|
6019
6083
|
capability = operation.capability;
|
|
6020
6084
|
action = {
|
|
@@ -6477,7 +6541,16 @@ function enrollCanaryTask(root, input, registry) {
|
|
|
6477
6541
|
throw new Error("intent content hash mismatch");
|
|
6478
6542
|
if (checks.gitBaseHead !== gitBaseHead)
|
|
6479
6543
|
throw new Error("Git HEAD moved after the enrollment confirmation");
|
|
6544
|
+
if (input.batch) {
|
|
6545
|
+
const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding, Date.parse(input.now));
|
|
6546
|
+
if (input.batch.registry.consumedChildren(input.batch.capability).length === 0 && input.batch.expected_head !== batch.base_head)
|
|
6547
|
+
throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${batch.base_head}, not ${input.batch.expected_head}`);
|
|
6548
|
+
if (checks.gitBaseHead !== input.batch.expected_head)
|
|
6549
|
+
throw new Error(`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`);
|
|
6550
|
+
}
|
|
6480
6551
|
registry.consume(input.capability, input.capability_binding);
|
|
6552
|
+
if (input.batch)
|
|
6553
|
+
input.batch.registry.consumeChild(input.batch.capability, input.batch.binding, input.task_id, Date.parse(input.now));
|
|
6481
6554
|
if (!gitBaseHead)
|
|
6482
6555
|
throw new Error("enrollment requires a committed Git HEAD");
|
|
6483
6556
|
const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
|
|
@@ -6496,16 +6569,23 @@ function enrollCanaryTask(root, input, registry) {
|
|
|
6496
6569
|
created_at: input.now,
|
|
6497
6570
|
updated_at: input.now
|
|
6498
6571
|
};
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6572
|
+
let mutation;
|
|
6573
|
+
try {
|
|
6574
|
+
mutation = commitEnrollmentLocked(root, input.task_id, {
|
|
6575
|
+
contract: "assurance_kernel/workspace_transaction/v2",
|
|
6576
|
+
task_id: input.task_id,
|
|
6577
|
+
expected_record_hash: checks.current.revision,
|
|
6578
|
+
next_record_content: `${JSON.stringify(record, null, 2)}
|
|
6504
6579
|
`,
|
|
6505
|
-
|
|
6506
|
-
|
|
6580
|
+
expected_workspace_hash: checks.workspace.revision,
|
|
6581
|
+
next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}
|
|
6507
6582
|
`
|
|
6508
|
-
|
|
6583
|
+
}, claim);
|
|
6584
|
+
} catch (error) {
|
|
6585
|
+
if (input.batch)
|
|
6586
|
+
input.batch.registry.releaseChild(input.batch.capability, input.task_id);
|
|
6587
|
+
throw error;
|
|
6588
|
+
}
|
|
6509
6589
|
return {
|
|
6510
6590
|
record: mutation.record,
|
|
6511
6591
|
backend_claim: claim,
|
|
@@ -6645,14 +6725,18 @@ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput)
|
|
|
6645
6725
|
return coordinator.abandonReview(taskId, observed.reason);
|
|
6646
6726
|
return { state: "blocked", reason: observed.reason };
|
|
6647
6727
|
}
|
|
6728
|
+
const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
|
|
6729
|
+
if (!parentValid)
|
|
6730
|
+
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
6731
|
+
const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
|
|
6732
|
+
if (!receiptValid) {
|
|
6733
|
+
return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
|
|
6734
|
+
}
|
|
6648
6735
|
const parentJson = extractVerdictJson(verdictInput);
|
|
6649
6736
|
const receiptJson = extractVerdictJson(observed.receipt.result);
|
|
6650
|
-
if (
|
|
6737
|
+
if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
|
|
6651
6738
|
return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
|
|
6652
6739
|
}
|
|
6653
|
-
if (parentJson && !receiptJson) {
|
|
6654
|
-
return { state: "blocked", reason: "reviewer receipt is not a valid verdict" };
|
|
6655
|
-
}
|
|
6656
6740
|
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
6657
6741
|
}
|
|
6658
6742
|
function stopReason(value) {
|
|
@@ -6948,6 +7032,12 @@ class ClaudeRuntime {
|
|
|
6948
7032
|
async submitReview(taskId, verdictInput) {
|
|
6949
7033
|
return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
|
|
6950
7034
|
}
|
|
7035
|
+
async resolveFinding(taskId, findingId) {
|
|
7036
|
+
return this.executeOrdinary({ cwd: this.cwd }, {
|
|
7037
|
+
taskId,
|
|
7038
|
+
operation: { op: "resolve_finding", finding_id: findingId, actor_id: "executor" }
|
|
7039
|
+
});
|
|
7040
|
+
}
|
|
6951
7041
|
async authorize(taskId, operation, meta, extra = {}) {
|
|
6952
7042
|
if (operation === "repair_authority_state") {
|
|
6953
7043
|
const authority = reconcileKernelAuthority(this.cwd, taskId);
|
|
@@ -6972,6 +7062,8 @@ class ClaudeRuntime {
|
|
|
6972
7062
|
throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
|
|
6973
7063
|
op = "resolve_user_decision";
|
|
6974
7064
|
decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
|
|
7065
|
+
} else if (readiness.state === "authorize_rework") {
|
|
7066
|
+
op = "authorize_rework";
|
|
6975
7067
|
} else {
|
|
6976
7068
|
throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
|
|
6977
7069
|
}
|
|
@@ -7088,7 +7180,7 @@ class ClaudeRuntime {
|
|
|
7088
7180
|
diffProvider: diffSnapshotOf,
|
|
7089
7181
|
now
|
|
7090
7182
|
});
|
|
7091
|
-
if (op === "stop" || op === "approve_breaking_intent_revision")
|
|
7183
|
+
if (op === "stop" || op === "authorize_rework" || op === "approve_breaking_intent_revision")
|
|
7092
7184
|
stagePlanningArtifactTransition(this.cwd, result.record);
|
|
7093
7185
|
return result;
|
|
7094
7186
|
} catch (error) {
|
|
@@ -7224,7 +7316,8 @@ var TOOLS = [
|
|
|
7224
7316
|
{ name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
|
|
7225
7317
|
{ name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
|
|
7226
7318
|
{ name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
|
|
7227
|
-
{ name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false }
|
|
7319
|
+
{ name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
|
|
7320
|
+
{ name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false }
|
|
7228
7321
|
];
|
|
7229
7322
|
function listMcpTools() {
|
|
7230
7323
|
return TOOLS.map((tool) => ({
|
|
@@ -7236,9 +7329,10 @@ function listMcpTools() {
|
|
|
7236
7329
|
task_id: { type: "string" },
|
|
7237
7330
|
...tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {},
|
|
7238
7331
|
...tool.name === "stop" ? { reason: { type: "string" } } : {},
|
|
7239
|
-
...tool.name === "submit_review" ? { verdict: { type: "object" } } : {}
|
|
7332
|
+
...tool.name === "submit_review" ? { verdict: { type: "object" } } : {},
|
|
7333
|
+
...tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}
|
|
7240
7334
|
},
|
|
7241
|
-
required: tool.name === "submit_review" ? ["task_id", "verdict"] : ["task_id"]
|
|
7335
|
+
required: tool.name === "submit_review" ? ["task_id", "verdict"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : ["task_id"]
|
|
7242
7336
|
},
|
|
7243
7337
|
annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
|
|
7244
7338
|
}));
|
|
@@ -7305,6 +7399,11 @@ function createMcpRuntime(options = {}) {
|
|
|
7305
7399
|
throw new Error("verdict is required");
|
|
7306
7400
|
return runtime.submitReview(taskId, args.verdict);
|
|
7307
7401
|
}
|
|
7402
|
+
if (name === "resolve_finding") {
|
|
7403
|
+
if (typeof args.finding_id !== "string" || !args.finding_id)
|
|
7404
|
+
throw new Error("finding_id is required");
|
|
7405
|
+
return runtime.resolveFinding(taskId, args.finding_id);
|
|
7406
|
+
}
|
|
7308
7407
|
if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
|
|
7309
7408
|
return runtime.authorize(taskId, name, toolMeta, args);
|
|
7310
7409
|
}
|
|
@@ -83,8 +83,14 @@ not bypass them. Do not poll or create detached jobs.
|
|
|
83
83
|
- Invoke `approve_breaking_intent_revision` with the complete next intent
|
|
84
84
|
directly; the native Host gate is the single user decision. Do not overwrite
|
|
85
85
|
enrolled intent sidecars or ask for chat pre-confirmation.
|
|
86
|
-
- On `awaiting_user`, invoke `request_authorization` directly
|
|
87
|
-
|
|
86
|
+
- On `awaiting_user`, invoke `request_authorization` directly for a concrete
|
|
87
|
+
unresolved decision or rework authorization, not risk tier alone.
|
|
88
|
+
- When the user explicitly asks to stop a Pi task, invoke
|
|
89
|
+
`imm_kernel_canary({ task_id, action: { op: "request_stop" } })` directly.
|
|
90
|
+
Its single native confirmation authorizes existing Kernel stop settlement.
|
|
91
|
+
Cancellation is not task termination. A busy invocation must finish or be
|
|
92
|
+
cancelled through existing Host controls before requesting stop; never clear
|
|
93
|
+
claims manually or use this operation to force-kill QA.
|
|
88
94
|
- Invoke `repair_authority_state` directly for a proven stale claim. Kernel
|
|
89
95
|
revalidation removes only the redundant claim without user interaction.
|
|
90
96
|
- A Managed native authority failure stays fail-closed. Report its stable reason
|