immune-brain 2.8.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +9 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
- package/plugins/immune-brain/.mcp.json +8 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
- package/plugins/immune-brain/dist/imm-loop.md +6 -7
- package/plugins/immune-brain/dist/imm-planner.md +1 -1
- package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
- package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +55 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
- package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
- package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
- package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
- package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
- package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
- package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +28 -13
- package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
- package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
- package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
- package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
- package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
- package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
- package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
- package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
|
@@ -31,7 +31,16 @@ import {
|
|
|
31
31
|
type FrozenRunner,
|
|
32
32
|
type VerificationDescriptor,
|
|
33
33
|
} from "./pi-canary-verification";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
captureReviewBundle,
|
|
36
|
+
captureReviewManifest,
|
|
37
|
+
listReviewRefs,
|
|
38
|
+
reconcileReviewRefs,
|
|
39
|
+
writeNativeReviewEvidence,
|
|
40
|
+
type ReviewBundle,
|
|
41
|
+
type ReviewManifestV5,
|
|
42
|
+
type ReviewRevision,
|
|
43
|
+
} from "./pi-canary-review-bundle";
|
|
35
44
|
import type { InvocationToken } from "./pi-canary-invocations";
|
|
36
45
|
import { qaFindingId } from "./pi-canary-qa-findings";
|
|
37
46
|
import {
|
|
@@ -58,7 +67,7 @@ import {
|
|
|
58
67
|
type UserAttentionReason,
|
|
59
68
|
} from "./pi-canary-interaction";
|
|
60
69
|
import { isToolFailureState, throwToolFailure } from "./pi-canary-tool-failure";
|
|
61
|
-
import {
|
|
70
|
+
import { taskDiffIdentity, taskRevisionIdentity, captureGitTaskSnapshot } from "../runtime/workspace_scope";
|
|
62
71
|
import {
|
|
63
72
|
AssuranceProgression,
|
|
64
73
|
buildReviewPrompt,
|
|
@@ -108,6 +117,7 @@ import {
|
|
|
108
117
|
digestOfAction,
|
|
109
118
|
type AssuranceAuthorizationReadiness,
|
|
110
119
|
type AssuranceProjectionResult,
|
|
120
|
+
type TaskRecordRead,
|
|
111
121
|
type CanaryApplication,
|
|
112
122
|
type MutationAuthorityRegistry,
|
|
113
123
|
type CapabilityBindingV2,
|
|
@@ -184,6 +194,14 @@ export interface SnapshotDescriptorInput {
|
|
|
184
194
|
acceptance: Array<{ id: string; assertion: string; verification: string }>;
|
|
185
195
|
dirty_files?: string[];
|
|
186
196
|
review_bundle_digest?: string | null;
|
|
197
|
+
/** Present only when Review authority binds an immutable Git revision (v4). */
|
|
198
|
+
review_revision?: {
|
|
199
|
+
contract: "assurance_kernel/review_revision_identity/v1";
|
|
200
|
+
base_head: string;
|
|
201
|
+
review_commit: string;
|
|
202
|
+
review_tree: string;
|
|
203
|
+
manifest_digest: string;
|
|
204
|
+
};
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
export function buildSnapshot(input: SnapshotDescriptorInput): SnapshotDescriptor {
|
|
@@ -205,6 +223,7 @@ export function buildSnapshot(input: SnapshotDescriptorInput): SnapshotDescripto
|
|
|
205
223
|
acceptance: input.acceptance,
|
|
206
224
|
dirty_files: [...(input.dirty_files ?? [])].sort(),
|
|
207
225
|
review_bundle_digest: input.review_bundle_digest ?? null,
|
|
226
|
+
...(input.review_revision ? { review_revision: input.review_revision } : {}),
|
|
208
227
|
root: resolve(input.root),
|
|
209
228
|
};
|
|
210
229
|
}
|
|
@@ -253,6 +272,7 @@ export default function (
|
|
|
253
272
|
frozenRunner: () => frozenRunner(),
|
|
254
273
|
buildAssurance: (root, taskId, role, projection, runner) =>
|
|
255
274
|
(dependencies.buildAssurance ?? buildAssuranceSnapshot)(root, taskId, role, projection, runner),
|
|
275
|
+
ensureReviewRevision: (root, taskId, projection) => ensureTaskReviewRevision(root, taskId, projection),
|
|
256
276
|
runQa: (snapshot, descriptors, runner, options) =>
|
|
257
277
|
(dependencies.runQa ?? runDeterministicQa)(snapshot, descriptors, runner, options),
|
|
258
278
|
writeReviewEvidence: (input) =>
|
|
@@ -328,11 +348,13 @@ export default function (
|
|
|
328
348
|
|
|
329
349
|
pi.on("session_start", async (_event: unknown, ctx?: ExtensionContext) => {
|
|
330
350
|
progression.onSessionStart();
|
|
331
|
-
if (!ctx
|
|
351
|
+
if (!ctx) return;
|
|
332
352
|
railContext = ctx;
|
|
353
|
+
await reconcileRefsQuietly(ctx.cwd);
|
|
354
|
+
if (ctx.mode !== "tui") return;
|
|
333
355
|
await refreshTaskRail(ctx);
|
|
334
356
|
});
|
|
335
|
-
pi.on("tool_call", (event: { toolName?: string; input?: unknown
|
|
357
|
+
pi.on("tool_call", (event: { toolName?: string; input?: unknown }, ctx?: ExtensionContext) => {
|
|
336
358
|
if (ctx) railContext = ctx;
|
|
337
359
|
if (event.toolName === "imm_canary_enrollment" && ctx) {
|
|
338
360
|
const input = event.input as { task_id?: string } | undefined;
|
|
@@ -343,7 +365,6 @@ export default function (
|
|
|
343
365
|
next: "Review the native enrollment decision",
|
|
344
366
|
});
|
|
345
367
|
}
|
|
346
|
-
return progression.observeToolCall(event);
|
|
347
368
|
});
|
|
348
369
|
pi.on("tool_result", (event: unknown, ctx?: ExtensionContext) => {
|
|
349
370
|
if (ctx) railContext = ctx;
|
|
@@ -352,10 +373,6 @@ export default function (
|
|
|
352
373
|
const taskId = typeof result.details?.task_id === "string" ? result.details.task_id : undefined;
|
|
353
374
|
if (taskId) presentTaskRailResult(ctx, taskId, result.details);
|
|
354
375
|
}
|
|
355
|
-
progression.observeToolResult(event as Parameters<AssuranceProgression["observeToolResult"]>[0]);
|
|
356
|
-
});
|
|
357
|
-
pi.on("tool_execution_end", (event: unknown) => {
|
|
358
|
-
progression.observeToolEnd(event as Parameters<AssuranceProgression["observeToolEnd"]>[0]);
|
|
359
376
|
});
|
|
360
377
|
pi.on("session_shutdown", async (_event: unknown, ctx?: ExtensionContext) => {
|
|
361
378
|
resetInteractionPresentation(ctx ?? railContext);
|
|
@@ -368,11 +385,11 @@ export default function (
|
|
|
368
385
|
label: "Kernel canary assurance and executor operations",
|
|
369
386
|
description:
|
|
370
387
|
"Advance observable QA/Review orchestration, record executor facts, or request host confirmation for one enrolled Kernel canary task.",
|
|
371
|
-
promptSnippet: "Kernel canary: record facts, run foreground QA, then submit one
|
|
388
|
+
promptSnippet: "Kernel canary: record facts, run foreground QA, then submit one structured Review verdict without polling.",
|
|
372
389
|
promptGuidelines: [
|
|
373
390
|
"Only the exact enrolled canary task is routable; verify the active backend claim first via status.",
|
|
374
391
|
"After implementation and focused verification, freeze the artifacts, call advance_assurance, and consume its direct terminal result; do not poll or create a detached job.",
|
|
375
|
-
"When advance_assurance returns review_ready, invoke the
|
|
392
|
+
"When advance_assurance returns review_ready, invoke the foreground Agent from agent_params once, then pass its structured verdict to submit_review.",
|
|
376
393
|
"For a complete breaking revision, call approve_breaking_intent_revision with the complete next_intent directly; do not ask for chat pre-confirmation because the host opens the single native confirmation before applying it.",
|
|
377
394
|
"For a proven stale authority claim, call repair_authority_state directly; do not ask for chat pre-confirmation because the host opens the single native confirmation.",
|
|
378
395
|
"After awaiting_user, call request_authorization directly so the host opens the single native confirmation; do not ask for chat pre-confirmation or ask the user to copy or report a command.",
|
|
@@ -382,7 +399,7 @@ export default function (
|
|
|
382
399
|
action: Type.Union([
|
|
383
400
|
Type.Object({ op: Type.Literal("status") }),
|
|
384
401
|
Type.Object({ op: Type.Literal("advance_assurance") }),
|
|
385
|
-
Type.Object({ op: Type.Literal("submit_review") }),
|
|
402
|
+
Type.Object({ op: Type.Literal("submit_review"), verdict: Type.Unknown() }),
|
|
386
403
|
Type.Object({ op: Type.Literal("request_authorization") }),
|
|
387
404
|
Type.Object({ op: Type.Literal("repair_authority_state") }),
|
|
388
405
|
Type.Object({ op: Type.Literal("freeze_artifacts") }),
|
|
@@ -407,7 +424,8 @@ export default function (
|
|
|
407
424
|
Type.Object({ op: Type.Literal("complete") }),
|
|
408
425
|
]),
|
|
409
426
|
}),
|
|
410
|
-
|
|
427
|
+
prepareArguments: prepareActionArgs,
|
|
428
|
+
execute: async (toolCallId: string, params: { task_id: string; action: { op: string; verdict?: unknown } }, signal: AbortSignal | undefined, onUpdate: ((update: ReturnType<typeof toolResult>) => void) | undefined, ctx: ExtensionContext) => {
|
|
411
429
|
const { task_id: taskId, action } = params;
|
|
412
430
|
railContext = ctx;
|
|
413
431
|
presentTaskRailResult(ctx, taskId, {
|
|
@@ -524,7 +542,7 @@ export default function (
|
|
|
524
542
|
presentTaskRailResult(ctx, taskId, update.details as Record<string, unknown> | undefined);
|
|
525
543
|
})
|
|
526
544
|
: action.op === "submit_review"
|
|
527
|
-
? await progression.submitReview(taskId, ctx)
|
|
545
|
+
? await progression.submitReview(taskId, ctx, action.verdict)
|
|
528
546
|
: action.op === "approve_breaking_intent_revision"
|
|
529
547
|
? await authorizeExactOperation(
|
|
530
548
|
taskId,
|
|
@@ -591,6 +609,7 @@ export default function (
|
|
|
591
609
|
result: "Kernel executor fact recorded",
|
|
592
610
|
next_action: nextAction,
|
|
593
611
|
};
|
|
612
|
+
await reconcileRefsQuietly(ctx.cwd);
|
|
594
613
|
presentTaskRailResult(ctx, taskId, details);
|
|
595
614
|
return toolResult(
|
|
596
615
|
JSON.stringify(
|
|
@@ -643,6 +662,7 @@ export default function (
|
|
|
643
662
|
}),
|
|
644
663
|
]),
|
|
645
664
|
}),
|
|
665
|
+
prepareArguments: prepareActionArgs,
|
|
646
666
|
execute: async (
|
|
647
667
|
_toolCallId: string,
|
|
648
668
|
params: { action: LoopToolAction },
|
|
@@ -720,6 +740,69 @@ export default function (
|
|
|
720
740
|
progression.closeInvocation(invocation);
|
|
721
741
|
return { state: "blocked", reason };
|
|
722
742
|
}
|
|
743
|
+
const priorIntent = await readTaskIntent(ctx.cwd, taskId);
|
|
744
|
+
const sidecar = nextIntent ? join(ctx.cwd, priorIntent.intent_ref.path) : undefined;
|
|
745
|
+
const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
|
|
746
|
+
const priorIndexState = sidecar
|
|
747
|
+
? execFileSync("git", ["ls-files", "--stage", "-z", "--", priorIntent.intent_ref.path], {
|
|
748
|
+
cwd: ctx.cwd,
|
|
749
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
750
|
+
})
|
|
751
|
+
: undefined;
|
|
752
|
+
let stagedNextDiffHash: string | undefined;
|
|
753
|
+
const restoreStagedIntent = (): void => {
|
|
754
|
+
if (!sidecar || !priorBytes || !priorIndexState) return;
|
|
755
|
+
writeFileSync(sidecar, priorBytes);
|
|
756
|
+
execFileSync("git", ["update-index", "--force-remove", "--", priorIntent.intent_ref.path], {
|
|
757
|
+
cwd: ctx.cwd,
|
|
758
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
759
|
+
});
|
|
760
|
+
if (priorIndexState.length > 0) {
|
|
761
|
+
execFileSync("git", ["update-index", "-z", "--index-info"], {
|
|
762
|
+
cwd: ctx.cwd,
|
|
763
|
+
input: priorIndexState,
|
|
764
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const restoredBytes = readFileSync(sidecar);
|
|
768
|
+
if (!restoredBytes.equals(priorBytes)) {
|
|
769
|
+
throw new Error("failed to restore prior intent bytes");
|
|
770
|
+
}
|
|
771
|
+
const restoredIndex = execFileSync("git", ["ls-files", "--stage", "-z", "--", priorIntent.intent_ref.path], {
|
|
772
|
+
cwd: ctx.cwd,
|
|
773
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
774
|
+
});
|
|
775
|
+
if (!restoredIndex.equals(priorIndexState)) {
|
|
776
|
+
throw new Error("failed to restore prior intent git index entry");
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
if (nextIntent) {
|
|
780
|
+
try {
|
|
781
|
+
if (!sidecar || !priorBytes) throw new Error("breaking revision sidecar is missing");
|
|
782
|
+
if (
|
|
783
|
+
priorIntent.intent.revision !== projection.projection.intent_revision
|
|
784
|
+
|| priorIntent.content_hash !== projection.projection.intent_content_hash
|
|
785
|
+
) throw new Error("Intent changed while preparing the breaking revision");
|
|
786
|
+
writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
|
|
787
|
+
execFileSync("git", ["add", "--", priorIntent.intent_ref.path], {
|
|
788
|
+
cwd: ctx.cwd,
|
|
789
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
790
|
+
});
|
|
791
|
+
const stagedRecord = await readTaskRecord(ctx.cwd, taskId);
|
|
792
|
+
if (!stagedRecord.record || stagedRecord.revision !== projection.projection.record_revision)
|
|
793
|
+
throw new Error("TaskRecord changed while preparing the breaking revision");
|
|
794
|
+
stagedNextDiffHash = diffHashOf(ctx.cwd, stagedRecord.record);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
let restoreError: unknown;
|
|
797
|
+
try { restoreStagedIntent(); } catch (err) { restoreError = err; }
|
|
798
|
+
const primaryReason = error instanceof Error ? error.message : String(error);
|
|
799
|
+
const reason = restoreError
|
|
800
|
+
? `${primaryReason}; additionally, ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`
|
|
801
|
+
: primaryReason;
|
|
802
|
+
progression.closeInvocation(invocation);
|
|
803
|
+
return { state: "blocked", reason };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
723
806
|
let userDecisionOperation: ReturnType<typeof buildUserDecisionOperation> | undefined;
|
|
724
807
|
if (operation === "resolve-user-decision") {
|
|
725
808
|
try {
|
|
@@ -754,6 +837,7 @@ export default function (
|
|
|
754
837
|
`Next Goal: ${nextIntent.goal}`,
|
|
755
838
|
`Next Scope: ${nextIntent.scope_hint.join(", ")}`,
|
|
756
839
|
`Next Acceptance Items: ${nextIntent.acceptance.length}`,
|
|
840
|
+
`Next staged diff: ${stagedNextDiffHash}`,
|
|
757
841
|
]
|
|
758
842
|
: []),
|
|
759
843
|
`Claim: ${projection.claim.lifecycle_status}`,
|
|
@@ -791,19 +875,28 @@ export default function (
|
|
|
791
875
|
],
|
|
792
876
|
});
|
|
793
877
|
confirmed = selected === "authorize";
|
|
794
|
-
} catch {
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (nextIntent) {
|
|
880
|
+
restoreStagedIntent();
|
|
881
|
+
}
|
|
795
882
|
if (operation !== "stop" && operation !== "approve-breaking-intent-revision")
|
|
796
883
|
await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
|
|
797
884
|
progression.closeInvocation(invocation);
|
|
798
885
|
return { state: "cancelled", operation, reason: "confirmation aborted" };
|
|
799
886
|
}
|
|
800
887
|
if (!confirmed) {
|
|
888
|
+
if (nextIntent) {
|
|
889
|
+
restoreStagedIntent();
|
|
890
|
+
}
|
|
801
891
|
if (operation !== "stop" && operation !== "approve-breaking-intent-revision")
|
|
802
892
|
await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
|
|
803
893
|
progression.closeInvocation(invocation);
|
|
804
894
|
return { state: "cancelled", operation, reason: "cancelled" };
|
|
805
895
|
}
|
|
806
896
|
if (!progression.sessionActiveValue() || progression.sessionGenerationValue() !== authorizationGeneration || progression.invocationState(invocation) !== "open") {
|
|
897
|
+
if (nextIntent) {
|
|
898
|
+
restoreStagedIntent();
|
|
899
|
+
}
|
|
807
900
|
notifyOnce(ctx, `authorization-session:${taskId}:${operation}`, `authorize ${operation}: session changed; confirmation discarded`, "warning");
|
|
808
901
|
progression.closeInvocation(invocation);
|
|
809
902
|
return { state: "blocked", reason: "session changed; confirmation discarded" };
|
|
@@ -814,13 +907,15 @@ export default function (
|
|
|
814
907
|
try {
|
|
815
908
|
progression.commitInvocation(invocation);
|
|
816
909
|
} catch (error) {
|
|
910
|
+
if (nextIntent) {
|
|
911
|
+
restoreStagedIntent();
|
|
912
|
+
}
|
|
817
913
|
const reason = error instanceof Error ? error.message : String(error);
|
|
818
914
|
notifyOnce(ctx, `authorization-commit:${taskId}:${operation}:${reason}`, `authorize ${operation} aborted: ${reason}`, "error");
|
|
819
915
|
return { state: "blocked", reason };
|
|
820
916
|
}
|
|
821
917
|
const { registry, app } = await authorityPair();
|
|
822
918
|
const now = new Date().toISOString();
|
|
823
|
-
const priorIntent = await readTaskIntent(ctx.cwd, taskId);
|
|
824
919
|
if (nextIntent) {
|
|
825
920
|
nextIntentRef = {
|
|
826
921
|
path: `docs/plans/${nextIntent.task_id}.intent.json`,
|
|
@@ -855,20 +950,18 @@ export default function (
|
|
|
855
950
|
: userDecisionOperation ?? userOperationFor(operation, approval);
|
|
856
951
|
// The exact host-built operation is shared by capability digest and
|
|
857
952
|
// application payload; command arguments cannot inject authority fields.
|
|
858
|
-
const sidecar = nextIntent ? join(ctx.cwd, priorIntent.intent_ref.path) : undefined;
|
|
859
|
-
const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
|
|
860
953
|
try {
|
|
861
|
-
if (
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const operationDiffHash = nextIntent
|
|
870
|
-
? diffHashOf(ctx.cwd, priorIntent.intent)
|
|
954
|
+
if (nextIntent) await dependencies.authorizationAfterSidecarStage?.();
|
|
955
|
+
// Re-read the owner record after confirmation and compare the staged
|
|
956
|
+
// candidate digest immediately before capability issuance.
|
|
957
|
+
const liveRecord = nextIntent ? await readTaskRecord(ctx.cwd, taskId) : null;
|
|
958
|
+
if (nextIntent && (!liveRecord?.record || liveRecord.revision !== projection.projection.record_revision || !stagedNextDiffHash))
|
|
959
|
+
throw new Error("TaskRecord changed before the breaking revision digest");
|
|
960
|
+
const operationDiffHash = liveRecord?.record
|
|
961
|
+
? diffHashOf(ctx.cwd, liveRecord.record)
|
|
871
962
|
: projection.projection.diff_hash;
|
|
963
|
+
if (nextIntent && operationDiffHash !== stagedNextDiffHash)
|
|
964
|
+
throw new Error("staged next-state diff changed after native confirmation");
|
|
872
965
|
const capability = await mintCapability(registry, {
|
|
873
966
|
authority_kind: "user",
|
|
874
967
|
task_id: taskId,
|
|
@@ -893,7 +986,7 @@ export default function (
|
|
|
893
986
|
task_id: taskId,
|
|
894
987
|
operation: { ...exactOperation, capability, actor_id: "literal-user" } as never,
|
|
895
988
|
prior_intent_token: priorIntent.token,
|
|
896
|
-
diffProvider: (root: string,
|
|
989
|
+
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
897
990
|
now,
|
|
898
991
|
})) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
|
|
899
992
|
if (
|
|
@@ -901,19 +994,13 @@ export default function (
|
|
|
901
994
|
|| exactOperation.op === "approve_breaking_intent_revision"
|
|
902
995
|
) stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
903
996
|
return { state: "applied", operation, lifecycle: result.record.lifecycle };
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
writeFileSync(sidecar, priorBytes);
|
|
909
|
-
execFileSync("git", ["add", "--", priorIntent.intent_ref.path], {
|
|
910
|
-
cwd: ctx.cwd,
|
|
911
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
throw error;
|
|
997
|
+
} catch (error) {
|
|
998
|
+
if (sidecar && priorBytes && priorIndexState) {
|
|
999
|
+
const current = await readTaskRecord(ctx.cwd, taskId);
|
|
1000
|
+
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) restoreStagedIntent();
|
|
916
1001
|
}
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
917
1004
|
} catch (error) {
|
|
918
1005
|
const reason = error instanceof Error ? error.message : String(error);
|
|
919
1006
|
notifyOnce(ctx, `authorization-apply:${taskId}:${operation}:${reason}`, `authorize failed: ${reason}`, "error");
|
|
@@ -1018,7 +1105,7 @@ export async function recordCancelledUserDecision(
|
|
|
1018
1105
|
actor_id: "literal-user",
|
|
1019
1106
|
} as never,
|
|
1020
1107
|
prior_intent_token: (await readTaskIntent(ctx.cwd, taskId)).token,
|
|
1021
|
-
diffProvider: (root: string,
|
|
1108
|
+
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
1022
1109
|
now: new Date().toISOString(),
|
|
1023
1110
|
});
|
|
1024
1111
|
return { recorded: true, finding_id: findingId };
|
|
@@ -1048,8 +1135,25 @@ export function userOperationFor(operation: AuthorizeOperation, approval?: unkno
|
|
|
1048
1135
|
return { op: "record_user_approval" as const, approval };
|
|
1049
1136
|
}
|
|
1050
1137
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1138
|
+
/**
|
|
1139
|
+
* One record-aware freshness identity. v4 derives the scoped revision digest
|
|
1140
|
+
* from the immutable Enrollment base so committed and staged task work share a
|
|
1141
|
+
* single diff_hash with Review; v3 keeps the legacy HEAD -> index digest.
|
|
1142
|
+
*/
|
|
1143
|
+
function diffSnapshotOf(root: string, record: NonNullable<TaskRecordRead["record"]>): {
|
|
1144
|
+
diff_hash: string;
|
|
1145
|
+
changed_paths: string[];
|
|
1146
|
+
} {
|
|
1147
|
+
if (record.contract === "assurance_kernel/task_record/v4") {
|
|
1148
|
+
if (!record.git_base_head)
|
|
1149
|
+
throw new Error("TaskRecord v4 is missing git_base_head");
|
|
1150
|
+
return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head);
|
|
1151
|
+
}
|
|
1152
|
+
return taskDiffIdentity(root, record.intent_snapshot.scope_hint);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function diffHashOf(root: string, record: NonNullable<TaskRecordRead["record"]>): string {
|
|
1156
|
+
return diffSnapshotOf(root, record).diff_hash;
|
|
1053
1157
|
}
|
|
1054
1158
|
|
|
1055
1159
|
// Translation-only adapter for the internal Kernel assurance projection. All
|
|
@@ -1058,7 +1162,81 @@ function diffHashOf(root: string, intent: { scope_hint?: unknown }): string {
|
|
|
1058
1162
|
// active-v2 migrator is gone: a v2 TaskRecord in the state layout is a
|
|
1059
1163
|
// fail-closed projection error, never an automatic migration trigger.
|
|
1060
1164
|
async function projectAssuranceState(root: string, taskId: string): Promise<AssuranceProjectionResult> {
|
|
1061
|
-
return projectAssurance(root, taskId,
|
|
1165
|
+
return projectAssurance(root, taskId, diffSnapshotOf);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Publish and prove the task-scoped synthetic revision for a v4 record. v3
|
|
1170
|
+
* records keep the legacy full-source bundle and return null here.
|
|
1171
|
+
*/
|
|
1172
|
+
async function ensureTaskReviewRevision(
|
|
1173
|
+
root: string,
|
|
1174
|
+
taskId: string,
|
|
1175
|
+
projection: AssuranceProjectionResult,
|
|
1176
|
+
): Promise<ReviewRevision | null> {
|
|
1177
|
+
const current = await readTaskRecord(root, taskId);
|
|
1178
|
+
const record = current.record;
|
|
1179
|
+
if (!record) throw new Error(`task ${taskId} has no TaskRecord before Review preparation`);
|
|
1180
|
+
if (current.revision !== projection.projection.record_revision)
|
|
1181
|
+
throw new Error("TaskRecord changed before Review preparation");
|
|
1182
|
+
if (record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
1183
|
+
if (!record.git_base_head)
|
|
1184
|
+
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
1185
|
+
const manifest = captureReviewManifest(root, {
|
|
1186
|
+
taskId,
|
|
1187
|
+
baseHead: record.git_base_head,
|
|
1188
|
+
scopeHint: record.intent_snapshot.scope_hint,
|
|
1189
|
+
expectedDiffHash: projection.projection.diff_hash,
|
|
1190
|
+
intentRevision: projection.projection.intent_revision,
|
|
1191
|
+
intentContentHash: projection.projection.intent_content_hash,
|
|
1192
|
+
recordRevision: projection.projection.record_revision,
|
|
1193
|
+
workspaceRevision: projection.projection.workspace_revision,
|
|
1194
|
+
lifecycle: projection.projection.lifecycle,
|
|
1195
|
+
artifactState: projection.projection.artifact_state,
|
|
1196
|
+
risk: record.intent_snapshot.risk,
|
|
1197
|
+
outcomes: reviewPreflightOutcomes(record.intent_snapshot.acceptance),
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
contract: "assurance_kernel/review_revision/v1",
|
|
1201
|
+
base_head: manifest.base_head,
|
|
1202
|
+
review_tree: manifest.review_tree,
|
|
1203
|
+
review_commit: manifest.review_commit,
|
|
1204
|
+
review_ref: manifest.review_ref,
|
|
1205
|
+
diff_hash: manifest.diff_hash,
|
|
1206
|
+
manifest_digest: manifest.manifest_digest,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Review refs are reconstructible evidence transport, never workflow authority.
|
|
1212
|
+
* A ref survives only while its task owns a nonterminal TaskRecord.
|
|
1213
|
+
*/
|
|
1214
|
+
async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: string[]; failed: string[] }> {
|
|
1215
|
+
let listed: ReturnType<typeof listReviewRefs>;
|
|
1216
|
+
try {
|
|
1217
|
+
listed = listReviewRefs(root);
|
|
1218
|
+
} catch {
|
|
1219
|
+
return { removed: [], failed: [] };
|
|
1220
|
+
}
|
|
1221
|
+
const live = new Set<string>();
|
|
1222
|
+
try {
|
|
1223
|
+
const claim = await readBackendClaim(root);
|
|
1224
|
+
for (const entry of listed) {
|
|
1225
|
+
if (live.has(entry.taskId)) continue;
|
|
1226
|
+
try {
|
|
1227
|
+
const record = await readTaskRecord(root, entry.taskId);
|
|
1228
|
+
if (record.record && record.record.lifecycle === "active" && claim?.task_id === entry.taskId)
|
|
1229
|
+
live.add(entry.taskId);
|
|
1230
|
+
} catch {
|
|
1231
|
+
// An unreadable owner is never proof of life, but deleting evidence on
|
|
1232
|
+
// a transient read failure is worse: leave it for the next pass.
|
|
1233
|
+
live.add(entry.taskId);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
} catch {
|
|
1237
|
+
for (const entry of listed) live.add(entry.taskId);
|
|
1238
|
+
}
|
|
1239
|
+
return reconcileReviewRefs(root, live);
|
|
1062
1240
|
}
|
|
1063
1241
|
|
|
1064
1242
|
export interface QaVerificationProgressInput {
|
|
@@ -1231,7 +1409,7 @@ async function applyAssuranceVerdict(
|
|
|
1231
1409
|
actor_id: actorId,
|
|
1232
1410
|
},
|
|
1233
1411
|
prior_intent_token: priorIntentToken,
|
|
1234
|
-
diffProvider: (root: string,
|
|
1412
|
+
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
1235
1413
|
now,
|
|
1236
1414
|
}))) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] }; findings?: Array<{ kind: string; status: string }> } };
|
|
1237
1415
|
stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
@@ -1256,6 +1434,11 @@ async function applyAssuranceVerdict(
|
|
|
1256
1434
|
diff_hash: snapshot.diff_hash,
|
|
1257
1435
|
actor_id: actorId,
|
|
1258
1436
|
summary: verdict.approval!.summary,
|
|
1437
|
+
// Trusted revision identity comes from the host-verified snapshot, never
|
|
1438
|
+
// from the reviewer payload.
|
|
1439
|
+
...(snapshot.role === "review" && snapshot.review_revision
|
|
1440
|
+
? { review_revision: snapshot.review_revision }
|
|
1441
|
+
: {}),
|
|
1259
1442
|
};
|
|
1260
1443
|
const capability = await mintCapability(registry, {
|
|
1261
1444
|
authority_kind: snapshot.role,
|
|
@@ -1275,7 +1458,7 @@ async function applyAssuranceVerdict(
|
|
|
1275
1458
|
task_id: snapshot.task_id,
|
|
1276
1459
|
operation: { op: "record_approval", capability, approval, actor_id: actorId },
|
|
1277
1460
|
prior_intent_token: priorIntentToken,
|
|
1278
|
-
diffProvider: (root: string,
|
|
1461
|
+
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
1279
1462
|
now,
|
|
1280
1463
|
}));
|
|
1281
1464
|
}
|
|
@@ -1286,7 +1469,12 @@ async function buildAssuranceSnapshot(
|
|
|
1286
1469
|
role: AssuranceRole,
|
|
1287
1470
|
projection: AssuranceProjectionResult,
|
|
1288
1471
|
runner: FrozenRunner,
|
|
1289
|
-
): Promise<{
|
|
1472
|
+
): Promise<{
|
|
1473
|
+
snapshot: SnapshotDescriptor;
|
|
1474
|
+
descriptors: Map<string, VerificationDescriptor>;
|
|
1475
|
+
reviewBundle: ReviewBundle | null;
|
|
1476
|
+
reviewManifest: ReviewManifestV5 | null;
|
|
1477
|
+
}> {
|
|
1290
1478
|
const record = await readTaskRecord(root, taskId);
|
|
1291
1479
|
if (
|
|
1292
1480
|
!record.record ||
|
|
@@ -1307,23 +1495,47 @@ async function buildAssuranceSnapshot(
|
|
|
1307
1495
|
assertRunnerCompatible(descriptor, runner);
|
|
1308
1496
|
descriptors.set(item.id, descriptor);
|
|
1309
1497
|
}
|
|
1310
|
-
const
|
|
1498
|
+
const reviewRevision = record.record.contract === "assurance_kernel/task_record/v4"
|
|
1499
|
+
? {
|
|
1500
|
+
contract: "assurance_kernel/review_revision_identity/v1" as const,
|
|
1501
|
+
base_head: record.record.git_base_head,
|
|
1502
|
+
review_commit: "",
|
|
1503
|
+
review_tree: "",
|
|
1504
|
+
manifest_digest: "",
|
|
1505
|
+
}
|
|
1506
|
+
: null;
|
|
1507
|
+
const reviewBundle = role === "review" && !reviewRevision
|
|
1311
1508
|
? captureReviewBundle(
|
|
1312
1509
|
root,
|
|
1313
1510
|
intent.scope_hint,
|
|
1314
1511
|
projection.projection.diff_hash,
|
|
1315
|
-
|
|
1316
|
-
record.record.attestations
|
|
1317
|
-
.filter((item) => item.kind === "qa")
|
|
1318
|
-
.flatMap((item) => item.acceptance_results)
|
|
1319
|
-
.map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
|
|
1320
|
-
),
|
|
1512
|
+
qaOutcomes(record.record),
|
|
1321
1513
|
)
|
|
1322
1514
|
: null;
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1515
|
+
const reviewManifest = role === "review" && reviewRevision
|
|
1516
|
+
? captureReviewManifest(root, {
|
|
1517
|
+
taskId,
|
|
1518
|
+
baseHead: reviewRevision.base_head,
|
|
1519
|
+
scopeHint: intent.scope_hint,
|
|
1520
|
+
expectedDiffHash: projection.projection.diff_hash,
|
|
1521
|
+
intentRevision: projection.projection.intent_revision,
|
|
1522
|
+
intentContentHash: projection.projection.intent_content_hash,
|
|
1523
|
+
recordRevision: projection.projection.record_revision,
|
|
1524
|
+
workspaceRevision: projection.projection.workspace_revision,
|
|
1525
|
+
lifecycle: projection.projection.lifecycle,
|
|
1526
|
+
artifactState: projection.projection.artifact_state,
|
|
1527
|
+
risk: intent.risk,
|
|
1528
|
+
outcomes: qaOutcomes(record.record),
|
|
1529
|
+
})
|
|
1530
|
+
: null;
|
|
1531
|
+
const taskSnapshot = !reviewBundle && !reviewManifest
|
|
1532
|
+
? captureGitTaskSnapshot(root, intent.scope_hint)
|
|
1533
|
+
: null;
|
|
1534
|
+
const dirtyFiles = reviewManifest
|
|
1535
|
+
? Object.keys(reviewManifest.changed_paths)
|
|
1536
|
+
: reviewBundle
|
|
1537
|
+
? Object.keys(reviewBundle.dirty_files)
|
|
1538
|
+
: Object.keys(taskSnapshot!.staged_files);
|
|
1327
1539
|
return {
|
|
1328
1540
|
snapshot: buildSnapshot({
|
|
1329
1541
|
root,
|
|
@@ -1342,13 +1554,43 @@ async function buildAssuranceSnapshot(
|
|
|
1342
1554
|
stale_attestation_ids: projection.projection.stale_attestation_ids,
|
|
1343
1555
|
acceptance,
|
|
1344
1556
|
dirty_files: dirtyFiles,
|
|
1345
|
-
review_bundle_digest: reviewBundle?.bundle_digest ?? null,
|
|
1557
|
+
review_bundle_digest: reviewManifest?.manifest_digest ?? reviewBundle?.bundle_digest ?? null,
|
|
1558
|
+
review_revision: reviewManifest
|
|
1559
|
+
? {
|
|
1560
|
+
contract: "assurance_kernel/review_revision_identity/v1",
|
|
1561
|
+
base_head: reviewManifest.base_head,
|
|
1562
|
+
review_commit: reviewManifest.review_commit,
|
|
1563
|
+
review_tree: reviewManifest.review_tree,
|
|
1564
|
+
manifest_digest: reviewManifest.manifest_digest,
|
|
1565
|
+
}
|
|
1566
|
+
: undefined,
|
|
1346
1567
|
}),
|
|
1347
1568
|
descriptors,
|
|
1348
1569
|
reviewBundle,
|
|
1570
|
+
reviewManifest,
|
|
1349
1571
|
};
|
|
1350
1572
|
}
|
|
1351
1573
|
|
|
1574
|
+
function reviewPreflightOutcomes(
|
|
1575
|
+
acceptance: Array<{ id: string }>,
|
|
1576
|
+
): Record<string, { status: "passed"; summary: string }> {
|
|
1577
|
+
const summary = `host-attested QA: all ${acceptance.length} fixed verification descriptor(s) passed`;
|
|
1578
|
+
return Object.fromEntries(
|
|
1579
|
+
acceptance.map((item) => [item.id, { status: "passed" as const, summary }]),
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function qaOutcomes(
|
|
1584
|
+
record: NonNullable<TaskRecordRead["record"]>,
|
|
1585
|
+
): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
|
|
1586
|
+
return Object.fromEntries(
|
|
1587
|
+
record.attestations
|
|
1588
|
+
.filter((item) => item.kind === "qa")
|
|
1589
|
+
.flatMap((item) => item.acceptance_results)
|
|
1590
|
+
.map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1352
1594
|
let frozenRunnerValue: FrozenRunner | undefined;
|
|
1353
1595
|
async function frozenRunner(): Promise<FrozenRunner> {
|
|
1354
1596
|
frozenRunnerValue ??= resolveBunRunner();
|
|
@@ -1407,7 +1649,9 @@ async function mintCapability(
|
|
|
1407
1649
|
intent_content_hash: input.intent_content_hash,
|
|
1408
1650
|
diff_hash: input.diff_hash,
|
|
1409
1651
|
actor_id: input.actor_id,
|
|
1410
|
-
confirmation_ref: `pi-confirm-${createHash("sha256").update(
|
|
1652
|
+
confirmation_ref: `pi-confirm-${createHash("sha256").update(
|
|
1653
|
+
`${input.task_id}\0${now}\0${input.intent_revision}\0${input.intent_content_hash}\0${input.diff_hash}`,
|
|
1654
|
+
).digest("hex").slice(0, 16)}`,
|
|
1411
1655
|
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
1412
1656
|
findings_digest:
|
|
1413
1657
|
input.action_kind === "request_rework"
|
|
@@ -1454,7 +1698,7 @@ async function executeOrdinaryOperation(
|
|
|
1454
1698
|
task_id: input.taskId,
|
|
1455
1699
|
operation: operation as never,
|
|
1456
1700
|
prior_intent_token: priorIntent.token,
|
|
1457
|
-
diffProvider: (root: string,
|
|
1701
|
+
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
1458
1702
|
now: new Date().toISOString(),
|
|
1459
1703
|
});
|
|
1460
1704
|
if (operation.op === "freeze_artifacts" || operation.op === "stop")
|
|
@@ -1471,6 +1715,18 @@ async function executeOrdinaryOperation(
|
|
|
1471
1715
|
|
|
1472
1716
|
type AssuranceTaskState = AssuranceProjectionResult["projection"] | { error: string };
|
|
1473
1717
|
|
|
1718
|
+
/**
|
|
1719
|
+
* Ref cleanup is transport hygiene, never workflow authority: a failure is
|
|
1720
|
+
* swallowed here and retried by the next startup or terminal reconciliation.
|
|
1721
|
+
*/
|
|
1722
|
+
async function reconcileRefsQuietly(root: string): Promise<void> {
|
|
1723
|
+
try {
|
|
1724
|
+
await reconcileReviewRevisionRefs(root);
|
|
1725
|
+
} catch {
|
|
1726
|
+
/* non-authoritative */
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1474
1730
|
async function enrichAssuranceResult(
|
|
1475
1731
|
ctx: ExtensionContext,
|
|
1476
1732
|
taskId: string,
|
|
@@ -1481,6 +1737,7 @@ async function enrichAssuranceResult(
|
|
|
1481
1737
|
? { error: projection.error }
|
|
1482
1738
|
: projection.projection;
|
|
1483
1739
|
let tracker: Awaited<ReturnType<typeof markGithubTaskTerminal>> | undefined;
|
|
1740
|
+
await reconcileRefsQuietly(ctx.cwd);
|
|
1484
1741
|
if (!projection.error) {
|
|
1485
1742
|
try {
|
|
1486
1743
|
const terminalInput = deriveGithubTerminalProjectionInput(
|
|
@@ -1509,9 +1766,11 @@ async function enrichAssuranceResult(
|
|
|
1509
1766
|
|
|
1510
1767
|
function nextActionForAssuranceResult(result: Record<string, unknown>, taskState: AssuranceTaskState): string {
|
|
1511
1768
|
if ("error" in taskState) return "inspect authority state";
|
|
1769
|
+
if (result.state === "review_preparation_failed") return "retry advance_assurance";
|
|
1770
|
+
if (result.code === "verdict_invalid") return "fix the verdict payload and resubmit submit_review; the Review reservation remains active; do not re-dispatch the reviewer";
|
|
1512
1771
|
if (taskState.lifecycle === "done" || taskState.lifecycle === "stopped") return "none";
|
|
1513
1772
|
switch (result.state) {
|
|
1514
|
-
case "review_ready": return "invoke the
|
|
1773
|
+
case "review_ready": return "invoke the foreground reviewer and submit its verdict";
|
|
1515
1774
|
case "awaiting_user": return "request_authorization";
|
|
1516
1775
|
case "applied": return taskState.completion_ready ? "complete task" : taskState.next_obligation;
|
|
1517
1776
|
case "completed":
|
|
@@ -1529,6 +1788,37 @@ function toolResult(text: string, details?: Record<string, unknown>) {
|
|
|
1529
1788
|
return { content: [{ type: "text" as const, text }], details };
|
|
1530
1789
|
}
|
|
1531
1790
|
|
|
1791
|
+
/**
|
|
1792
|
+
* Pre-validation compatibility recovery for providers that encode the
|
|
1793
|
+
* object-valued Tool `action` as a JSON string (observed with
|
|
1794
|
+
* `hyper/qwen3.8-flash`: `{ "action": "{\"op\":\"status\"}" }`).
|
|
1795
|
+
*
|
|
1796
|
+
* Only the top-level `action` field is parsed, and only when it parses to a
|
|
1797
|
+
* non-null, non-array object. Native object input, invalid JSON, arrays,
|
|
1798
|
+
* `null`, primitives, and every other shape are returned unchanged so the
|
|
1799
|
+
* strict TypeBox schemas remain the authoritative boundary. Nested string
|
|
1800
|
+
* fields (`context`, `next_intent`, `finding`) are deliberately never
|
|
1801
|
+
* touched; this helper does not validate schemas, mutate inputs, or throw.
|
|
1802
|
+
*
|
|
1803
|
+
* Temporary compatibility layer owned by GitHub Issue #14
|
|
1804
|
+
* (github.com/dereknex/immune-brain/issues/14): remove after two consecutive
|
|
1805
|
+
* Pi or Hyper adapter upgrade cycles pass a live nested-object Tool-call
|
|
1806
|
+
* probe at least 30 days apart.
|
|
1807
|
+
*/
|
|
1808
|
+
function prepareActionArgs(args: unknown): unknown {
|
|
1809
|
+
if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
|
|
1810
|
+
const input = args as Record<string, unknown>;
|
|
1811
|
+
if (typeof input.action !== "string") return input;
|
|
1812
|
+
try {
|
|
1813
|
+
const parsed: unknown = JSON.parse(input.action);
|
|
1814
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
1815
|
+
return { ...input, action: parsed };
|
|
1816
|
+
} catch {
|
|
1817
|
+
// Unchanged input keeps the normal host schema error authoritative.
|
|
1818
|
+
}
|
|
1819
|
+
return input;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1532
1822
|
function stagePlanningArtifactTransition(root: string, record: {
|
|
1533
1823
|
intent_ref: { path: string };
|
|
1534
1824
|
intent_snapshot: { scope_hint: string[] };
|
|
@@ -1585,7 +1875,7 @@ function throwIfCanaryToolFailure(
|
|
|
1585
1875
|
taskId,
|
|
1586
1876
|
operation,
|
|
1587
1877
|
result.state,
|
|
1588
|
-
`assurance_${result.state}`,
|
|
1878
|
+
typeof result.code === "string" ? result.code : `assurance_${result.state}`,
|
|
1589
1879
|
typeof result.reason === "string"
|
|
1590
1880
|
? result.reason
|
|
1591
1881
|
: typeof result.result === "string"
|