immune-brain 3.6.3 → 3.6.4
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 +6 -3
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +72 -29
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +6 -4
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -44
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +67 -34
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +17 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +23 -13
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +103 -37
- package/plugins/immune-brain/runtime/claude/review_host.ts +0 -10
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +13 -8
- package/plugins/immune-brain/runtime/kernel/application.ts +5 -0
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +4 -0
- package/plugins/immune-brain/runtime/kernel/index.ts +2 -0
- package/plugins/immune-brain/runtime/kernel/observation.ts +2 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +17 -2
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +0 -4
- package/plugins/immune-brain/runtime/kernel/types.ts +9 -0
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +2 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "immune-brain",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.4",
|
|
4
4
|
"description": "Immune-Brain agent skill system",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -30,13 +30,16 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"changeset": "changeset",
|
|
32
32
|
"changeset:version": "changeset version && bun scripts/build-claude-plugin.ts && bun scripts/plugin_versioning.ts validate",
|
|
33
|
-
"verify:release": "bun scripts/plugin_versioning.ts validate && bun scripts/build-claude-plugin.ts --check && bun scripts/sync-dist-docs.ts --check && bun test && npm pack --dry-run --ignore-scripts",
|
|
33
|
+
"verify:release": "bun scripts/plugin_versioning.ts validate && bun scripts/build-claude-plugin.ts --check && bun scripts/sync-dist-docs.ts --check && bun run typecheck && bun test && npm pack --dry-run --ignore-scripts",
|
|
34
34
|
"changeset:publish": "bun run verify:release && changeset publish",
|
|
35
|
-
"release": "bun run changeset:publish"
|
|
35
|
+
"release": "bun run changeset:publish",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@changesets/changelog-github": "^0.5.2",
|
|
39
40
|
"@changesets/cli": "^2.29.8",
|
|
41
|
+
"@types/bun": "^1.4.1",
|
|
42
|
+
"typescript": "5.7",
|
|
40
43
|
"zod": "^4.1.8"
|
|
41
44
|
},
|
|
42
45
|
"peerDependencies": {
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
clearTerminalTaskRailOnInput,
|
|
57
57
|
loopResultDetails,
|
|
58
58
|
notifyOnce,
|
|
59
|
+
type UiContext,
|
|
59
60
|
presentTaskOverviewOverlay,
|
|
60
61
|
presentTaskRail,
|
|
61
62
|
presentTaskRailResult,
|
|
@@ -68,7 +69,7 @@ import {
|
|
|
68
69
|
type UserAttentionEventV1,
|
|
69
70
|
type UserAttentionReason,
|
|
70
71
|
} from "./pi-canary-interaction";
|
|
71
|
-
import { isToolFailureState, throwToolFailure } from "./pi-canary-tool-failure";
|
|
72
|
+
import { isToolFailureState, throwToolFailure, type ToolFailureV1 } from "./pi-canary-tool-failure";
|
|
72
73
|
import { taskDiffIdentity, taskRevisionIdentity, captureGitTaskSnapshot } from "../runtime/workspace_scope";
|
|
73
74
|
import {
|
|
74
75
|
AssuranceProgression,
|
|
@@ -87,6 +88,7 @@ import {
|
|
|
87
88
|
type AssuranceProgressionPorts,
|
|
88
89
|
type AssuranceSubmitReviewResult,
|
|
89
90
|
type AssuranceVerdict,
|
|
91
|
+
type HostContext,
|
|
90
92
|
type QaVerificationProgress,
|
|
91
93
|
type SnapshotDescriptor,
|
|
92
94
|
} from "./pi-canary-assurance-progression";
|
|
@@ -262,11 +264,18 @@ type LoopToolAction =
|
|
|
262
264
|
context: Record<string, unknown>;
|
|
263
265
|
};
|
|
264
266
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
/**
|
|
268
|
+
* The exact ports object the Pi Assurance progression runs on.
|
|
269
|
+
*
|
|
270
|
+
* This lived as an inline literal inside the anonymous default export, so no
|
|
271
|
+
* test could ever obtain what production wires; every host adapter defect that
|
|
272
|
+
* reached a published plugin lived in this object. Exporting the factory lets
|
|
273
|
+
* the dual-host conformance suite drive the real thing.
|
|
274
|
+
*/
|
|
275
|
+
export function createPiAssuranceProgressionPorts(
|
|
267
276
|
dependencies: CanaryWorkExtensionDependencies = {},
|
|
268
|
-
) {
|
|
269
|
-
|
|
277
|
+
): AssuranceProgressionPorts {
|
|
278
|
+
return {
|
|
270
279
|
projectTask: (root, taskId) => projectAssuranceState(root, taskId),
|
|
271
280
|
readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
|
|
272
281
|
readTaskIntent: (root, taskId) => readTaskIntent(root, taskId),
|
|
@@ -294,7 +303,14 @@ export default function (
|
|
|
294
303
|
qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
|
|
295
304
|
qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
|
|
296
305
|
qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
|
|
297
|
-
} satisfies AssuranceProgressionPorts
|
|
306
|
+
} satisfies AssuranceProgressionPorts;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export default function (
|
|
310
|
+
pi: ExtensionAPI,
|
|
311
|
+
dependencies: CanaryWorkExtensionDependencies = {},
|
|
312
|
+
) {
|
|
313
|
+
const progression = new AssuranceProgression(createPiAssuranceProgressionPorts(dependencies));
|
|
298
314
|
|
|
299
315
|
let railContext: ExtensionContext | undefined;
|
|
300
316
|
const refreshTaskRail = async (ctx: ExtensionContext) => {
|
|
@@ -1196,7 +1212,7 @@ async function projectAssuranceState(root: string, taskId: string): Promise<Assu
|
|
|
1196
1212
|
* Publish and prove the task-scoped synthetic revision for a v4 record. v3
|
|
1197
1213
|
* records keep the legacy full-source bundle and return null here.
|
|
1198
1214
|
*/
|
|
1199
|
-
async function ensureTaskReviewRevision(
|
|
1215
|
+
export async function ensureTaskReviewRevision(
|
|
1200
1216
|
root: string,
|
|
1201
1217
|
taskId: string,
|
|
1202
1218
|
projection: AssuranceProjectionResult,
|
|
@@ -1221,7 +1237,11 @@ async function ensureTaskReviewRevision(
|
|
|
1221
1237
|
lifecycle: projection.projection.lifecycle,
|
|
1222
1238
|
artifactState: projection.projection.artifact_state,
|
|
1223
1239
|
risk: record.intent_snapshot.risk,
|
|
1224
|
-
outcomes
|
|
1240
|
+
// The same outcomes the Review snapshot is built from. A preflight stand-in
|
|
1241
|
+
// only matched the settled QA attestation because deterministic QA happens to
|
|
1242
|
+
// write that exact summary, so the submit-time digest comparison held by
|
|
1243
|
+
// coincidence rather than by construction.
|
|
1244
|
+
outcomes: qaOutcomes(record),
|
|
1225
1245
|
});
|
|
1226
1246
|
return {
|
|
1227
1247
|
contract: "assurance_kernel/review_revision/v1",
|
|
@@ -1266,8 +1286,22 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
|
|
|
1266
1286
|
return reconcileReviewRefs(root, live);
|
|
1267
1287
|
}
|
|
1268
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* The coordinator port supplies a `HostContext`, which carries no UI. Pi hands
|
|
1291
|
+
* its full `ExtensionContext` through at runtime, so the notice still reaches
|
|
1292
|
+
* the user; a host that does not is left un-notified rather than throwing from
|
|
1293
|
+
* inside an authority commit, where a notification has no authority anyway.
|
|
1294
|
+
*/
|
|
1295
|
+
function notifyHost(ctx: HostContext, key: string, message: string, level: "warning" | "error"): void {
|
|
1296
|
+
const ui = (ctx as Partial<UiContext>).ui;
|
|
1297
|
+
if (ui) notifyOnce({ ui }, key, message, level);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1269
1300
|
async function applyAssuranceVerdict(
|
|
1270
|
-
|
|
1301
|
+
// The coordinator port hands these a `HostContext`, not the Pi
|
|
1302
|
+
// `ExtensionContext`. Both functions only ever read `cwd`; declaring the
|
|
1303
|
+
// wider host type made the port assignment unsound.
|
|
1304
|
+
ctx: HostContext,
|
|
1271
1305
|
snapshot: SnapshotDescriptor,
|
|
1272
1306
|
verdict: AssuranceVerdict,
|
|
1273
1307
|
invocation: InvocationToken,
|
|
@@ -1352,7 +1386,7 @@ async function applyAssuranceVerdict(
|
|
|
1352
1386
|
const parked = (result.record as { findings?: Array<{ kind: string; status: string }> }).findings?.some(
|
|
1353
1387
|
(finding) => finding.kind === "replan_required" && finding.status === "open",
|
|
1354
1388
|
);
|
|
1355
|
-
if (parked)
|
|
1389
|
+
if (parked) notifyHost(
|
|
1356
1390
|
ctx,
|
|
1357
1391
|
`rework-parked:${snapshot.task_id}`,
|
|
1358
1392
|
`rework applied: review parked for replan with ${findings.length} finding(s)`,
|
|
@@ -1431,10 +1465,17 @@ async function buildAssuranceSnapshot(
|
|
|
1431
1465
|
assertRunnerCompatible(descriptor, runner);
|
|
1432
1466
|
descriptors.set(item.id, descriptor);
|
|
1433
1467
|
}
|
|
1434
|
-
|
|
1468
|
+
// `git_base_head` is optional on the read shape because v3 records carry
|
|
1469
|
+
// none, so the contract test alone does not prove it is present.
|
|
1470
|
+
const baseHead = record.record.contract === "assurance_kernel/task_record/v4"
|
|
1471
|
+
? record.record.git_base_head
|
|
1472
|
+
: undefined;
|
|
1473
|
+
if (record.record.contract === "assurance_kernel/task_record/v4" && !baseHead)
|
|
1474
|
+
throw new Error("TaskRecord v4 is missing its Enrollment git_base_head");
|
|
1475
|
+
const reviewRevision = baseHead
|
|
1435
1476
|
? {
|
|
1436
1477
|
contract: "assurance_kernel/review_revision_identity/v1" as const,
|
|
1437
|
-
base_head:
|
|
1478
|
+
base_head: baseHead,
|
|
1438
1479
|
review_commit: "",
|
|
1439
1480
|
review_tree: "",
|
|
1440
1481
|
manifest_digest: "",
|
|
@@ -1507,15 +1548,6 @@ async function buildAssuranceSnapshot(
|
|
|
1507
1548
|
};
|
|
1508
1549
|
}
|
|
1509
1550
|
|
|
1510
|
-
function reviewPreflightOutcomes(
|
|
1511
|
-
acceptance: Array<{ id: string }>,
|
|
1512
|
-
): Record<string, { status: "passed"; summary: string }> {
|
|
1513
|
-
const summary = `host-attested QA: all ${acceptance.length} fixed verification descriptor(s) passed`;
|
|
1514
|
-
return Object.fromEntries(
|
|
1515
|
-
acceptance.map((item) => [item.id, { status: "passed" as const, summary }]),
|
|
1516
|
-
);
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
1551
|
function qaOutcomes(
|
|
1520
1552
|
record: NonNullable<TaskRecordRead["record"]>,
|
|
1521
1553
|
): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
|
|
@@ -1617,7 +1649,7 @@ function authorityPair(): Promise<{ registry: MutationAuthorityRegistry; app: Ca
|
|
|
1617
1649
|
}
|
|
1618
1650
|
|
|
1619
1651
|
async function executeOrdinaryOperation(
|
|
1620
|
-
ctx:
|
|
1652
|
+
ctx: HostContext,
|
|
1621
1653
|
input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } },
|
|
1622
1654
|
): Promise<unknown> {
|
|
1623
1655
|
const { app } = await authorityPair();
|
|
@@ -1638,7 +1670,10 @@ async function executeOrdinaryOperation(
|
|
|
1638
1670
|
now: new Date().toISOString(),
|
|
1639
1671
|
});
|
|
1640
1672
|
if (operation.op === "freeze_artifacts" || operation.op === "stop")
|
|
1641
|
-
stagePlanningArtifactTransition(
|
|
1673
|
+
stagePlanningArtifactTransition(
|
|
1674
|
+
ctx.cwd,
|
|
1675
|
+
(result as { record: Parameters<typeof stagePlanningArtifactTransition>[1] }).record,
|
|
1676
|
+
);
|
|
1642
1677
|
return result;
|
|
1643
1678
|
} catch (error) {
|
|
1644
1679
|
if (priorBytes) {
|
|
@@ -1741,18 +1776,24 @@ function toolResult(text: string, details?: Record<string, unknown>) {
|
|
|
1741
1776
|
* Pi or Hyper adapter upgrade cycles pass a live nested-object Tool-call
|
|
1742
1777
|
* probe at least 30 days apart.
|
|
1743
1778
|
*/
|
|
1744
|
-
|
|
1745
|
-
|
|
1779
|
+
/**
|
|
1780
|
+
* Pre-schema normalizer: some hosts deliver `action` as a JSON string. The Pi
|
|
1781
|
+
* runtime validates the returned value against the Tool schema immediately
|
|
1782
|
+
* after this shim, so the parameter type is the schema's, not a claim this
|
|
1783
|
+
* function makes about unvalidated input.
|
|
1784
|
+
*/
|
|
1785
|
+
function prepareActionArgs<Params>(args: unknown): Params {
|
|
1786
|
+
if (args === null || typeof args !== "object" || Array.isArray(args)) return args as Params;
|
|
1746
1787
|
const input = args as Record<string, unknown>;
|
|
1747
|
-
if (typeof input.action !== "string") return input;
|
|
1788
|
+
if (typeof input.action !== "string") return input as Params;
|
|
1748
1789
|
try {
|
|
1749
1790
|
const parsed: unknown = JSON.parse(input.action);
|
|
1750
1791
|
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
1751
|
-
return { ...input, action: parsed };
|
|
1792
|
+
return { ...input, action: parsed } as Params;
|
|
1752
1793
|
} catch {
|
|
1753
1794
|
// Unchanged input keeps the normal host schema error authoritative.
|
|
1754
1795
|
}
|
|
1755
|
-
return input;
|
|
1796
|
+
return input as Params;
|
|
1756
1797
|
}
|
|
1757
1798
|
|
|
1758
1799
|
function stagePlanningArtifactTransition(root: string, record: {
|
|
@@ -1785,7 +1826,9 @@ function stagePlanningArtifactTransition(root: string, record: {
|
|
|
1785
1826
|
function failCanaryTool(
|
|
1786
1827
|
taskId: string,
|
|
1787
1828
|
operation: string,
|
|
1788
|
-
|
|
1829
|
+
// `review_preparation_failed` is a declared ToolFailureV1 state and a
|
|
1830
|
+
// documented Loop recovery path; omitting it here made it unreportable.
|
|
1831
|
+
state: ToolFailureV1["state"],
|
|
1789
1832
|
code: string,
|
|
1790
1833
|
message: string,
|
|
1791
1834
|
nextAction: string,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DynamicBorder, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { DynamicBorder, type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Container, SelectList, Text, type Component, type SelectItem } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
export const USER_ATTENTION_EVENT = "immune-brain:user-attention.v1" as const;
|
|
@@ -75,7 +75,7 @@ export interface AuthorityDialogOptions<T extends string> {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
type EventPublisher = Pick<ExtensionAPI, "events">;
|
|
78
|
-
type UiContext = Pick<ExtensionContext, "ui">;
|
|
78
|
+
export type UiContext = Pick<ExtensionContext, "ui">;
|
|
79
79
|
|
|
80
80
|
const terminalRailUis = new WeakSet<object>();
|
|
81
81
|
const deliveredNotifications = new WeakMap<object, Set<string>>();
|
|
@@ -373,7 +373,9 @@ function emitAttention(pi: EventPublisher, event: UserAttentionEventV1): void {
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
function formatTaskRailState(state: TaskRailState, theme?: Theme): string {
|
|
376
|
-
|
|
376
|
+
// Typed as the theme's own colour union: a plain `string` here silently
|
|
377
|
+
// accepted a name the theme cannot resolve.
|
|
378
|
+
const symbolAndColor: Record<TaskRailState, { symbol: string; color: ThemeColor }> = {
|
|
377
379
|
Planning: { symbol: "●", color: "muted" },
|
|
378
380
|
"Approval required": { symbol: "▲", color: "accent" },
|
|
379
381
|
Working: { symbol: "●", color: "accent" },
|
|
@@ -383,7 +385,7 @@ function formatTaskRailState(state: TaskRailState, theme?: Theme): string {
|
|
|
383
385
|
Completed: { symbol: "✓", color: "success" },
|
|
384
386
|
Stopped: { symbol: "■", color: "muted" },
|
|
385
387
|
};
|
|
386
|
-
const cfg = symbolAndColor[state] ?? { symbol: "●", color: "dim" };
|
|
388
|
+
const cfg: { symbol: string; color: ThemeColor } = symbolAndColor[state] ?? { symbol: "●", color: "dim" };
|
|
387
389
|
if (!theme) return `${cfg.symbol} ${state}`;
|
|
388
390
|
return `${theme.fg(cfg.color, cfg.symbol)} ${theme.fg(cfg.color, state)}`;
|
|
389
391
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Extension-local runtime adapter: type-isolated, executable stub.
|
|
2
2
|
// Extensions import this file directly (relative path, resolvable by the Pi
|
|
3
3
|
// extension loader); it forwards to the real Kernel modules via dynamic
|
|
4
|
-
// import.
|
|
5
|
-
// type
|
|
4
|
+
// import. Shared contracts are re-exported as types from the real Kernel
|
|
5
|
+
// modules: `export type` is erased at compile time, so the extension still
|
|
6
|
+
// carries no static runtime import, but a Kernel contract change now breaks
|
|
7
|
+
// the extension's build instead of silently drifting from it.
|
|
6
8
|
|
|
7
9
|
// --- Types (structural contracts, no runtime import) ---
|
|
8
10
|
export interface EnrollmentCapabilityBinding {
|
|
@@ -178,40 +180,15 @@ export interface TaskRecordRead {
|
|
|
178
180
|
|
|
179
181
|
// --- Assurance projection (host-neutral Kernel facts, not exported from the
|
|
180
182
|
// public Kernel index) ---
|
|
181
|
-
export
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
diff_hash: string;
|
|
191
|
-
lifecycle: "active" | "done" | "stopped" | "";
|
|
192
|
-
artifact_state: "active" | "frozen" | "";
|
|
193
|
-
risk: "routine" | "material" | "critical" | "";
|
|
194
|
-
next_obligation: "resolve_findings" | "resolve_user_decision" | "revise_intent" | "submit_assurance" | "run_qa" | "run_review" | "complete" | "none";
|
|
195
|
-
fresh_acceptance_ids: string[];
|
|
196
|
-
missing_acceptance_ids: string[];
|
|
197
|
-
stale_attestation_ids: string[];
|
|
198
|
-
fresh_approval_kinds: string[];
|
|
199
|
-
missing_approval_kinds: string[];
|
|
200
|
-
blocking_finding_ids: string[];
|
|
201
|
-
unresolved_user_decision_ids: string[];
|
|
202
|
-
replan_required_ids: string[];
|
|
203
|
-
independence_violations: string[];
|
|
204
|
-
open_user_decision_count: number;
|
|
205
|
-
completion_ready: boolean;
|
|
206
|
-
authorization: AssuranceAuthorizationReadiness;
|
|
207
|
-
}
|
|
208
|
-
export interface AssuranceProjectionResult {
|
|
209
|
-
contract: "assurance_kernel/assurance_projection/v1";
|
|
210
|
-
task_id: string;
|
|
211
|
-
error: string | null;
|
|
212
|
-
claim: { task_id: string; lifecycle_status: string } | null;
|
|
213
|
-
projection: AssuranceProjection;
|
|
214
|
-
}
|
|
183
|
+
export type {
|
|
184
|
+
AssuranceAuthorizationReadiness,
|
|
185
|
+
AssuranceProjection,
|
|
186
|
+
AssuranceProjectionResult,
|
|
187
|
+
} from "../runtime/kernel/assurance_projection";
|
|
188
|
+
import type {
|
|
189
|
+
AssuranceAuthorizationReadiness,
|
|
190
|
+
AssuranceProjectionResult,
|
|
191
|
+
} from "../runtime/kernel/assurance_projection";
|
|
215
192
|
|
|
216
193
|
// --- Runtime forwarding (dynamic import keeps the graph out of tsc) ---
|
|
217
194
|
function kernelPath(module: string): string {
|
|
@@ -349,12 +326,8 @@ export interface TaskIntentV1 {
|
|
|
349
326
|
acceptance: Array<{ id: string; assertion: string; verification: string }>;
|
|
350
327
|
scope_hint: string[];
|
|
351
328
|
}
|
|
352
|
-
export
|
|
353
|
-
|
|
354
|
-
content_hash: string;
|
|
355
|
-
intent: TaskIntentV1;
|
|
356
|
-
intent_ref: { path: string; revision: number; content_hash: string };
|
|
357
|
-
}
|
|
329
|
+
export type { ReadTaskIntentResult as TaskIntentRead } from "../runtime/kernel/intent";
|
|
330
|
+
import type { ReadTaskIntentResult as TaskIntentRead } from "../runtime/kernel/intent";
|
|
358
331
|
export interface WorkspaceRead {
|
|
359
332
|
revision: string;
|
|
360
333
|
state: { contract: string; current_working: string | null };
|
|
@@ -420,7 +393,7 @@ export async function projectAssurance(
|
|
|
420
393
|
},
|
|
421
394
|
): Promise<AssuranceProjectionResult> {
|
|
422
395
|
const mod = await import(/* @vite-ignore */ kernelPath("assurance_projection"));
|
|
423
|
-
return mod.projectAssurance(root, taskId, diffProvider) as
|
|
396
|
+
return mod.projectAssurance(root, taskId, diffProvider) as AssuranceProjectionResult;
|
|
424
397
|
}
|
|
425
398
|
|
|
426
399
|
export async function deriveAssuranceAuthorization(input: {
|
|
@@ -428,5 +401,5 @@ export async function deriveAssuranceAuthorization(input: {
|
|
|
428
401
|
open_user_decision_count: number;
|
|
429
402
|
}): Promise<AssuranceAuthorizationReadiness> {
|
|
430
403
|
const mod = await import(/* @vite-ignore */ kernelPath("assurance_projection"));
|
|
431
|
-
return mod.deriveAssuranceAuthorization(input) as
|
|
404
|
+
return mod.deriveAssuranceAuthorization(input) as AssuranceAuthorizationReadiness;
|
|
432
405
|
}
|
|
@@ -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.4";
|
|
46
46
|
|
|
47
47
|
// plugins/immune-brain/runtime/claude/interaction.ts
|
|
48
48
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -2484,9 +2484,6 @@ function captureReviewManifest(root, input) {
|
|
|
2484
2484
|
throw new Error("immutable review manifest metadata exceeds bounded output limit");
|
|
2485
2485
|
return manifest;
|
|
2486
2486
|
}
|
|
2487
|
-
function ensureReviewRevision(root, input) {
|
|
2488
|
-
return publishInput(root, input).revision;
|
|
2489
|
-
}
|
|
2490
2487
|
function writeNativeReviewEvidence(payload) {
|
|
2491
2488
|
const rawDirectory = mkdtempSync(join4(tmpdir2(), "imm-canary-native-review-"));
|
|
2492
2489
|
try {
|
|
@@ -2716,6 +2713,9 @@ var TASK_RECORD_CONTRACT_V2 = "assurance_kernel/task_record/v2";
|
|
|
2716
2713
|
var TASK_RECORD_CONTRACT_V3 = "assurance_kernel/task_record/v3";
|
|
2717
2714
|
var TASK_RECORD_CONTRACT_V4 = "assurance_kernel/task_record/v4";
|
|
2718
2715
|
var REVIEW_REVISION_IDENTITY_CONTRACT = "assurance_kernel/review_revision_identity/v1";
|
|
2716
|
+
function isTaskRecordV4(record) {
|
|
2717
|
+
return record.contract === TASK_RECORD_CONTRACT_V4;
|
|
2718
|
+
}
|
|
2719
2719
|
var REDUCED_MUTATION_BRAND = Symbol("assurance-kernel-reduced-mutation-v2");
|
|
2720
2720
|
var MUTATION_AUTHORITY_CAPABILITY_BRAND = Symbol("assurance-kernel-mutation-authority-capability");
|
|
2721
2721
|
|
|
@@ -5737,6 +5737,8 @@ function applyTaskAction(input) {
|
|
|
5737
5737
|
};
|
|
5738
5738
|
}
|
|
5739
5739
|
if (input.terminal) {
|
|
5740
|
+
if (nextRecord.lifecycle === "active")
|
|
5741
|
+
throw new Error("terminal settlement requires a done or stopped TaskRecord lifecycle");
|
|
5740
5742
|
const tombstone = {
|
|
5741
5743
|
contract: TASK_TOMBSTONE_CONTRACT,
|
|
5742
5744
|
task_id,
|
|
@@ -6476,6 +6478,8 @@ function enrollCanaryTask(root, input, registry) {
|
|
|
6476
6478
|
if (checks.gitBaseHead !== gitBaseHead)
|
|
6477
6479
|
throw new Error("Git HEAD moved after the enrollment confirmation");
|
|
6478
6480
|
registry.consume(input.capability, input.capability_binding);
|
|
6481
|
+
if (!gitBaseHead)
|
|
6482
|
+
throw new Error("enrollment requires a committed Git HEAD");
|
|
6479
6483
|
const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
|
|
6480
6484
|
const nextWorkspace = {
|
|
6481
6485
|
...checks.workspace.state,
|
|
@@ -6651,6 +6655,9 @@ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput)
|
|
|
6651
6655
|
}
|
|
6652
6656
|
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
6653
6657
|
}
|
|
6658
|
+
function stopReason(value) {
|
|
6659
|
+
return typeof value === "string" && value.length > 0 ? value : "user stop";
|
|
6660
|
+
}
|
|
6654
6661
|
function assertProjectionBinding(before, after, allowDiffChange = false) {
|
|
6655
6662
|
const fields = allowDiffChange ? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"] : ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"];
|
|
6656
6663
|
if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id || fields.some((field) => before.projection[field] !== after.projection[field])) {
|
|
@@ -6660,22 +6667,57 @@ function assertProjectionBinding(before, after, allowDiffChange = false) {
|
|
|
6660
6667
|
function qaOutcomes(record) {
|
|
6661
6668
|
return Object.fromEntries(record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results).map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]));
|
|
6662
6669
|
}
|
|
6670
|
+
async function ensureClaudeReviewRevision(root, taskId, projection) {
|
|
6671
|
+
const current = await readTaskRecord(root, taskId);
|
|
6672
|
+
const record = current.record;
|
|
6673
|
+
if (!record)
|
|
6674
|
+
throw new Error(`task ${taskId} has no TaskRecord`);
|
|
6675
|
+
if (current.revision !== projection.projection.record_revision)
|
|
6676
|
+
throw new Error("TaskRecord changed before Review revision preparation");
|
|
6677
|
+
if (record.contract !== "assurance_kernel/task_record/v4")
|
|
6678
|
+
return null;
|
|
6679
|
+
if (!record.git_base_head)
|
|
6680
|
+
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
6681
|
+
const manifest = captureReviewManifest(root, {
|
|
6682
|
+
taskId,
|
|
6683
|
+
baseHead: record.git_base_head,
|
|
6684
|
+
scopeHint: record.intent_snapshot.scope_hint,
|
|
6685
|
+
expectedDiffHash: projection.projection.diff_hash,
|
|
6686
|
+
intentRevision: projection.projection.intent_revision,
|
|
6687
|
+
intentContentHash: projection.projection.intent_content_hash,
|
|
6688
|
+
recordRevision: projection.projection.record_revision,
|
|
6689
|
+
workspaceRevision: projection.projection.workspace_revision,
|
|
6690
|
+
lifecycle: projection.projection.lifecycle,
|
|
6691
|
+
artifactState: projection.projection.artifact_state,
|
|
6692
|
+
risk: record.intent_snapshot.risk,
|
|
6693
|
+
outcomes: qaOutcomes(record)
|
|
6694
|
+
});
|
|
6695
|
+
return {
|
|
6696
|
+
contract: "assurance_kernel/review_revision/v1",
|
|
6697
|
+
base_head: manifest.base_head,
|
|
6698
|
+
review_tree: manifest.review_tree,
|
|
6699
|
+
review_commit: manifest.review_commit,
|
|
6700
|
+
review_ref: manifest.review_ref,
|
|
6701
|
+
diff_hash: manifest.diff_hash,
|
|
6702
|
+
manifest_digest: manifest.manifest_digest
|
|
6703
|
+
};
|
|
6704
|
+
}
|
|
6663
6705
|
async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
|
|
6664
|
-
const
|
|
6665
|
-
|
|
6706
|
+
const read = await readTaskRecord(root, taskId);
|
|
6707
|
+
const record = read.record;
|
|
6708
|
+
if (!record || read.revision !== projection.projection.record_revision)
|
|
6666
6709
|
throw new Error("TaskRecord changed before assurance snapshot capture");
|
|
6667
|
-
const intent = record.
|
|
6710
|
+
const intent = record.intent_snapshot;
|
|
6668
6711
|
const descriptors = new Map;
|
|
6669
6712
|
for (const item of intent.acceptance) {
|
|
6670
6713
|
const descriptor = parseVerificationDescriptor(item.verification);
|
|
6671
6714
|
assertRunnerCompatible(descriptor, runner);
|
|
6672
6715
|
descriptors.set(item.id, descriptor);
|
|
6673
6716
|
}
|
|
6674
|
-
const
|
|
6675
|
-
const
|
|
6676
|
-
const reviewManifest = role === "review" && v4 ? captureReviewManifest(root, {
|
|
6717
|
+
const reviewBundle = role === "review" && !isTaskRecordV4(record) ? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record)) : null;
|
|
6718
|
+
const reviewManifest = role === "review" && isTaskRecordV4(record) ? captureReviewManifest(root, {
|
|
6677
6719
|
taskId,
|
|
6678
|
-
baseHead: record.
|
|
6720
|
+
baseHead: record.git_base_head,
|
|
6679
6721
|
scopeHint: intent.scope_hint,
|
|
6680
6722
|
expectedDiffHash: projection.projection.diff_hash,
|
|
6681
6723
|
intentRevision: projection.projection.intent_revision,
|
|
@@ -6685,7 +6727,7 @@ async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
|
|
|
6685
6727
|
lifecycle: projection.projection.lifecycle,
|
|
6686
6728
|
artifactState: projection.projection.artifact_state,
|
|
6687
6729
|
risk: intent.risk,
|
|
6688
|
-
outcomes: qaOutcomes(record
|
|
6730
|
+
outcomes: qaOutcomes(record)
|
|
6689
6731
|
}) : null;
|
|
6690
6732
|
const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : reviewBundle ? Object.keys(reviewBundle.dirty_files) : [];
|
|
6691
6733
|
const snapshot = {
|
|
@@ -6784,11 +6826,11 @@ class ClaudeRuntime {
|
|
|
6784
6826
|
this.interactive = options.interactive ?? true;
|
|
6785
6827
|
this.requestConfirmation = options.requestConfirmation;
|
|
6786
6828
|
this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
|
|
6787
|
-
|
|
6788
|
-
this.
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6829
|
+
this.coordinator = new AssuranceCoordinator({
|
|
6830
|
+
...this.createKernelPorts(),
|
|
6831
|
+
...options.ports,
|
|
6832
|
+
host: this.host
|
|
6833
|
+
});
|
|
6792
6834
|
}
|
|
6793
6835
|
observe(event) {
|
|
6794
6836
|
this.host.observe(event);
|
|
@@ -6802,27 +6844,18 @@ class ClaudeRuntime {
|
|
|
6802
6844
|
async shutdown() {
|
|
6803
6845
|
await this.coordinator.onSessionShutdown();
|
|
6804
6846
|
}
|
|
6847
|
+
kernelPorts() {
|
|
6848
|
+
return this.createKernelPorts();
|
|
6849
|
+
}
|
|
6805
6850
|
createKernelPorts() {
|
|
6806
6851
|
return {
|
|
6807
6852
|
host: this.host,
|
|
6808
6853
|
projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
|
|
6809
|
-
readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
|
|
6810
|
-
readTaskIntent: (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
6854
|
+
readTaskRecord: async (root, taskId) => readTaskRecord(root, taskId),
|
|
6855
|
+
readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
6811
6856
|
frozenRunner: async () => resolveBunRunner(),
|
|
6812
6857
|
buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
|
|
6813
|
-
ensureReviewRevision:
|
|
6814
|
-
const current = await readTaskRecord(root, taskId);
|
|
6815
|
-
if (!current.record)
|
|
6816
|
-
throw new Error(`task ${taskId} has no TaskRecord`);
|
|
6817
|
-
if (current.record.contract !== "assurance_kernel/task_record/v4")
|
|
6818
|
-
return null;
|
|
6819
|
-
return ensureReviewRevision(root, {
|
|
6820
|
-
taskId,
|
|
6821
|
-
baseHead: current.record.git_base_head,
|
|
6822
|
-
scopeHint: current.record.intent_snapshot.scope_hint,
|
|
6823
|
-
expectedDiffHash: projection.projection.diff_hash
|
|
6824
|
-
});
|
|
6825
|
-
},
|
|
6858
|
+
ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
|
|
6826
6859
|
runQa: (snapshot, descriptors, runner, options) => runDeterministicQa(snapshot, descriptors, runner, options),
|
|
6827
6860
|
writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
|
|
6828
6861
|
applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
|
|
@@ -7037,7 +7070,7 @@ class ClaudeRuntime {
|
|
|
7037
7070
|
confirmation_ref: confirmation,
|
|
7038
7071
|
...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
|
|
7039
7072
|
...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
|
|
7040
|
-
...op === "stop" ? { reason: extra.reason
|
|
7073
|
+
...op === "stop" ? { reason: stopReason(extra.reason) } : {}
|
|
7041
7074
|
});
|
|
7042
7075
|
throwIfCancelled(meta.signal);
|
|
7043
7076
|
const result = app.execute({
|
|
@@ -7049,7 +7082,7 @@ class ClaudeRuntime {
|
|
|
7049
7082
|
actor_id: actorId,
|
|
7050
7083
|
...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
|
|
7051
7084
|
...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
|
|
7052
|
-
...op === "stop" ? { reason: extra.reason
|
|
7085
|
+
...op === "stop" ? { reason: stopReason(extra.reason) } : {}
|
|
7053
7086
|
},
|
|
7054
7087
|
prior_intent_token: priorIntent.token,
|
|
7055
7088
|
diffProvider: diffSnapshotOf,
|
|
@@ -17,6 +17,7 @@ import { createInvocationRegistry, type InvocationState, type InvocationToken }
|
|
|
17
17
|
import type { ReviewBundle, ReviewManifestV5, ReviewRevision } from "./review_evidence";
|
|
18
18
|
import { buildRoleDelegationPacket } from "../role_prompt_bridge";
|
|
19
19
|
import type { AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
20
|
+
import type { TaskIntentIdentityToken } from "../kernel/intent_token_registry";
|
|
20
21
|
import type { AssuranceHostPort, HostReviewReservation } from "./host_port";
|
|
21
22
|
|
|
22
23
|
export type AssuranceRole = "qa" | "review";
|
|
@@ -38,7 +39,11 @@ export interface TaskRecordRead {
|
|
|
38
39
|
record?: { contract?: string; findings: Array<{ kind: string; status: string }> } | null;
|
|
39
40
|
}
|
|
40
41
|
export interface TaskIntentRead {
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The kernel's own intent identity token. Declared as `string` before this
|
|
44
|
+
* port was ever type checked, which is a shape no host reader has produced.
|
|
45
|
+
*/
|
|
46
|
+
token?: TaskIntentIdentityToken;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export interface GithubTerminalProjectionInput {
|
|
@@ -873,7 +878,17 @@ export class AssuranceCoordinator {
|
|
|
873
878
|
}
|
|
874
879
|
}
|
|
875
880
|
|
|
876
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Declared as the wider advance result, which made it unusable from
|
|
883
|
+
* `submitReview` without an implicit widen. The value has always been the
|
|
884
|
+
* settlement_unknown member both result unions share.
|
|
885
|
+
*/
|
|
886
|
+
private unknownAfterCommit(
|
|
887
|
+
taskId: string,
|
|
888
|
+
operation: "qa" | "review",
|
|
889
|
+
operationId: string,
|
|
890
|
+
reason: string,
|
|
891
|
+
): { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string } {
|
|
877
892
|
this.unknownOperations.set(taskId, { operation, operationId, reason });
|
|
878
893
|
return { state: "settlement_unknown", operation, operation_id: operationId, reason };
|
|
879
894
|
}
|
|
@@ -240,14 +240,32 @@ const SNAPSHOT_IDENTITY = {
|
|
|
240
240
|
date: "1970-01-01T00:00:00 +0000",
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
/**
|
|
244
|
+
* The bare synthetic-commit identity. `publishReviewRevision` can prove these
|
|
245
|
+
* five fields from Git alone; it has no manifest inputs and therefore cannot
|
|
246
|
+
* produce a digest.
|
|
247
|
+
*/
|
|
248
|
+
export interface ReviewRevisionCommit {
|
|
244
249
|
contract: "assurance_kernel/review_revision/v1";
|
|
245
250
|
base_head: string;
|
|
246
251
|
review_tree: string;
|
|
247
252
|
review_commit: string;
|
|
248
253
|
review_ref: string;
|
|
249
254
|
diff_hash: string;
|
|
250
|
-
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The full Review revision identity a host port must return.
|
|
259
|
+
*
|
|
260
|
+
* `manifest_digest` was optional here, which is the type-level hole that let a
|
|
261
|
+
* host return the bare commit identity and still satisfy
|
|
262
|
+
* `AssuranceCoordinatorPorts.ensureReviewRevision`. `submitReview` compares all
|
|
263
|
+
* four identity fields against the reservation, so an absent digest compared
|
|
264
|
+
* against a real one and failed every v4 submission at runtime instead of at
|
|
265
|
+
* build time. Requiring it makes that omission a compile error.
|
|
266
|
+
*/
|
|
267
|
+
export interface ReviewRevision extends ReviewRevisionCommit {
|
|
268
|
+
manifest_digest: string;
|
|
251
269
|
}
|
|
252
270
|
|
|
253
271
|
export interface ReviewManifestV5 {
|
|
@@ -354,7 +372,7 @@ export function publishReviewRevision(
|
|
|
354
372
|
snapshot: GitTaskRevisionSnapshot,
|
|
355
373
|
diffHash: string,
|
|
356
374
|
taskId: string,
|
|
357
|
-
):
|
|
375
|
+
): ReviewRevisionCommit {
|
|
358
376
|
if (snapshot.base_head !== snapshot.base_head.toLowerCase() || !GIT_COMMIT_ID.test(snapshot.base_head))
|
|
359
377
|
throw new Error("review revision base has invalid identity");
|
|
360
378
|
if (!REVISION_DIFF_HASH.test(diffHash)) throw new Error("review revision diff hash has invalid identity");
|
|
@@ -429,7 +447,7 @@ function publishInput(
|
|
|
429
447
|
scopeHint: unknown;
|
|
430
448
|
expectedDiffHash: string;
|
|
431
449
|
},
|
|
432
|
-
): { snapshot: GitTaskRevisionSnapshot; revision:
|
|
450
|
+
): { snapshot: GitTaskRevisionSnapshot; revision: ReviewRevisionCommit } {
|
|
433
451
|
if (typeof input.baseHead !== "string" || !GIT_COMMIT_ID.test(input.baseHead))
|
|
434
452
|
throw new Error("review requires a TaskRecord v4 git_base_head");
|
|
435
453
|
if (!REVISION_DIFF_HASH.test(input.expectedDiffHash))
|
|
@@ -486,14 +504,6 @@ export function captureReviewManifest(
|
|
|
486
504
|
return manifest;
|
|
487
505
|
}
|
|
488
506
|
|
|
489
|
-
/** Publish and return the exact revision a v4 task must use. */
|
|
490
|
-
export function ensureReviewRevision(
|
|
491
|
-
root: string,
|
|
492
|
-
input: { taskId: string; baseHead: string; scopeHint: unknown; expectedDiffHash: string },
|
|
493
|
-
): ReviewRevision {
|
|
494
|
-
return publishInput(root, input).revision;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
507
|
export function listReviewRefs(root: string): Array<{ ref: string; commit: string; taskId: string }> {
|
|
498
508
|
const output = gitEvidence(root, ["for-each-ref", "--format=%(refname) %(objectname)", `${REVIEW_REF_NAMESPACE}/`]);
|
|
499
509
|
const refs: Array<{ ref: string; commit: string; taskId: string }> = [];
|
|
@@ -534,7 +544,7 @@ export function reconcileReviewRefs(
|
|
|
534
544
|
return { removed, failed };
|
|
535
545
|
}
|
|
536
546
|
|
|
537
|
-
export function deleteReviewRef(root: string, revision:
|
|
547
|
+
export function deleteReviewRef(root: string, revision: ReviewRevisionCommit): void {
|
|
538
548
|
const parts = revision.review_ref.split("/");
|
|
539
549
|
let validRef = false;
|
|
540
550
|
if (
|
|
@@ -20,12 +20,12 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
captureReviewBundle,
|
|
22
22
|
captureReviewManifest,
|
|
23
|
-
ensureReviewRevision,
|
|
24
23
|
writeNativeReviewEvidence,
|
|
24
|
+
type ReviewRevision,
|
|
25
25
|
} from "../assurance/review_evidence";
|
|
26
26
|
import { parseVerificationDescriptor } from "../verification_descriptor";
|
|
27
|
-
import { projectAssurance, type AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
28
|
-
import type
|
|
27
|
+
import { projectAssurance, type AssuranceProjection, type AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
28
|
+
import { isTaskRecordV4, type TaskApprovalV2, type TaskRecord } from "../kernel/types";
|
|
29
29
|
import { readTaskRecord, readTaskRecordRaw } from "../kernel/storage";
|
|
30
30
|
import { canonicalIntentHash, parseTaskIntentV1, readTaskIntent } from "../kernel/intent";
|
|
31
31
|
import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
isPrivilegedOperation,
|
|
52
52
|
NativeAuthorityError,
|
|
53
53
|
type NativeConfirmationPort,
|
|
54
|
+
type PrivilegedOperation,
|
|
54
55
|
} from "./interaction";
|
|
55
56
|
import { ClaudeReviewHost, FileHookEventLog, type ClaudeHookEvent } from "./review_host";
|
|
56
57
|
import { probeHost, type PermissionMode } from "./capability";
|
|
@@ -129,10 +130,15 @@ export async function submitClaudeReview(
|
|
|
129
130
|
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/** `extra` arrives as `Record<string, unknown>`; only a real string is a reason. */
|
|
134
|
+
function stopReason(value: unknown): string {
|
|
135
|
+
return typeof value === "string" && value.length > 0 ? value : "user stop";
|
|
136
|
+
}
|
|
137
|
+
|
|
132
138
|
function assertProjectionBinding(before: AssuranceProjectionResult, after: AssuranceProjectionResult, allowDiffChange = false): void {
|
|
133
|
-
const fields =
|
|
139
|
+
const fields: ReadonlyArray<keyof AssuranceProjection> = allowDiffChange
|
|
134
140
|
? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"]
|
|
135
|
-
: ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"]
|
|
141
|
+
: ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"];
|
|
136
142
|
if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id
|
|
137
143
|
|| fields.some((field) => before.projection[field] !== after.projection[field])) {
|
|
138
144
|
throw new Error("Task changed after native confirmation; authority aborted before capability issuance");
|
|
@@ -146,6 +152,58 @@ function qaOutcomes(record: { attestations: Array<{ kind: string; acceptance_res
|
|
|
146
152
|
);
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Publish the task-scoped synthetic revision for a v4 record and return the
|
|
157
|
+
* exact identity the Review snapshot binds.
|
|
158
|
+
*
|
|
159
|
+
* `submitReview` re-derives this identity and compares all four fields —
|
|
160
|
+
* `manifest_digest` included — against the reservation. Returning the bare
|
|
161
|
+
* commit identity therefore compared a real digest against `undefined` and
|
|
162
|
+
* failed every v4 submission with "Review revision changed before submission",
|
|
163
|
+
* so the manifest is recomputed here rather than only the commit. The outcomes
|
|
164
|
+
* come from the same `qaOutcomes` the Review snapshot is built from, which
|
|
165
|
+
* makes the two digests equal by construction instead of by coincidence.
|
|
166
|
+
*
|
|
167
|
+
* v3 records keep the legacy full-source bundle and return null.
|
|
168
|
+
*/
|
|
169
|
+
export async function ensureClaudeReviewRevision(
|
|
170
|
+
root: string,
|
|
171
|
+
taskId: string,
|
|
172
|
+
projection: AssuranceProjectionResult,
|
|
173
|
+
): Promise<ReviewRevision | null> {
|
|
174
|
+
const current = await readTaskRecord(root, taskId);
|
|
175
|
+
const record = current.record;
|
|
176
|
+
if (!record) throw new Error(`task ${taskId} has no TaskRecord`);
|
|
177
|
+
if (current.revision !== projection.projection.record_revision)
|
|
178
|
+
throw new Error("TaskRecord changed before Review revision preparation");
|
|
179
|
+
if (record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
180
|
+
if (!record.git_base_head)
|
|
181
|
+
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
182
|
+
const manifest = captureReviewManifest(root, {
|
|
183
|
+
taskId,
|
|
184
|
+
baseHead: record.git_base_head,
|
|
185
|
+
scopeHint: record.intent_snapshot.scope_hint,
|
|
186
|
+
expectedDiffHash: projection.projection.diff_hash,
|
|
187
|
+
intentRevision: projection.projection.intent_revision,
|
|
188
|
+
intentContentHash: projection.projection.intent_content_hash,
|
|
189
|
+
recordRevision: projection.projection.record_revision,
|
|
190
|
+
workspaceRevision: projection.projection.workspace_revision,
|
|
191
|
+
lifecycle: projection.projection.lifecycle,
|
|
192
|
+
artifactState: projection.projection.artifact_state,
|
|
193
|
+
risk: record.intent_snapshot.risk,
|
|
194
|
+
outcomes: qaOutcomes(record),
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
contract: "assurance_kernel/review_revision/v1",
|
|
198
|
+
base_head: manifest.base_head,
|
|
199
|
+
review_tree: manifest.review_tree,
|
|
200
|
+
review_commit: manifest.review_commit,
|
|
201
|
+
review_ref: manifest.review_ref,
|
|
202
|
+
diff_hash: manifest.diff_hash,
|
|
203
|
+
manifest_digest: manifest.manifest_digest,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
149
207
|
async function buildAssuranceSnapshot(
|
|
150
208
|
root: string,
|
|
151
209
|
taskId: string,
|
|
@@ -153,23 +211,25 @@ async function buildAssuranceSnapshot(
|
|
|
153
211
|
projection: AssuranceProjectionResult,
|
|
154
212
|
runner: FrozenRunner,
|
|
155
213
|
) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
214
|
+
const read = await readTaskRecord(root, taskId);
|
|
215
|
+
const record = read.record;
|
|
216
|
+
if (!record || read.revision !== projection.projection.record_revision) throw new Error("TaskRecord changed before assurance snapshot capture");
|
|
217
|
+
const intent = record.intent_snapshot;
|
|
159
218
|
const descriptors = new Map<string, VerificationDescriptor>();
|
|
160
219
|
for (const item of intent.acceptance) {
|
|
161
220
|
const descriptor = parseVerificationDescriptor(item.verification);
|
|
162
221
|
assertRunnerCompatible(descriptor, runner);
|
|
163
222
|
descriptors.set(item.id, descriptor);
|
|
164
223
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
224
|
+
// `git_base_head` exists only on TaskRecord v4, so this must narrow the union
|
|
225
|
+
// rather than test the contract string into a plain boolean.
|
|
226
|
+
const reviewBundle = role === "review" && !isTaskRecordV4(record)
|
|
227
|
+
? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record))
|
|
168
228
|
: null;
|
|
169
|
-
const reviewManifest = role === "review" &&
|
|
229
|
+
const reviewManifest = role === "review" && isTaskRecordV4(record)
|
|
170
230
|
? captureReviewManifest(root, {
|
|
171
231
|
taskId,
|
|
172
|
-
baseHead: record.
|
|
232
|
+
baseHead: record.git_base_head,
|
|
173
233
|
scopeHint: intent.scope_hint,
|
|
174
234
|
expectedDiffHash: projection.projection.diff_hash,
|
|
175
235
|
intentRevision: projection.projection.intent_revision,
|
|
@@ -179,7 +239,7 @@ async function buildAssuranceSnapshot(
|
|
|
179
239
|
lifecycle: projection.projection.lifecycle,
|
|
180
240
|
artifactState: projection.projection.artifact_state,
|
|
181
241
|
risk: intent.risk,
|
|
182
|
-
outcomes: qaOutcomes(record
|
|
242
|
+
outcomes: qaOutcomes(record),
|
|
183
243
|
})
|
|
184
244
|
: null;
|
|
185
245
|
const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : reviewBundle ? Object.keys(reviewBundle.dirty_files) : [];
|
|
@@ -295,7 +355,13 @@ export interface ClaudeRuntimeOptions {
|
|
|
295
355
|
cwd: string;
|
|
296
356
|
env?: Record<string, string | undefined>;
|
|
297
357
|
host?: ClaudeReviewHost;
|
|
298
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Overrides layered on top of the real production ports, never a
|
|
360
|
+
* replacement for them. A whole synthetic ports object could previously be
|
|
361
|
+
* substituted here, so a suite could pass while the object production
|
|
362
|
+
* actually wires was never constructed once.
|
|
363
|
+
*/
|
|
364
|
+
ports?: Partial<AssuranceCoordinatorPorts>;
|
|
299
365
|
interactive?: boolean;
|
|
300
366
|
permissionMode?: PermissionMode;
|
|
301
367
|
requestConfirmation?: NativeConfirmationPort;
|
|
@@ -319,11 +385,11 @@ export class ClaudeRuntime {
|
|
|
319
385
|
this.interactive = options.interactive ?? true;
|
|
320
386
|
this.requestConfirmation = options.requestConfirmation;
|
|
321
387
|
this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog());
|
|
322
|
-
|
|
323
|
-
this.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
388
|
+
this.coordinator = new AssuranceCoordinator({
|
|
389
|
+
...this.createKernelPorts(),
|
|
390
|
+
...options.ports,
|
|
391
|
+
host: this.host,
|
|
392
|
+
});
|
|
327
393
|
}
|
|
328
394
|
|
|
329
395
|
observe(event: ClaudeHookEvent): void {
|
|
@@ -343,25 +409,25 @@ export class ClaudeRuntime {
|
|
|
343
409
|
await this.coordinator.onSessionShutdown();
|
|
344
410
|
}
|
|
345
411
|
|
|
412
|
+
/**
|
|
413
|
+
* The exact ports object the coordinator runs on. Public so a conformance
|
|
414
|
+
* suite can drive what production wires instead of a hand-built double: the
|
|
415
|
+
* host adapter defects that reached published plugins all lived in this
|
|
416
|
+
* object and none of them were reachable from a test while it was private.
|
|
417
|
+
*/
|
|
418
|
+
kernelPorts(): AssuranceCoordinatorPorts {
|
|
419
|
+
return this.createKernelPorts();
|
|
420
|
+
}
|
|
421
|
+
|
|
346
422
|
private createKernelPorts(): AssuranceCoordinatorPorts {
|
|
347
423
|
return {
|
|
348
424
|
host: this.host,
|
|
349
425
|
projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
|
|
350
|
-
readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
|
|
351
|
-
readTaskIntent: (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
426
|
+
readTaskRecord: async (root, taskId) => readTaskRecord(root, taskId),
|
|
427
|
+
readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
352
428
|
frozenRunner: async () => resolveBunRunner(),
|
|
353
429
|
buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
|
|
354
|
-
ensureReviewRevision:
|
|
355
|
-
const current = await readTaskRecord(root, taskId);
|
|
356
|
-
if (!current.record) throw new Error(`task ${taskId} has no TaskRecord`);
|
|
357
|
-
if (current.record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
358
|
-
return ensureReviewRevision(root, {
|
|
359
|
-
taskId,
|
|
360
|
-
baseHead: current.record.git_base_head,
|
|
361
|
-
scopeHint: current.record.intent_snapshot.scope_hint,
|
|
362
|
-
expectedDiffHash: projection.projection.diff_hash,
|
|
363
|
-
});
|
|
364
|
-
},
|
|
430
|
+
ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
|
|
365
431
|
runQa: (snapshot, descriptors, runner, options) => runDeterministicQa(snapshot, descriptors, runner, options),
|
|
366
432
|
writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
|
|
367
433
|
applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
|
|
@@ -468,7 +534,7 @@ export class ClaudeRuntime {
|
|
|
468
534
|
return repairKernelAuthority(this.cwd, taskId, authority.revision);
|
|
469
535
|
}
|
|
470
536
|
if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
|
|
471
|
-
let op = operation;
|
|
537
|
+
let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" = operation;
|
|
472
538
|
let decisionOp: { finding_id: string; resolution: string } | undefined;
|
|
473
539
|
const projection = await this.status(taskId);
|
|
474
540
|
if (projection.error || !projection.claim) throw new Error(projection.error ?? "no active backend claim");
|
|
@@ -584,7 +650,7 @@ export class ClaudeRuntime {
|
|
|
584
650
|
confirmation_ref: confirmation,
|
|
585
651
|
...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
|
|
586
652
|
...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
|
|
587
|
-
...(op === "stop" ? { reason: extra.reason
|
|
653
|
+
...(op === "stop" ? { reason: stopReason(extra.reason) } : {}),
|
|
588
654
|
});
|
|
589
655
|
throwIfCancelled(meta.signal);
|
|
590
656
|
const result = app.execute({
|
|
@@ -596,7 +662,7 @@ export class ClaudeRuntime {
|
|
|
596
662
|
actor_id: actorId,
|
|
597
663
|
...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
|
|
598
664
|
...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
|
|
599
|
-
...(op === "stop" ? { reason: extra.reason
|
|
665
|
+
...(op === "stop" ? { reason: stopReason(extra.reason) } : {}),
|
|
600
666
|
} as never,
|
|
601
667
|
prior_intent_token: priorIntent.token,
|
|
602
668
|
diffProvider: diffSnapshotOf,
|
|
@@ -672,7 +738,7 @@ export class ClaudeRuntime {
|
|
|
672
738
|
stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
673
739
|
return;
|
|
674
740
|
}
|
|
675
|
-
const approval = {
|
|
741
|
+
const approval: TaskApprovalV2 = {
|
|
676
742
|
id: `approval-${input.snapshot.role}-${randomUUID().slice(0, 8)}`,
|
|
677
743
|
kind: input.snapshot.role === "qa" ? "qa" : "review",
|
|
678
744
|
authority_role: input.snapshot.role === "qa" ? "qa" : "reviewer",
|
|
@@ -271,16 +271,6 @@ interface PendingReview {
|
|
|
271
271
|
error?: string;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
function matchesReservation(event: ClaudeHookEvent, pending: PendingReview): boolean {
|
|
275
|
-
if ("operationId" in event && event.operationId && event.operationId !== pending.request.operationId) return false;
|
|
276
|
-
if ("taskId" in event && event.taskId && event.taskId !== pending.request.taskId) return false;
|
|
277
|
-
if (pending.sessionId && event.sessionId !== pending.sessionId) return false;
|
|
278
|
-
const eventAgentId = "agentId" in event ? event.agentId : "";
|
|
279
|
-
if (event.type === "SubagentStop" && (!eventAgentId || !pending.agentId || eventAgentId !== pending.agentId)) return false;
|
|
280
|
-
if (pending.agentId && eventAgentId && eventAgentId !== pending.agentId) return false;
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
274
|
function bindsStart(event: Extract<ClaudeHookEvent, { type: "SubagentStart" }>, pending: PendingReview): boolean {
|
|
285
275
|
if (event.taskId && event.taskId !== pending.request.taskId) return false;
|
|
286
276
|
if (event.prompt !== undefined) {
|
|
@@ -275,8 +275,8 @@ export function createGhTransport(binary = "gh"): GhTransport {
|
|
|
275
275
|
return {
|
|
276
276
|
run(args, options = {}) {
|
|
277
277
|
return new Promise((complete) => {
|
|
278
|
-
let stdout = Buffer.alloc(0);
|
|
279
|
-
let stderr = Buffer.alloc(0);
|
|
278
|
+
let stdout: Buffer = Buffer.alloc(0);
|
|
279
|
+
let stderr: Buffer = Buffer.alloc(0);
|
|
280
280
|
let timedOut = false;
|
|
281
281
|
let outputExceeded = false;
|
|
282
282
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -304,7 +304,7 @@ export function createGhTransport(binary = "gh"): GhTransport {
|
|
|
304
304
|
finish(1, error instanceof Error ? error.message : String(error));
|
|
305
305
|
return;
|
|
306
306
|
}
|
|
307
|
-
const append = (current: Buffer, chunk:
|
|
307
|
+
const append = (current: Buffer, chunk: Uint8Array): Buffer => {
|
|
308
308
|
const available = Math.max(0, MAX_GH_OUTPUT - stdout.length - stderr.length);
|
|
309
309
|
if (chunk.length > available) {
|
|
310
310
|
outputExceeded = true;
|
|
@@ -312,17 +312,22 @@ export function createGhTransport(binary = "gh"): GhTransport {
|
|
|
312
312
|
}
|
|
313
313
|
return available > 0 ? Buffer.concat([current, chunk.subarray(0, available)]) : current;
|
|
314
314
|
};
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
const { stdout: childOut, stderr: childErr, stdin: childIn } = child;
|
|
316
|
+
if (!childOut || !childErr || !childIn) {
|
|
317
|
+
finish(1, "gh was spawned without the stdio pipes this reader requires");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
childOut.on("data", (chunk: Uint8Array) => { stdout = append(stdout, chunk); });
|
|
321
|
+
childErr.on("data", (chunk: Uint8Array) => { stderr = append(stderr, chunk); });
|
|
317
322
|
child.once("error", (error) => { finish(1, error.message); });
|
|
318
|
-
|
|
323
|
+
childIn.once("error", (error) => { finish(1, error.message); });
|
|
319
324
|
timer = setTimeout(() => {
|
|
320
325
|
timedOut = true;
|
|
321
326
|
child.kill("SIGKILL");
|
|
322
327
|
}, GH_TIMEOUT_MS);
|
|
323
328
|
child.once("close", (code) => { finish(code ?? 1); });
|
|
324
329
|
try {
|
|
325
|
-
|
|
330
|
+
childIn.end(options.stdin ?? "");
|
|
326
331
|
} catch (error) {
|
|
327
332
|
finish(1, error instanceof Error ? error.message : String(error));
|
|
328
333
|
}
|
|
@@ -371,7 +376,7 @@ function parseSubIssueNumbers(raw: string): number[] {
|
|
|
371
376
|
if (!Array.isArray(pages)) throw new Error("gh returned malformed Sub-issue list");
|
|
372
377
|
return pages.map((item, index) => {
|
|
373
378
|
const number = (item as { number?: unknown })?.number;
|
|
374
|
-
if (!Number.isSafeInteger(number)) throw new Error(`gh returned a malformed Sub-issue entry at ${index}`);
|
|
379
|
+
if (typeof number !== "number" || !Number.isSafeInteger(number)) throw new Error(`gh returned a malformed Sub-issue entry at ${index}`);
|
|
375
380
|
return number;
|
|
376
381
|
});
|
|
377
382
|
}
|
|
@@ -260,6 +260,11 @@ export function applyTaskAction(
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
if (input.terminal) {
|
|
263
|
+
// A tombstone may only record a terminal lifecycle. Nothing upstream
|
|
264
|
+
// proved that, so an active record could have been tombstoned as
|
|
265
|
+
// `terminal_lifecycle: "active"` in violation of its own contract.
|
|
266
|
+
if (nextRecord.lifecycle === "active")
|
|
267
|
+
throw new Error("terminal settlement requires a done or stopped TaskRecord lifecycle");
|
|
263
268
|
const tombstone: TaskTombstone = {
|
|
264
269
|
contract: TASK_TOMBSTONE_CONTRACT,
|
|
265
270
|
task_id,
|
|
@@ -244,6 +244,10 @@ export function enrollCanaryTask(
|
|
|
244
244
|
// consume immediately before the marker write
|
|
245
245
|
registry.consume(input.capability, input.capability_binding);
|
|
246
246
|
|
|
247
|
+
// Set by beforeLock above, which throws when the repository has no
|
|
248
|
+
// committed HEAD. Re-assert it here: the compiler cannot carry a
|
|
249
|
+
// closure's narrowing across to this one.
|
|
250
|
+
if (!gitBaseHead) throw new Error("enrollment requires a committed Git HEAD");
|
|
247
251
|
const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
|
|
248
252
|
const nextWorkspace: WorkspaceStateLike = {
|
|
249
253
|
...checks.workspace.state,
|
|
@@ -229,10 +229,12 @@ export function buildAuthorityObservationSeedV2(
|
|
|
229
229
|
function committedReceiptV2(
|
|
230
230
|
receipt: AuthorityCommitReceipt,
|
|
231
231
|
): receipt is AuthorityCommitReceipt & {
|
|
232
|
+
contract: "assurance_kernel/authority_commit_receipt/v2";
|
|
232
233
|
status: "committed" | "recovered_committed";
|
|
233
234
|
observation_seed: AuthorityObservationSeedV2;
|
|
234
235
|
} {
|
|
235
236
|
return (
|
|
237
|
+
receipt.contract === "assurance_kernel/authority_commit_receipt/v2" &&
|
|
236
238
|
(receipt.status === "committed" ||
|
|
237
239
|
receipt.status === "recovered_committed") &&
|
|
238
240
|
receipt.observation_seed !== undefined
|
|
@@ -78,7 +78,22 @@ export type JournalReasonCode =
|
|
|
78
78
|
| "source_read_failed"
|
|
79
79
|
| "shadow_divergence"
|
|
80
80
|
| "migration_ambiguous"
|
|
81
|
-
| "readiness_query_nonqualifying"
|
|
81
|
+
| "readiness_query_nonqualifying"
|
|
82
|
+
// Emitted by runtime/commands/kernel.ts. Absent from this union until the
|
|
83
|
+
// journal types were first exported and type checked.
|
|
84
|
+
| "routing_policy_invalid"
|
|
85
|
+
| "routing_unavailable"
|
|
86
|
+
| "kernel_owner_active"
|
|
87
|
+
| "v3_owner_nonterminal"
|
|
88
|
+
| "input_oversize"
|
|
89
|
+
| "input_invalid"
|
|
90
|
+
| "intent_invalid"
|
|
91
|
+
| "task_path_mismatch"
|
|
92
|
+
| "destination_invalid"
|
|
93
|
+
| "destination_parent_invalid"
|
|
94
|
+
| "destination_parent_missing"
|
|
95
|
+
| "destination_exists"
|
|
96
|
+
| "destination_write_failed";
|
|
82
97
|
|
|
83
98
|
export interface JournalEntry {
|
|
84
99
|
contract: "assurance_kernel/journal/v1";
|
|
@@ -1337,7 +1352,7 @@ export function commitEnrollmentLocked(
|
|
|
1337
1352
|
taskId: string,
|
|
1338
1353
|
transaction: WorkspaceTransactionV2,
|
|
1339
1354
|
claim: Record<string, unknown>,
|
|
1340
|
-
): { record:
|
|
1355
|
+
): { record: TaskRecord; workspace: WorkspaceState } {
|
|
1341
1356
|
const marker: EnrollmentMarker = {
|
|
1342
1357
|
contract: "assurance_kernel/enrollment_transaction/v1",
|
|
1343
1358
|
task_id: taskId,
|
|
@@ -700,7 +700,7 @@ export function migrateLegacyLayout(root: string): MigrationOutcome {
|
|
|
700
700
|
affected_paths: initial.dirty_affected_paths,
|
|
701
701
|
reason: initial.reason,
|
|
702
702
|
};
|
|
703
|
-
if (
|
|
703
|
+
if (initial.layout === "migration_blocked_active" || initial.layout === "invalid")
|
|
704
704
|
return {
|
|
705
705
|
contract: "immune_brain/storage_layout_migration_result/v1",
|
|
706
706
|
outcome: initial.layout,
|
|
@@ -242,10 +242,6 @@ function inspectOldLayout(root: string): OldLayoutFacts {
|
|
|
242
242
|
facts.blocked_active = true;
|
|
243
243
|
continue;
|
|
244
244
|
}
|
|
245
|
-
if (kind === "marker") {
|
|
246
|
-
facts.pending_marker ??= path;
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
245
|
if (kind === "claim") {
|
|
250
246
|
facts.blocked_active = true;
|
|
251
247
|
continue;
|
|
@@ -235,6 +235,15 @@ export interface TaskRecordV4 extends Omit<TaskRecordV3, "contract"> {
|
|
|
235
235
|
/** The record shape every Kernel owner passes around during the v3 drain window. */
|
|
236
236
|
export type TaskRecord = TaskRecordV3 | TaskRecordV4;
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Narrow the stored record union before reading a v4-only field such as
|
|
240
|
+
* `git_base_head`. Comparing `record.contract` into a plain boolean does not
|
|
241
|
+
* narrow, which let adapters read v4 fields off a v3-shaped value unchecked.
|
|
242
|
+
*/
|
|
243
|
+
export function isTaskRecordV4(record: TaskRecord): record is TaskRecordV4 {
|
|
244
|
+
return record.contract === TASK_RECORD_CONTRACT_V4;
|
|
245
|
+
}
|
|
246
|
+
|
|
238
247
|
export interface TaskProjectionV3 extends CompletionDecision {
|
|
239
248
|
contract: "assurance_kernel/projection/v3";
|
|
240
249
|
task_id: string;
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { execFileSync } from "node:child_process";
|
|
28
28
|
import { createHash } from "node:crypto";
|
|
29
|
-
import {
|
|
29
|
+
import { type Stats,
|
|
30
30
|
closeSync,
|
|
31
31
|
constants as fsConstants,
|
|
32
32
|
fstatSync,
|
|
@@ -145,7 +145,7 @@ export function setRoutingPolicyReaderTestHook(
|
|
|
145
145
|
routingPolicyReaderTestHook = hook;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
function statIdentity(stat:
|
|
148
|
+
function statIdentity(stat: Stats): {
|
|
149
149
|
dev: number;
|
|
150
150
|
ino: number;
|
|
151
151
|
size: number;
|
|
@@ -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.4" as const;
|