immune-brain 5.2.0 → 5.5.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/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
- package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
- package/plugins/immune-brain/dist/BASELINE.md +17 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16145 -12745
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +63 -19
- package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
- package/plugins/immune-brain/dist/imm-run.md +89 -344
- package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
- package/plugins/immune-brain/hooks/hooks.json +22 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
- package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +10 -26
- package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
- package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
- package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
- package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +30 -20
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
- package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
- package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
- package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
- package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
- package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
- package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
- package/plugins/immune-brain/runtime/plan_core.ts +15 -8
- package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
- package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
- package/plugins/immune-brain/skills/BASELINE.md +17 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -3
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -2
- package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
|
@@ -24,9 +24,14 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
INTENT_MAX_BYTES,
|
|
26
26
|
canonicalIntentHash,
|
|
27
|
+
parseProjectTaskIntent,
|
|
27
28
|
parseTaskIntentV1,
|
|
28
|
-
RISK_FLOOR_SCOPE_PREFIXES,
|
|
29
29
|
} from "../kernel/intent";
|
|
30
|
+
import {
|
|
31
|
+
floorMatchingScopeEntries,
|
|
32
|
+
floorScopePrefixes,
|
|
33
|
+
readRiskFloor,
|
|
34
|
+
} from "../kernel/risk_floor";
|
|
30
35
|
import { inspectSpecBinding } from "../kernel/spec_binding";
|
|
31
36
|
import { projectTask, resolveProjectedRisk } from "../kernel/completion";
|
|
32
37
|
import { deriveAssuranceAuthorization } from "../kernel/assurance_projection";
|
|
@@ -39,7 +44,7 @@ import {
|
|
|
39
44
|
import { inspectRoutingPolicy } from "../managed_task_routing_policy"
|
|
40
45
|
import { projectLegacyAudit } from "../kernel/legacy_audit";
|
|
41
46
|
import { inspectStorageLayout } from "../kernel/storage_paths";
|
|
42
|
-
import {
|
|
47
|
+
import { commitPendingMigrationDiff, migrateAndCommitLegacyLayout } from "../kernel/storage_layout_migration";
|
|
43
48
|
import { readBackendClaim } from "../kernel/backend_claim";
|
|
44
49
|
import { inspectEnrollmentGitBase, type EnrollmentGitBase } from "../assurance/enrollment_git_base";
|
|
45
50
|
import { probeKernelStore } from "../kernel";
|
|
@@ -158,26 +163,6 @@ const RISK_ORDER: Record<TaskRisk, number> = {
|
|
|
158
163
|
critical: 2,
|
|
159
164
|
};
|
|
160
165
|
|
|
161
|
-
function matchingFloorEntries(intent: TaskIntentV1): string[] {
|
|
162
|
-
return intent.scope_hint.filter((entry) => {
|
|
163
|
-
try {
|
|
164
|
-
const trial = parseTaskIntentV1({
|
|
165
|
-
contract: intent.contract,
|
|
166
|
-
task_id: intent.task_id,
|
|
167
|
-
goal: intent.goal,
|
|
168
|
-
acceptance: intent.acceptance,
|
|
169
|
-
scope_hint: [entry],
|
|
170
|
-
risk: "routine",
|
|
171
|
-
revision: intent.revision,
|
|
172
|
-
owner: "user",
|
|
173
|
-
});
|
|
174
|
-
return trial.risk !== "routine";
|
|
175
|
-
} catch {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
166
|
function readWorkspaceOwner(root: string): string | null {
|
|
182
167
|
try {
|
|
183
168
|
return readWorkspaceStateRaw(root).state.current_working;
|
|
@@ -204,10 +189,12 @@ function runInspect(root: string): KernelExecution {
|
|
|
204
189
|
: null,
|
|
205
190
|
workspace: { current_working: workspace },
|
|
206
191
|
};
|
|
192
|
+
const floor = readRiskFloor(root);
|
|
207
193
|
const idleRisk = {
|
|
208
194
|
declared: null as TaskRisk | null,
|
|
209
195
|
resolved: null as TaskRisk | null,
|
|
210
|
-
floor_prefixes:
|
|
196
|
+
floor_prefixes: floorScopePrefixes(floor),
|
|
197
|
+
floor_invalid: floor.invalid,
|
|
211
198
|
floor_applied: false,
|
|
212
199
|
matching_scope_entries: [] as string[],
|
|
213
200
|
};
|
|
@@ -265,7 +252,7 @@ function runInspect(root: string): KernelExecution {
|
|
|
265
252
|
if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
|
|
266
253
|
throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
|
|
267
254
|
declared = raw.risk;
|
|
268
|
-
intent =
|
|
255
|
+
intent = parseProjectTaskIntent(root, raw, record.intent_snapshot);
|
|
269
256
|
if (canonicalIntentHash(intent) !== record.intent_ref.content_hash)
|
|
270
257
|
throw new Error("TaskIntent sidecar does not match TaskRecord content hash");
|
|
271
258
|
} catch (error) {
|
|
@@ -280,11 +267,11 @@ function runInspect(root: string): KernelExecution {
|
|
|
280
267
|
}
|
|
281
268
|
const diffHash = identity.diff_hash;
|
|
282
269
|
const changedPaths = identity.changed_paths;
|
|
283
|
-
const decision = projectTask(intent, record, diffHash, record.intent_ref.content_hash, changedPaths);
|
|
270
|
+
const decision = projectTask(intent, record, diffHash, record.intent_ref.content_hash, changedPaths, floor);
|
|
284
271
|
const openUserDecisionCount = record.findings.filter(
|
|
285
272
|
(finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
|
|
286
273
|
).length;
|
|
287
|
-
const resolved = resolveProjectedRisk(intent, changedPaths);
|
|
274
|
+
const resolved = resolveProjectedRisk(intent, changedPaths, floor);
|
|
288
275
|
return {
|
|
289
276
|
result: jsonResult({
|
|
290
277
|
contract: "assurance_kernel/inspect/v1",
|
|
@@ -303,7 +290,7 @@ function runInspect(root: string): KernelExecution {
|
|
|
303
290
|
diff_hash: diffHash,
|
|
304
291
|
lifecycle: record.lifecycle,
|
|
305
292
|
artifact_state: record.artifact_state,
|
|
306
|
-
risk:
|
|
293
|
+
risk: resolved,
|
|
307
294
|
next_obligation: decision.next_obligation,
|
|
308
295
|
fresh_acceptance_ids: decision.fresh_acceptance_ids,
|
|
309
296
|
missing_acceptance_ids: decision.missing_acceptance_ids,
|
|
@@ -325,9 +312,10 @@ function runInspect(root: string): KernelExecution {
|
|
|
325
312
|
risk: {
|
|
326
313
|
declared,
|
|
327
314
|
resolved,
|
|
328
|
-
floor_prefixes:
|
|
315
|
+
floor_prefixes: floorScopePrefixes(floor),
|
|
316
|
+
floor_invalid: floor.invalid,
|
|
329
317
|
floor_applied: RISK_ORDER[resolved] > RISK_ORDER[declared],
|
|
330
|
-
matching_scope_entries:
|
|
318
|
+
matching_scope_entries: floorMatchingScopeEntries(intent.scope_hint, floor),
|
|
331
319
|
},
|
|
332
320
|
unobservable: UNOBSERVABLE,
|
|
333
321
|
}),
|
|
@@ -474,31 +462,16 @@ function runIntentAuthor(args: string[], root: string): KernelExecution {
|
|
|
474
462
|
};
|
|
475
463
|
}
|
|
476
464
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
result: errorResult(
|
|
487
|
-
"layout_recovery_failed",
|
|
488
|
-
`Kernel transaction recovery failed before authoring: ${error instanceof Error ? error.message : String(error)}`,
|
|
489
|
-
1,
|
|
490
|
-
),
|
|
491
|
-
journal: journalFor(
|
|
492
|
-
"intent",
|
|
493
|
-
null,
|
|
494
|
-
"rejected",
|
|
495
|
-
"source_invalid",
|
|
496
|
-
null,
|
|
497
|
-
"Resolve the pending Kernel transaction marker before retrying.",
|
|
498
|
-
),
|
|
499
|
-
};
|
|
465
|
+
// Pending diffs may be committed; conversion stays on the explicit command.
|
|
466
|
+
let layout = inspectStorageLayout(root);
|
|
467
|
+
if (layout.layout === "migration_uncommitted") {
|
|
468
|
+
try {
|
|
469
|
+
commitPendingMigrationDiff(root);
|
|
470
|
+
layout = inspectStorageLayout(root);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return sourceFailure("intent", error);
|
|
473
|
+
}
|
|
500
474
|
}
|
|
501
|
-
const layout = inspectStorageLayout(root);
|
|
502
475
|
if (layout.layout === "migration_blocked_active") {
|
|
503
476
|
return {
|
|
504
477
|
result: errorResult(
|
|
@@ -532,7 +505,7 @@ function runIntentAuthor(args: string[], root: string): KernelExecution {
|
|
|
532
505
|
"rejected",
|
|
533
506
|
"source_invalid",
|
|
534
507
|
null,
|
|
535
|
-
"Run imm-kernel migrate --storage-layout,
|
|
508
|
+
"Run imm-kernel migrate --storage-layout, then retry intent author.",
|
|
536
509
|
),
|
|
537
510
|
};
|
|
538
511
|
}
|
|
@@ -549,11 +522,17 @@ function runIntentAuthor(args: string[], root: string): KernelExecution {
|
|
|
549
522
|
"rejected",
|
|
550
523
|
"source_invalid",
|
|
551
524
|
null,
|
|
552
|
-
"
|
|
525
|
+
"Resolve the reported layout condition before retrying authoring.",
|
|
553
526
|
),
|
|
554
527
|
};
|
|
555
528
|
}
|
|
556
529
|
|
|
530
|
+
try {
|
|
531
|
+
probeKernelStore(root);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
return sourceFailure("intent", error);
|
|
534
|
+
}
|
|
535
|
+
|
|
557
536
|
// Existing managed ownership rejects before opening the destination.
|
|
558
537
|
const owner = intentActiveOwner(root);
|
|
559
538
|
if (owner.kernel)
|
|
@@ -632,7 +611,7 @@ function runIntentAuthor(args: string[], root: string): KernelExecution {
|
|
|
632
611
|
}
|
|
633
612
|
let intent: ReturnType<typeof parseTaskIntentV1>;
|
|
634
613
|
try {
|
|
635
|
-
intent =
|
|
614
|
+
intent = parseProjectTaskIntent(root, parsed);
|
|
636
615
|
} catch (error) {
|
|
637
616
|
return {
|
|
638
617
|
result: errorResult(
|
|
@@ -1086,7 +1065,7 @@ function runIntentValidate(args: string[], root: string): KernelExecution {
|
|
|
1086
1065
|
// Strict parse + verification eligibility per acceptance item.
|
|
1087
1066
|
let intent: ReturnType<typeof parseTaskIntentV1>;
|
|
1088
1067
|
try {
|
|
1089
|
-
intent =
|
|
1068
|
+
intent = parseProjectTaskIntent(root, parsed);
|
|
1090
1069
|
} catch (error) {
|
|
1091
1070
|
return {
|
|
1092
1071
|
result: jsonResult(
|
|
@@ -1331,7 +1310,7 @@ function executeKernelCommand(args: string[], root: string): KernelExecution {
|
|
|
1331
1310
|
reason: layout.reason,
|
|
1332
1311
|
next_action:
|
|
1333
1312
|
layout.layout === "migration_required"
|
|
1334
|
-
? "run
|
|
1313
|
+
? "run imm-kernel migrate --storage-layout, then retry legacy audit"
|
|
1335
1314
|
: "resolve the reported layout condition before legacy audit",
|
|
1336
1315
|
},
|
|
1337
1316
|
}),
|
|
@@ -1418,58 +1397,17 @@ function runStorageLayoutMigration(root: string): KernelExecution {
|
|
|
1418
1397
|
"Resolve the reported layout condition, then rerun imm-kernel migrate --storage-layout.",
|
|
1419
1398
|
),
|
|
1420
1399
|
};
|
|
1421
|
-
const migration =
|
|
1422
|
-
if (migration.outcome === "migrated")
|
|
1400
|
+
const migration = migrateAndCommitLegacyLayout(root);
|
|
1401
|
+
if (migration.outcome === "migrated" || migration.outcome === "already_migrated")
|
|
1423
1402
|
return {
|
|
1424
1403
|
result: jsonResult({
|
|
1425
1404
|
contract: "assurance_kernel/migration_completed/v1",
|
|
1426
1405
|
operation: "storage layout migration",
|
|
1406
|
+
layout: "ready",
|
|
1427
1407
|
affected_paths: migration.affected_paths,
|
|
1428
|
-
next_action: "commit the affected migration paths, then retry the original command",
|
|
1429
|
-
}),
|
|
1430
|
-
journal: journalFor(
|
|
1431
|
-
"migrate",
|
|
1432
|
-
null,
|
|
1433
|
-
"escalated",
|
|
1434
|
-
"migration_ambiguous",
|
|
1435
|
-
null,
|
|
1436
|
-
"Commit the affected migration paths, then retry the original command.",
|
|
1437
|
-
),
|
|
1438
|
-
};
|
|
1439
|
-
if (migration.outcome === "migration_uncommitted")
|
|
1440
|
-
return {
|
|
1441
|
-
result: jsonResult({
|
|
1442
|
-
contract: "assurance_kernel/migration_pending_commit/v1",
|
|
1443
|
-
operation: "storage layout migration",
|
|
1444
|
-
affected_paths: migration.affected_paths,
|
|
1445
|
-
reason: migration.reason,
|
|
1446
|
-
next_action: "commit the preserved audit evidence, then rerun imm-kernel migrate --storage-layout",
|
|
1447
|
-
}),
|
|
1448
|
-
journal: journalFor(
|
|
1449
|
-
"migrate",
|
|
1450
|
-
null,
|
|
1451
|
-
"escalated",
|
|
1452
|
-
"migration_ambiguous",
|
|
1453
|
-
null,
|
|
1454
|
-
"Commit the preserved audit evidence, then rerun imm-kernel migrate --storage-layout.",
|
|
1455
|
-
),
|
|
1456
|
-
};
|
|
1457
|
-
if (migration.outcome === "already_migrated")
|
|
1458
|
-
return {
|
|
1459
|
-
result: jsonResult({
|
|
1460
|
-
contract: "assurance_kernel/migration_completed/v1",
|
|
1461
|
-
operation: "storage layout migration",
|
|
1462
|
-
affected_paths: [],
|
|
1463
1408
|
next_action: "retry the original command",
|
|
1464
1409
|
}),
|
|
1465
|
-
journal: journalFor(
|
|
1466
|
-
"migrate",
|
|
1467
|
-
null,
|
|
1468
|
-
"ok",
|
|
1469
|
-
"command_ok",
|
|
1470
|
-
null,
|
|
1471
|
-
"Retry the original command.",
|
|
1472
|
-
),
|
|
1410
|
+
journal: journalFor("migrate", null, "ok", "command_ok", null, "Retry the original command."),
|
|
1473
1411
|
};
|
|
1474
1412
|
return {
|
|
1475
1413
|
result: errorResult(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { basename, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { observeLocalInitiative } from "./local_initiative";
|
|
4
5
|
import { readTaskIntent } from "./kernel/intent";
|
|
5
6
|
|
|
6
7
|
const CONTRACT = "immune_brain/github_issue_tracker_result/v1" as const;
|
|
@@ -877,6 +878,49 @@ async function readBlockedByIds(
|
|
|
877
878
|
}
|
|
878
879
|
}
|
|
879
880
|
|
|
881
|
+
export async function assertIsolatedTaskPrerequisites(root: string, taskId: string,
|
|
882
|
+
transport?: GhTransport): Promise<void> {
|
|
883
|
+
const directory = resolve(root, "docs/initiatives");
|
|
884
|
+
const local = existsSync(directory) ? readdirSync(directory, { withFileTypes: true })
|
|
885
|
+
.filter(entry => entry.isFile() && /^[A-Za-z0-9][A-Za-z0-9._-]*\.md$/.test(entry.name))
|
|
886
|
+
// Only a file that names this Task can own it; an unrelated or malformed Initiative file must
|
|
887
|
+
// not block Enrollment, while a malformed file that does name it still fails closed below.
|
|
888
|
+
.filter(entry => readFileSync(resolve(directory, entry.name), "utf8").includes(taskId))
|
|
889
|
+
.map(entry => observeLocalInitiative(root, entry.name.slice(0, -3)))
|
|
890
|
+
.filter(parent => parent.tasks.some(task => task.task_id === taskId)) : [];
|
|
891
|
+
if (local.length > 1) throw new Error("Slice has ambiguous Local Initiative ownership");
|
|
892
|
+
if (local.length === 1) {
|
|
893
|
+
const parent = local[0]!;
|
|
894
|
+
const child = parent.tasks.find(task => task.task_id === taskId)!;
|
|
895
|
+
if (child.blocked_by.length) {
|
|
896
|
+
const { observeParentCompletion } = await import("./unattended/batch_git");
|
|
897
|
+
const proof = await observeParentCompletion(root, parent.initiative_id, child.blocked_by);
|
|
898
|
+
if (proof.missing_task_ids.length) throw new Error(`Isolated Slice prerequisites are not integrated: ${proof.missing_task_ids.join(", ")}; execute only this Slice after its prerequisites are delivered`);
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (!transport) {
|
|
903
|
+
const remotes = spawnSync("git", ["-C", root, "remote"], { encoding: "utf8" });
|
|
904
|
+
if (remotes.status !== 0) throw new Error("cannot verify repository carrier before Slice Enrollment");
|
|
905
|
+
if (!remotes.stdout.trim()) return;
|
|
906
|
+
}
|
|
907
|
+
const gh = transport ?? createGhTransport();
|
|
908
|
+
const source = await snapshot(resolve(root), gh, "create-initiative");
|
|
909
|
+
if ("contract" in source) throw new Error(source.message);
|
|
910
|
+
const task = taskLookup(source.issues, source.repository.id, taskId);
|
|
911
|
+
if (task.kind === "missing") return;
|
|
912
|
+
if (task.kind === "ambiguous") throw new Error(task.message);
|
|
913
|
+
const initiative = ownershipMarkerValue(task.issue.body, "initiative-id");
|
|
914
|
+
if (!initiative) throw new Error("Task Issue has no unique Initiative identity");
|
|
915
|
+
const observation = await observeGithubInitiative(root, initiative, gh);
|
|
916
|
+
const child = observation.tasks.find(candidate => candidate.task_id === taskId);
|
|
917
|
+
if (!child) throw new Error("Task Issue is absent from its Parent topology");
|
|
918
|
+
if (!child.blocked_by.length) return;
|
|
919
|
+
const { observeParentCompletion } = await import("./unattended/batch_git");
|
|
920
|
+
const proof = await observeParentCompletion(root, initiative, child.blocked_by);
|
|
921
|
+
if (proof.missing_task_ids.length) throw new Error(`Isolated Slice prerequisites are not integrated: ${proof.missing_task_ids.join(", ")}; execute only this Slice after its prerequisites are delivered`);
|
|
922
|
+
}
|
|
923
|
+
|
|
880
924
|
export async function observeGithubInitiative(
|
|
881
925
|
root: string,
|
|
882
926
|
initiativeId: string,
|
|
@@ -1669,6 +1713,20 @@ async function closeParentWhenComplete(
|
|
|
1669
1713
|
if (sliceChildren.length === 0
|
|
1670
1714
|
|| !sliceChildren.every((candidate) => candidate.state === "closed" && candidate.state_reason === "completed"))
|
|
1671
1715
|
return childResult;
|
|
1716
|
+
const sliceIds = [...parent.body.matchAll(/<!-- immune-brain:slice-id=([^>]+) -->/g)].map(match => match[1]!);
|
|
1717
|
+
if (sliceIds.length !== sliceChildren.length || new Set(sliceIds).size !== sliceIds.length ||
|
|
1718
|
+
sliceChildren.some(candidate => !sliceIds.includes(ownershipMarkerValue(candidate.body, "slice-id") ?? "")))
|
|
1719
|
+
return retry("required Slice topology does not match the Parent", "ambiguous_remote_state");
|
|
1720
|
+
const taskIds = sliceChildren.map(candidate => ownershipMarkerValue(candidate.body, "task-id"));
|
|
1721
|
+
if (taskIds.some(id => !id)) return retry("required Slice has no Task identity", "ambiguous_remote_state");
|
|
1722
|
+
try {
|
|
1723
|
+
const { observeParentCompletion } = await import("./unattended/batch_git");
|
|
1724
|
+
const completion = await observeParentCompletion(root, initiativeId, taskIds as string[]);
|
|
1725
|
+
if (!completion.complete) return result(operation.op, childResult.status,
|
|
1726
|
+
`${childResult.message}; Parent #${parentNumber} remains open: ${completion.reason}`, child.issue);
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
return retry(`local completion proof could not be verified: ${error instanceof Error ? error.message : String(error)}`);
|
|
1729
|
+
}
|
|
1672
1730
|
const close = await gh.run(["issue", "close", String(parentNumber), "--repo", source.repository.name_with_owner, "--reason", "completed"], { cwd: root });
|
|
1673
1731
|
const confirmed = await readParent();
|
|
1674
1732
|
if ("contract" in confirmed) return confirmed;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { asTaskDiffSnapshot } from "./completion";
|
|
12
12
|
import { taskDeliveryIdentity } from "../workspace_scope";
|
|
13
13
|
import { canonicalIntentHash, readTaskIntent } from "./intent";
|
|
14
|
+
import { readRiskFloor } from "./risk_floor";
|
|
14
15
|
import {
|
|
15
16
|
inspectIntentTokenPair,
|
|
16
17
|
consumeIntentToken,
|
|
@@ -98,7 +99,7 @@ export function applyTaskAction(
|
|
|
98
99
|
]);
|
|
99
100
|
|
|
100
101
|
// Fresh secure reread of the sidecar inside the same lock.
|
|
101
|
-
const freshRead = readTaskIntent(root, task_id, current.record.intent_ref.path);
|
|
102
|
+
const freshRead = readTaskIntent(root, task_id, current.record.intent_ref.path, current.record.intent_snapshot);
|
|
102
103
|
const { prior: priorIdentity, current: currentIdentity } =
|
|
103
104
|
inspectIntentTokenPair(prior_intent_token, freshRead.token);
|
|
104
105
|
if (
|
|
@@ -161,6 +162,7 @@ export function applyTaskAction(
|
|
|
161
162
|
action.type === "approve_breaking_intent_revision" ||
|
|
162
163
|
action.type === "request_rework" ||
|
|
163
164
|
action.type === "authorize_rework" ||
|
|
165
|
+
action.type === "accept_residual_risk" ||
|
|
164
166
|
action.type === "stop" ||
|
|
165
167
|
action.type === "resolve_user_decision";
|
|
166
168
|
|
|
@@ -201,6 +203,7 @@ export function applyTaskAction(
|
|
|
201
203
|
action,
|
|
202
204
|
inspectedAudit ? (inspectedAudit.audit as AuthorityAuditDescriptor) : null,
|
|
203
205
|
trustedDiff.changed_paths,
|
|
206
|
+
readRiskFloor(root),
|
|
204
207
|
);
|
|
205
208
|
if (!isReducedMutation(mutation))
|
|
206
209
|
throw new KernelInvariantError(["reducer returned an invalid mutation"]);
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
reconcileKernelAuthority,
|
|
22
22
|
} from "./storage";
|
|
23
23
|
import { projectTask, resolveProjectedRisk } from "./completion";
|
|
24
|
+
import { readRiskFloor, type RiskFloor } from "./risk_floor";
|
|
24
25
|
import { inspectSpecBinding } from "./spec_binding";
|
|
26
|
+
import { MAX_REWORK_GRANT_ROUNDS } from "./validation";
|
|
25
27
|
import { taskDeliveryIdentity } from "../workspace_scope";
|
|
26
28
|
import type { AssuranceObligation, TaskIntentV1, TaskRecord, TaskRecordV3 } from "./types";
|
|
27
29
|
import type { TaskRecordV2 } from "./legacy_task_record";
|
|
@@ -41,6 +43,90 @@ export interface AssuranceAuthorizationReadiness {
|
|
|
41
43
|
state: "resolve_user_decision" | "authorize_rework" | "none";
|
|
42
44
|
/** Non-null only when Kernel facts prove the authorization is blocked. */
|
|
43
45
|
blocked: string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Present only with state "authorize_rework": the exits the literal user may
|
|
48
|
+
* choose at the open replan boundary. Both Hosts render exactly these and map
|
|
49
|
+
* each to one Kernel action, so the offer cannot drift between them.
|
|
50
|
+
*/
|
|
51
|
+
exits?: ReworkExitsReadiness;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ReworkExitsReadiness {
|
|
55
|
+
/** `authorize_rework` with `rounds` in this inclusive range. */
|
|
56
|
+
grant_rounds: { min: number; max: number };
|
|
57
|
+
/** `accept_residual_risk`; offered only when the boundary is a budget exhaustion. */
|
|
58
|
+
accept_residual_risk: boolean;
|
|
59
|
+
/** `approve_breaking_intent_revision`; always available. */
|
|
60
|
+
replan: true;
|
|
61
|
+
/** The open blocking Review findings the user would be accepting. */
|
|
62
|
+
findings: Array<{ id: string; summary: string }>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The one exit the literal user picked at the replan boundary. */
|
|
66
|
+
export type ReworkChoice =
|
|
67
|
+
| { exit: "grant_rounds"; rounds: number }
|
|
68
|
+
| { exit: "accept_residual_risk"; reason: string }
|
|
69
|
+
| { exit: "replan" };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse a Host's form content into a choice. Both Hosts call this, so what is
|
|
73
|
+
* accepted cannot drift between them. Unknown keys, an exit the boundary does not
|
|
74
|
+
* offer, an out-of-range grant and a blank reason all yield null.
|
|
75
|
+
*/
|
|
76
|
+
export function parseReworkChoice(exits: ReworkExitsReadiness, content: unknown): ReworkChoice | null {
|
|
77
|
+
if (typeof content !== "object" || content === null || Array.isArray(content)) return null;
|
|
78
|
+
const fields = content as Record<string, unknown>;
|
|
79
|
+
const allowed = new Set(["exit", "rounds", "reason"]);
|
|
80
|
+
if (Object.keys(fields).some((key) => !allowed.has(key))) return null;
|
|
81
|
+
if (fields.exit === "replan") return { exit: "replan" };
|
|
82
|
+
if (fields.exit === "grant_rounds") {
|
|
83
|
+
const rounds = fields.rounds ?? exits.grant_rounds.min;
|
|
84
|
+
if (!Number.isInteger(rounds)) return null;
|
|
85
|
+
const value = rounds as number;
|
|
86
|
+
if (value < exits.grant_rounds.min || value > exits.grant_rounds.max) return null;
|
|
87
|
+
return { exit: "grant_rounds", rounds: value };
|
|
88
|
+
}
|
|
89
|
+
if (fields.exit === "accept_residual_risk") {
|
|
90
|
+
if (!exits.accept_residual_risk) return null;
|
|
91
|
+
const reason = typeof fields.reason === "string" ? fields.reason.trim() : "";
|
|
92
|
+
return reason ? { exit: "accept_residual_risk", reason } : null;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The exits as the lines both Hosts show, so the offer reads the same everywhere. */
|
|
98
|
+
export function reworkExitLines(exits: ReworkExitsReadiness): string[] {
|
|
99
|
+
return [
|
|
100
|
+
`Exit grant_rounds: grant ${exits.grant_rounds.min}-${exits.grant_rounds.max} more Review round(s)`,
|
|
101
|
+
...(exits.accept_residual_risk
|
|
102
|
+
? ["Exit accept_residual_risk: accept the open Review findings below as residual risk (a reason is required)"]
|
|
103
|
+
: []),
|
|
104
|
+
"Exit replan: revise the Intent through approve_breaking_intent_revision",
|
|
105
|
+
...(exits.findings.length > 0
|
|
106
|
+
? [`Open blocking Review findings (${exits.findings.length}):`, ...exits.findings.map((finding) => ` - ${finding.id}: ${finding.summary}`)]
|
|
107
|
+
: []),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function deriveReworkExits(record: Pick<TaskRecord, "findings">): ReworkExitsReadiness {
|
|
112
|
+
const boundaries = record.findings.filter(
|
|
113
|
+
(finding) => finding.kind === "replan_required" && finding.status === "open",
|
|
114
|
+
);
|
|
115
|
+
const findings = record.findings
|
|
116
|
+
.filter(
|
|
117
|
+
(finding) =>
|
|
118
|
+
finding.kind === "blocking" && finding.source === "review" && finding.status === "open",
|
|
119
|
+
)
|
|
120
|
+
.map((finding) => ({ id: finding.id, summary: finding.summary }));
|
|
121
|
+
return {
|
|
122
|
+
grant_rounds: { min: 1, max: MAX_REWORK_GRANT_ROUNDS },
|
|
123
|
+
accept_residual_risk:
|
|
124
|
+
boundaries.length > 0 &&
|
|
125
|
+
boundaries.every((finding) => finding.replan_cause === "budget_exhausted") &&
|
|
126
|
+
findings.length > 0,
|
|
127
|
+
replan: true,
|
|
128
|
+
findings,
|
|
129
|
+
};
|
|
44
130
|
}
|
|
45
131
|
|
|
46
132
|
export interface AssuranceProjection {
|
|
@@ -84,6 +170,7 @@ export interface AssuranceProjectionResult {
|
|
|
84
170
|
export function deriveAssuranceAuthorization(input: {
|
|
85
171
|
next_obligation: AssuranceObligation;
|
|
86
172
|
open_user_decision_count: number;
|
|
173
|
+
exits?: ReworkExitsReadiness;
|
|
87
174
|
}): AssuranceAuthorizationReadiness {
|
|
88
175
|
if (input.open_user_decision_count === 1)
|
|
89
176
|
return { state: "resolve_user_decision", blocked: null };
|
|
@@ -93,7 +180,11 @@ export function deriveAssuranceAuthorization(input: {
|
|
|
93
180
|
blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`,
|
|
94
181
|
};
|
|
95
182
|
if (input.next_obligation === "revise_intent")
|
|
96
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
state: "authorize_rework",
|
|
185
|
+
blocked: null,
|
|
186
|
+
...(input.exits ? { exits: input.exits } : {}),
|
|
187
|
+
};
|
|
97
188
|
return { state: "none", blocked: null };
|
|
98
189
|
}
|
|
99
190
|
|
|
@@ -145,8 +236,10 @@ function projectHistoricalTerminal(
|
|
|
145
236
|
};
|
|
146
237
|
}
|
|
147
238
|
|
|
148
|
-
|
|
149
|
-
|
|
239
|
+
// Terminal proof validates historical settlement, not the caller's live checkout.
|
|
240
|
+
// Current integrated delivery remains independently checked by the batch boundary.
|
|
241
|
+
function projectSettledTerminal(
|
|
242
|
+
record: TaskRecordV3 | TaskRecord,
|
|
150
243
|
recordRevision: string,
|
|
151
244
|
workspaceRevision: string,
|
|
152
245
|
): AssuranceProjection {
|
|
@@ -201,6 +294,7 @@ function projectFromRecord(
|
|
|
201
294
|
recordRevision: string,
|
|
202
295
|
workspaceRevision: string,
|
|
203
296
|
snapshot: TaskDiffSnapshot,
|
|
297
|
+
floor: RiskFloor,
|
|
204
298
|
): AssuranceProjection {
|
|
205
299
|
const intent: TaskIntentV1 = record.intent_snapshot;
|
|
206
300
|
const decision = projectTask(
|
|
@@ -209,6 +303,7 @@ function projectFromRecord(
|
|
|
209
303
|
snapshot.diff_hash,
|
|
210
304
|
record.intent_ref.content_hash,
|
|
211
305
|
snapshot.changed_paths,
|
|
306
|
+
floor,
|
|
212
307
|
);
|
|
213
308
|
const approvalKinds = freshApprovalKinds(record, record.intent_ref.content_hash, snapshot.diff_hash);
|
|
214
309
|
const openUserDecisionCount = record.findings.filter(
|
|
@@ -224,7 +319,7 @@ function projectFromRecord(
|
|
|
224
319
|
diff_hash: snapshot.diff_hash,
|
|
225
320
|
lifecycle: record.lifecycle,
|
|
226
321
|
artifact_state: record.artifact_state,
|
|
227
|
-
risk: resolveProjectedRisk(intent, snapshot.changed_paths),
|
|
322
|
+
risk: resolveProjectedRisk(intent, snapshot.changed_paths, floor),
|
|
228
323
|
next_obligation: decision.next_obligation,
|
|
229
324
|
fresh_acceptance_ids: decision.fresh_acceptance_ids,
|
|
230
325
|
missing_acceptance_ids: decision.missing_acceptance_ids,
|
|
@@ -242,6 +337,7 @@ function projectFromRecord(
|
|
|
242
337
|
: deriveAssuranceAuthorization({
|
|
243
338
|
next_obligation: decision.next_obligation,
|
|
244
339
|
open_user_decision_count: openUserDecisionCount,
|
|
340
|
+
exits: deriveReworkExits(record),
|
|
245
341
|
}),
|
|
246
342
|
};
|
|
247
343
|
}
|
|
@@ -329,12 +425,7 @@ export async function projectAssurance(
|
|
|
329
425
|
error: null,
|
|
330
426
|
claim: null,
|
|
331
427
|
projection: {
|
|
332
|
-
...
|
|
333
|
-
committed.record,
|
|
334
|
-
committed.revision,
|
|
335
|
-
workspace.revision,
|
|
336
|
-
diffProvider(root, committed.record),
|
|
337
|
-
),
|
|
428
|
+
...projectSettledTerminal(committed.record, committed.revision, workspace.revision),
|
|
338
429
|
run_id: committedRunId,
|
|
339
430
|
},
|
|
340
431
|
};
|
|
@@ -353,20 +444,12 @@ export async function projectAssurance(
|
|
|
353
444
|
claim: null,
|
|
354
445
|
projection: projectHistoricalTerminal(auditPair.record, auditPair.recordRevision, workspace.revision),
|
|
355
446
|
};
|
|
356
|
-
if (auditPair.record.contract === "assurance_kernel/task_record/v3")
|
|
357
|
-
return {
|
|
358
|
-
contract: "assurance_kernel/assurance_projection/v1",
|
|
359
|
-
task_id: taskId,
|
|
360
|
-
error: null,
|
|
361
|
-
claim: null,
|
|
362
|
-
projection: projectHistoricalTerminalV3(auditPair.record, auditPair.recordRevision, workspace.revision),
|
|
363
|
-
};
|
|
364
447
|
return {
|
|
365
448
|
contract: "assurance_kernel/assurance_projection/v1",
|
|
366
449
|
task_id: taskId,
|
|
367
450
|
error: null,
|
|
368
451
|
claim: null,
|
|
369
|
-
projection:
|
|
452
|
+
projection: projectSettledTerminal(auditPair.record, auditPair.recordRevision, workspace.revision),
|
|
370
453
|
};
|
|
371
454
|
}
|
|
372
455
|
if (read.record.task_id !== taskId)
|
|
@@ -381,7 +464,7 @@ export async function projectAssurance(
|
|
|
381
464
|
error: null,
|
|
382
465
|
claim,
|
|
383
466
|
projection: {
|
|
384
|
-
...projectFromRecord(read.record, read.revision, workspace.revision, snapshot),
|
|
467
|
+
...projectFromRecord(read.record, read.revision, workspace.revision, snapshot, readRiskFloor(root)),
|
|
385
468
|
run_id: reconcileKernelAuthority(root, taskId).owner_run_id,
|
|
386
469
|
},
|
|
387
470
|
};
|