immune-brain 3.6.3 → 3.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +117 -47
- 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 +213 -81
- package/plugins/immune-brain/dist/imm-loop.md +8 -2
- package/plugins/immune-brain/dist/imm-planner.md +9 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +35 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +23 -13
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +133 -45
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +14 -1
- package/plugins/immune-brain/runtime/claude/review_host.ts +0 -10
- package/plugins/immune-brain/runtime/commands/kernel.ts +15 -13
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +107 -11
- package/plugins/immune-brain/runtime/kernel/application.ts +6 -0
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +407 -0
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +17 -8
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +76 -13
- package/plugins/immune-brain/runtime/kernel/index.ts +2 -0
- package/plugins/immune-brain/runtime/kernel/intent.ts +67 -23
- package/plugins/immune-brain/runtime/kernel/observation.ts +2 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +34 -8
- 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 +10 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +10 -6
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +2 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
|
@@ -55,6 +55,7 @@ export type CanaryOperation =
|
|
|
55
55
|
| { op: "approve_breaking_intent_revision"; capability: object; next_intent: TaskIntentV1; actor_id: string }
|
|
56
56
|
| { op: "complete"; actor_id: string }
|
|
57
57
|
| { op: "stop"; capability: object; reason: string; actor_id: string }
|
|
58
|
+
| { op: "authorize_rework"; capability: object; actor_id: string }
|
|
58
59
|
| { op: "resolve_user_decision"; capability: object; finding_id: string; resolution: string; actor_id: string };
|
|
59
60
|
|
|
60
61
|
export interface CanaryExecuteInput {
|
|
@@ -142,6 +143,8 @@ export function capabilityActionFor(input: {
|
|
|
142
143
|
return { ...base, approval: input.approval } as TaskAction;
|
|
143
144
|
case "request_rework":
|
|
144
145
|
return { ...base, findings: input.findings } as TaskAction;
|
|
146
|
+
case "authorize_rework":
|
|
147
|
+
return { ...base, type: "authorize_rework" } as TaskAction;
|
|
145
148
|
case "stop":
|
|
146
149
|
return { ...base, reason: input.reason } as TaskAction;
|
|
147
150
|
case "approve_breaking_intent_revision":
|
|
@@ -326,15 +329,17 @@ export function createCanaryApplication(
|
|
|
326
329
|
);
|
|
327
330
|
if (operation.op === "complete" && hasBoundSpec && snapshot.record.artifact_state !== "frozen")
|
|
328
331
|
throw new KernelInvariantError(["complete requires frozen planning artifacts"]);
|
|
329
|
-
const artifactTransition =
|
|
330
|
-
&&
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
const artifactTransition =
|
|
333
|
+
snapshot.record.artifact_state === "frozen" &&
|
|
334
|
+
(
|
|
335
|
+
operation.op === "request_rework" ||
|
|
336
|
+
operation.op === "authorize_rework" ||
|
|
337
|
+
operation.op === "approve_breaking_intent_revision"
|
|
333
338
|
)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
339
|
+
? transitionFor(input.root, snapshot.record, "restore")
|
|
340
|
+
: operation.op === "stop" && snapshot.record.artifact_state !== "frozen"
|
|
341
|
+
? transitionFor(input.root, snapshot.record, "freeze", true)
|
|
342
|
+
: undefined;
|
|
338
343
|
const event_id = `${operation.op}:${input.task_id}:${at}`;
|
|
339
344
|
const base = {
|
|
340
345
|
event_id,
|
|
@@ -400,6 +405,10 @@ export function createCanaryApplication(
|
|
|
400
405
|
capability = operation.capability;
|
|
401
406
|
action = { ...base, type: "stop", reason: operation.reason };
|
|
402
407
|
break;
|
|
408
|
+
case "authorize_rework":
|
|
409
|
+
capability = operation.capability;
|
|
410
|
+
action = { ...base, type: "authorize_rework" };
|
|
411
|
+
break;
|
|
403
412
|
case "resolve_user_decision":
|
|
404
413
|
capability = operation.capability;
|
|
405
414
|
action = {
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
type EnrollmentAuthorityRegistry,
|
|
9
9
|
type EnrollmentCapabilityBinding,
|
|
10
10
|
} from "./enrollment_authority";
|
|
11
|
+
import type {
|
|
12
|
+
BatchAuthorityRegistry,
|
|
13
|
+
BatchAuthorizationBinding,
|
|
14
|
+
} from "./batch_authority";
|
|
11
15
|
import { readTaskTombstone, type BackendClaim } from "./backend_claim";
|
|
12
16
|
import { preparePiCanary, readGitHead } from "./pi_canary_prepare";
|
|
13
17
|
import {
|
|
@@ -18,6 +22,19 @@ import {
|
|
|
18
22
|
} from "./storage";
|
|
19
23
|
import type { TaskRecord, TaskRecordV4, WorkspaceStateLike } from "./types";
|
|
20
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Present only for a batch-derived enrollment. The child slot is consumed
|
|
27
|
+
* inside the same store lock as the TaskRecord write and released again if that
|
|
28
|
+
* write does not commit, so a consumed slot and a TaskRecord always agree.
|
|
29
|
+
*/
|
|
30
|
+
export interface EnrollBatchContext {
|
|
31
|
+
registry: BatchAuthorityRegistry;
|
|
32
|
+
capability: object;
|
|
33
|
+
binding: BatchAuthorizationBinding;
|
|
34
|
+
/** base_head, then each commit this batch created on its own branch. */
|
|
35
|
+
expected_head: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
export interface EnrollCanaryInput {
|
|
22
39
|
task_id: string;
|
|
23
40
|
intent_path: string;
|
|
@@ -25,6 +42,7 @@ export interface EnrollCanaryInput {
|
|
|
25
42
|
preparation_digest: string;
|
|
26
43
|
capability: object;
|
|
27
44
|
capability_binding: EnrollmentCapabilityBinding;
|
|
45
|
+
batch?: EnrollBatchContext;
|
|
28
46
|
now: string;
|
|
29
47
|
}
|
|
30
48
|
|
|
@@ -241,9 +259,46 @@ export function enrollCanaryTask(
|
|
|
241
259
|
if (checks.gitBaseHead !== gitBaseHead)
|
|
242
260
|
throw new Error("Git HEAD moved after the enrollment confirmation");
|
|
243
261
|
|
|
262
|
+
// A batch-derived enrollment must still stand on the lineage the
|
|
263
|
+
// literal user confirmed: child N's base is the commit child N-1
|
|
264
|
+
// produced on the batch branch, and child 1's base is base_head.
|
|
265
|
+
// expected_head is caller-supplied, so anchor its origin here rather
|
|
266
|
+
// than trusting the caller's own assertion: before this batch has
|
|
267
|
+
// consumed any slot it has created no commit, so the only lineage
|
|
268
|
+
// value it can hold is the confirmed base_head.
|
|
269
|
+
if (input.batch) {
|
|
270
|
+
const batch = input.batch.registry.inspect(
|
|
271
|
+
input.batch.capability,
|
|
272
|
+
input.batch.binding,
|
|
273
|
+
Date.parse(input.now),
|
|
274
|
+
);
|
|
275
|
+
if (
|
|
276
|
+
input.batch.registry.consumedChildren(input.batch.capability).length === 0 &&
|
|
277
|
+
input.batch.expected_head !== batch.base_head
|
|
278
|
+
)
|
|
279
|
+
throw new Error(
|
|
280
|
+
`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${batch.base_head}, not ${input.batch.expected_head}`,
|
|
281
|
+
);
|
|
282
|
+
if (checks.gitBaseHead !== input.batch.expected_head)
|
|
283
|
+
throw new Error(
|
|
284
|
+
`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
244
288
|
// consume immediately before the marker write
|
|
245
289
|
registry.consume(input.capability, input.capability_binding);
|
|
290
|
+
if (input.batch)
|
|
291
|
+
input.batch.registry.consumeChild(
|
|
292
|
+
input.batch.capability,
|
|
293
|
+
input.batch.binding,
|
|
294
|
+
input.task_id,
|
|
295
|
+
Date.parse(input.now),
|
|
296
|
+
);
|
|
246
297
|
|
|
298
|
+
// Set by beforeLock above, which throws when the repository has no
|
|
299
|
+
// committed HEAD. Re-assert it here: the compiler cannot carry a
|
|
300
|
+
// closure's narrowing across to this one.
|
|
301
|
+
if (!gitBaseHead) throw new Error("enrollment requires a committed Git HEAD");
|
|
247
302
|
const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
|
|
248
303
|
const nextWorkspace: WorkspaceStateLike = {
|
|
249
304
|
...checks.workspace.state,
|
|
@@ -260,19 +315,27 @@ export function enrollCanaryTask(
|
|
|
260
315
|
created_at: input.now,
|
|
261
316
|
updated_at: input.now,
|
|
262
317
|
};
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
318
|
+
let mutation: ReturnType<typeof commitEnrollmentLocked>;
|
|
319
|
+
try {
|
|
320
|
+
mutation = commitEnrollmentLocked(
|
|
321
|
+
root,
|
|
322
|
+
input.task_id,
|
|
323
|
+
{
|
|
324
|
+
contract: "assurance_kernel/workspace_transaction/v2",
|
|
325
|
+
task_id: input.task_id,
|
|
326
|
+
expected_record_hash: checks.current.revision,
|
|
327
|
+
next_record_content: `${JSON.stringify(record, null, 2)}\n`,
|
|
328
|
+
expected_workspace_hash: checks.workspace.revision,
|
|
329
|
+
next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}\n`,
|
|
330
|
+
},
|
|
331
|
+
claim as unknown as Record<string, unknown>,
|
|
332
|
+
);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
// No TaskRecord was written, so the child slot must not stay used.
|
|
335
|
+
if (input.batch)
|
|
336
|
+
input.batch.registry.releaseChild(input.batch.capability, input.task_id);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
276
339
|
return {
|
|
277
340
|
record: mutation.record,
|
|
278
341
|
backend_claim: claim,
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
mintToken,
|
|
29
29
|
type TaskIntentIdentityToken,
|
|
30
|
+
type TokenIdentity,
|
|
30
31
|
} from "./intent_token_registry";
|
|
31
32
|
|
|
32
33
|
export const INTENT_MAX_BYTES = 64 * 1024;
|
|
@@ -406,6 +407,26 @@ export interface ReadTaskIntentResult {
|
|
|
406
407
|
token: TaskIntentIdentityToken;
|
|
407
408
|
}
|
|
408
409
|
|
|
410
|
+
export interface ObservedTaskIntent {
|
|
411
|
+
intent: TaskIntentV1;
|
|
412
|
+
content_hash: string;
|
|
413
|
+
intent_ref: TaskIntentRefV1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export class TaskIntentObservationError extends Error {
|
|
417
|
+
readonly code: "missing" | "invalid";
|
|
418
|
+
|
|
419
|
+
constructor(code: "missing" | "invalid", message: string) {
|
|
420
|
+
super(message);
|
|
421
|
+
this.name = "TaskIntentObservationError";
|
|
422
|
+
this.code = code;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
interface TaskIntentReadSource extends ObservedTaskIntent {
|
|
427
|
+
identity: TokenIdentity;
|
|
428
|
+
}
|
|
429
|
+
|
|
409
430
|
export function setIntentReaderTestHook(
|
|
410
431
|
hook: { onBeforeDescriptorRead?: () => void } | null,
|
|
411
432
|
): void {
|
|
@@ -523,11 +544,11 @@ function assertIdentitiesUnchanged(
|
|
|
523
544
|
}
|
|
524
545
|
}
|
|
525
546
|
|
|
526
|
-
|
|
547
|
+
function readTaskIntentSource(
|
|
527
548
|
root: string,
|
|
528
549
|
taskId: string,
|
|
529
550
|
requestedPath?: string,
|
|
530
|
-
):
|
|
551
|
+
): TaskIntentReadSource {
|
|
531
552
|
validateTaskId(taskId);
|
|
532
553
|
|
|
533
554
|
const canonicalRoot = resolveCanonicalRoot(root);
|
|
@@ -545,7 +566,7 @@ export function readTaskIntent(
|
|
|
545
566
|
if (!target.startsWith(canonicalRoot + sep))
|
|
546
567
|
throw new Error("intent sidecar escapes project root");
|
|
547
568
|
if (!sidecarPresent(canonicalRoot, sidecarPath))
|
|
548
|
-
throw new
|
|
569
|
+
throw new TaskIntentObservationError("missing", `TaskIntent sidecar is missing at ${sidecarPath}`);
|
|
549
570
|
|
|
550
571
|
const pathIdentities = collectPathIdentities(canonicalRoot, sidecarPath);
|
|
551
572
|
const fileIdentity = pathIdentities[pathIdentities.length - 1];
|
|
@@ -557,13 +578,19 @@ export function readTaskIntent(
|
|
|
557
578
|
["ls-files", "--error-unmatch", "--", sidecarPath],
|
|
558
579
|
{ cwd: canonicalRoot, stdio: ["ignore", "pipe", "pipe"] },
|
|
559
580
|
);
|
|
560
|
-
} catch {
|
|
561
|
-
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (
|
|
583
|
+
typeof error === "object"
|
|
584
|
+
&& error !== null
|
|
585
|
+
&& "status" in error
|
|
586
|
+
&& (error as { status?: unknown }).status === 1
|
|
587
|
+
) throw new TaskIntentObservationError("invalid", "TaskIntent sidecar is not Git-tracked");
|
|
588
|
+
throw error;
|
|
562
589
|
}
|
|
563
590
|
|
|
564
591
|
const before = lstatSync(target);
|
|
565
592
|
if (!before.isFile() || before.size > INTENT_MAX_BYTES)
|
|
566
|
-
throw new
|
|
593
|
+
throw new TaskIntentObservationError("invalid", "TaskIntent sidecar must be a regular file no larger than 64 KiB");
|
|
567
594
|
|
|
568
595
|
const fd = openSync(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
569
596
|
let bytes: Buffer;
|
|
@@ -576,7 +603,7 @@ export function readTaskIntent(
|
|
|
576
603
|
closeSync(fd);
|
|
577
604
|
}
|
|
578
605
|
if (bytes.byteLength > INTENT_MAX_BYTES)
|
|
579
|
-
throw new
|
|
606
|
+
throw new TaskIntentObservationError("invalid", "TaskIntent sidecar exceeds 64 KiB");
|
|
580
607
|
|
|
581
608
|
// Post-read identity re-verification without a second path read as the
|
|
582
609
|
// source of bytes.
|
|
@@ -594,27 +621,15 @@ export function readTaskIntent(
|
|
|
594
621
|
try {
|
|
595
622
|
intent = parseTaskIntentV1(JSON.parse(bytes.toString("utf8")));
|
|
596
623
|
} catch (error) {
|
|
597
|
-
throw new
|
|
624
|
+
throw new TaskIntentObservationError(
|
|
625
|
+
"invalid",
|
|
598
626
|
`TaskIntent sidecar is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
599
627
|
);
|
|
600
628
|
}
|
|
601
629
|
if (intent.task_id !== taskId)
|
|
602
|
-
throw new
|
|
630
|
+
throw new TaskIntentObservationError("invalid", "intent.task_id does not match the sidecar filename task id");
|
|
603
631
|
|
|
604
632
|
const contentHash = canonicalIntentHash(intent);
|
|
605
|
-
const token = mintToken({
|
|
606
|
-
canonical_root: canonicalRoot,
|
|
607
|
-
sidecar_path: sidecarPath,
|
|
608
|
-
path_dev: fileIdentity.dev,
|
|
609
|
-
path_ino: fileIdentity.ino,
|
|
610
|
-
fd_dev: before.dev,
|
|
611
|
-
fd_ino: before.ino,
|
|
612
|
-
fd_size: before.size,
|
|
613
|
-
fd_mtime_ms: before.mtimeMs,
|
|
614
|
-
source_bytes_sha256: sourceBytesSha256,
|
|
615
|
-
intent_content_hash: contentHash,
|
|
616
|
-
});
|
|
617
|
-
|
|
618
633
|
return {
|
|
619
634
|
intent,
|
|
620
635
|
content_hash: contentHash,
|
|
@@ -623,6 +638,35 @@ export function readTaskIntent(
|
|
|
623
638
|
revision: intent.revision,
|
|
624
639
|
content_hash: contentHash,
|
|
625
640
|
},
|
|
626
|
-
|
|
641
|
+
identity: {
|
|
642
|
+
canonical_root: canonicalRoot,
|
|
643
|
+
sidecar_path: sidecarPath,
|
|
644
|
+
path_dev: fileIdentity.dev,
|
|
645
|
+
path_ino: fileIdentity.ino,
|
|
646
|
+
fd_dev: before.dev,
|
|
647
|
+
fd_ino: before.ino,
|
|
648
|
+
fd_size: before.size,
|
|
649
|
+
fd_mtime_ms: before.mtimeMs,
|
|
650
|
+
source_bytes_sha256: sourceBytesSha256,
|
|
651
|
+
intent_content_hash: contentHash,
|
|
652
|
+
},
|
|
627
653
|
};
|
|
628
654
|
}
|
|
655
|
+
|
|
656
|
+
export function observeTaskIntent(
|
|
657
|
+
root: string,
|
|
658
|
+
taskId: string,
|
|
659
|
+
requestedPath?: string,
|
|
660
|
+
): ObservedTaskIntent {
|
|
661
|
+
const { identity: _identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
|
|
662
|
+
return observed;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export function readTaskIntent(
|
|
666
|
+
root: string,
|
|
667
|
+
taskId: string,
|
|
668
|
+
requestedPath?: string,
|
|
669
|
+
): ReadTaskIntentResult {
|
|
670
|
+
const { identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
|
|
671
|
+
return { ...observed, token: mintToken(identity) };
|
|
672
|
+
}
|
|
@@ -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
|
|
@@ -178,6 +178,7 @@ function hasPrivilegedKind(action: TaskAction): boolean {
|
|
|
178
178
|
action.type === "record_approval" ||
|
|
179
179
|
action.type === "approve_breaking_intent_revision" ||
|
|
180
180
|
action.type === "request_rework" ||
|
|
181
|
+
action.type === "authorize_rework" ||
|
|
181
182
|
action.type === "stop" ||
|
|
182
183
|
action.type === "resolve_user_decision"
|
|
183
184
|
);
|
|
@@ -449,13 +450,16 @@ export function reduceTask(
|
|
|
449
450
|
"request_rework requires review, qa, or user authority",
|
|
450
451
|
]);
|
|
451
452
|
const round = reviewRound(record);
|
|
452
|
-
const
|
|
453
|
-
(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
453
|
+
const hasPriorBlockingReviewRework = record.findings.some(
|
|
454
|
+
(finding) =>
|
|
455
|
+
finding.source === "review" &&
|
|
456
|
+
finding.kind === "blocking" &&
|
|
457
|
+
finding.review_round !== null,
|
|
458
|
+
);
|
|
457
459
|
const parkForReplan =
|
|
458
|
-
authorityAudit.authority_kind === "review" &&
|
|
460
|
+
authorityAudit.authority_kind === "review" &&
|
|
461
|
+
hasPriorBlockingReviewRework &&
|
|
462
|
+
action.findings.some((finding) => finding.kind === "blocking");
|
|
459
463
|
if (!parkForReplan) {
|
|
460
464
|
record.artifact_state = "active";
|
|
461
465
|
record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
|
|
@@ -470,8 +474,8 @@ export function reduceTask(
|
|
|
470
474
|
record.findings.push({
|
|
471
475
|
...finding,
|
|
472
476
|
status: "open",
|
|
473
|
-
source: "review",
|
|
474
|
-
review_round: round,
|
|
477
|
+
source: authorityAudit.authority_kind === "review" ? "review" : "execution",
|
|
478
|
+
review_round: authorityAudit.authority_kind === "review" ? round : null,
|
|
475
479
|
});
|
|
476
480
|
}
|
|
477
481
|
if (
|
|
@@ -502,6 +506,28 @@ export function reduceTask(
|
|
|
502
506
|
appendHistory(record, action, from, `review_round_${round}`, authorityAudit);
|
|
503
507
|
break;
|
|
504
508
|
}
|
|
509
|
+
case "authorize_rework": {
|
|
510
|
+
if (record.lifecycle !== "active")
|
|
511
|
+
throw new KernelInvariantError([
|
|
512
|
+
`cannot authorize rework while lifecycle is ${record.lifecycle}`,
|
|
513
|
+
]);
|
|
514
|
+
if (authorityAudit?.authority_kind !== "user")
|
|
515
|
+
throw new KernelInvariantError([
|
|
516
|
+
"authorize_rework requires literal-user authority",
|
|
517
|
+
]);
|
|
518
|
+
const open = record.findings.filter(
|
|
519
|
+
(finding) => finding.kind === "replan_required" && finding.status === "open",
|
|
520
|
+
);
|
|
521
|
+
if (open.length === 0)
|
|
522
|
+
throw new KernelInvariantError([
|
|
523
|
+
"authorize_rework requires an open replan boundary",
|
|
524
|
+
]);
|
|
525
|
+
for (const finding of open) finding.status = "resolved";
|
|
526
|
+
record.artifact_state = "active";
|
|
527
|
+
record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
|
|
528
|
+
appendHistory(record, action, from, open.map((finding) => finding.id).join(","), authorityAudit);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
505
531
|
case "complete": {
|
|
506
532
|
if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
|
|
507
533
|
throw new KernelInvariantError([
|
|
@@ -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;
|
|
@@ -288,6 +297,7 @@ export type TaskAction =
|
|
|
288
297
|
| (TaskActionBase & { type: "request_rework"; findings: TaskFinding[] })
|
|
289
298
|
| (TaskActionBase & { type: "complete" })
|
|
290
299
|
| (TaskActionBase & { type: "stop"; reason: string })
|
|
300
|
+
| (TaskActionBase & { type: "authorize_rework" })
|
|
291
301
|
| (TaskActionBase & { type: "resolve_user_decision"; finding_id: string; resolution: string });
|
|
292
302
|
|
|
293
303
|
|
|
@@ -735,6 +735,7 @@ const ACTION_V2_TYPES = [
|
|
|
735
735
|
"request_rework",
|
|
736
736
|
"complete",
|
|
737
737
|
"stop",
|
|
738
|
+
"authorize_rework",
|
|
738
739
|
"resolve_user_decision",
|
|
739
740
|
] as const;
|
|
740
741
|
|
|
@@ -897,7 +898,8 @@ export function parseTaskAction(raw: unknown): TaskAction {
|
|
|
897
898
|
};
|
|
898
899
|
break;
|
|
899
900
|
}
|
|
900
|
-
case "complete":
|
|
901
|
+
case "complete":
|
|
902
|
+
case "authorize_rework": {
|
|
901
903
|
rejectUnknown(value, [...ACTION_BASE_FIELDS], "action", violations);
|
|
902
904
|
action = { ...base, type: base.type };
|
|
903
905
|
break;
|
|
@@ -1002,6 +1004,7 @@ export function assertTaskRecordUpdateV3(
|
|
|
1002
1004
|
if (
|
|
1003
1005
|
next.intent_ref.path !== previous.intent_ref.path &&
|
|
1004
1006
|
action.type !== "request_rework" &&
|
|
1007
|
+
action.type !== "authorize_rework" &&
|
|
1005
1008
|
action.type !== "stop"
|
|
1006
1009
|
)
|
|
1007
1010
|
violations.push("only artifact transitions may change intent_ref path");
|
|
@@ -1023,11 +1026,12 @@ export function assertTaskRecordUpdateV3(
|
|
|
1023
1026
|
? [action.finding_id]
|
|
1024
1027
|
: action.type === "resolve_user_decision"
|
|
1025
1028
|
? [action.finding_id]
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1029
|
+
: action.type === "authorize_rework" ||
|
|
1030
|
+
action.type === "approve_breaking_intent_revision"
|
|
1031
|
+
? previous.findings
|
|
1032
|
+
.filter((item) => item.kind === "replan_required" && item.status === "open")
|
|
1033
|
+
.map((item) => item.id)
|
|
1034
|
+
: [];
|
|
1031
1035
|
const reworkFindingIds =
|
|
1032
1036
|
action.type === "request_rework"
|
|
1033
1037
|
? new Set(action.findings.map((item) => item.id))
|
|
@@ -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.5" as const;
|