immune-brain 4.5.0 → 5.0.0
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 +157 -39
- package/README.zh-CN.md +160 -40
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +42 -45
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +189 -476
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +77 -102
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +7 -2
- package/plugins/immune-brain/.pi-extension/tsconfig.json +2 -2
- package/plugins/immune-brain/agents/immune-brain-advisory-reviewer.md +23 -0
- package/plugins/immune-brain/agents/immune-brain-qa.md +42 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +90 -1
- package/plugins/immune-brain/agents/immune-brain-ui-review.md +21 -0
- package/plugins/immune-brain/dist/BASELINE.md +4 -4
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +9924 -9654
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
- package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
- package/plugins/immune-brain/dist/imm-planner.md +60 -20
- package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
- package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
- package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +44 -11
- package/plugins/immune-brain/dist/registry.yaml +9 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +138 -69
- package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +135 -396
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +5 -4
- package/plugins/immune-brain/runtime/claude/review_host.ts +63 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +83 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
- package/plugins/immune-brain/runtime/kernel/application.ts +7 -7
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -5
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +170 -17
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
- package/plugins/immune-brain/runtime/loop_contract.ts +22 -22
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +27 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +66 -28
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +132 -45
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +40 -36
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +98 -209
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
- package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +78 -8
- package/plugins/immune-brain/skills/BASELINE.md +4 -4
- package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
- package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
- package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
- package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
- package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
- package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +7 -7
- package/plugins/immune-brain/skills/registry.yaml +9 -9
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -584
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
|
@@ -1,30 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stagePlanningArtifactTransition, type VerdictAuthority } from "../assurance/verdict_authority";
|
|
2
|
+
export { ensureReviewRevision as ensureClaudeReviewRevision } from "../assurance/verdict_authority";
|
|
2
3
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { execFileSync } from "node:child_process";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import {
|
|
6
7
|
AssuranceCoordinator,
|
|
7
|
-
reviewAdvisoryRecords,
|
|
8
|
-
reviewReworkFindings,
|
|
9
8
|
type AssuranceCoordinatorPorts,
|
|
10
9
|
type AssuranceSubmitReviewResult,
|
|
11
|
-
type AssuranceVerdict,
|
|
12
10
|
type HostContext,
|
|
13
|
-
type SnapshotDescriptor,
|
|
14
11
|
} from "../assurance/coordinator";
|
|
15
12
|
import {
|
|
16
|
-
type VerificationDescriptor,
|
|
17
13
|
} from "../assurance/verification";
|
|
18
14
|
import {
|
|
19
|
-
captureReviewManifest,
|
|
20
15
|
writeNativeReviewEvidence,
|
|
21
|
-
type ReviewRevision,
|
|
22
16
|
} from "../assurance/review_evidence";
|
|
23
|
-
import { parseVerificationDescriptor } from "../verification_descriptor";
|
|
24
17
|
import { projectAssurance, type AssuranceProjection, type AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
25
|
-
import { type
|
|
18
|
+
import { type TaskFinding, type TaskRecord } from "../kernel/types";
|
|
26
19
|
import { findingsDigestV2 } from "../kernel/reducer";
|
|
27
|
-
import {
|
|
20
|
+
import { readTaskRecordRaw, recoverKernelStoreFollowUps } from "../kernel/storage";
|
|
28
21
|
import { canonicalIntentHash, parseTaskIntentV1, readTaskIntent } from "../kernel/intent";
|
|
29
22
|
import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
|
|
30
23
|
import {
|
|
@@ -37,14 +30,13 @@ import {
|
|
|
37
30
|
createEnrollmentAuthorityRegistry,
|
|
38
31
|
type EnrollmentCapabilityBinding,
|
|
39
32
|
} from "../kernel/enrollment_authority";
|
|
40
|
-
import {
|
|
33
|
+
import { enrollTask } from "../kernel/enrollment";
|
|
41
34
|
import { reconcileKernelAuthority, repairKernelAuthority } from "../kernel/storage";
|
|
42
35
|
import { inspectEnrollmentGitBase, enrollmentGitBaseNotice, initializeEnrollmentGitBase } from "../assurance/enrollment_git_base";
|
|
43
36
|
import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare";
|
|
44
37
|
import { runDeterministicQa } from "../assurance/qa";
|
|
45
|
-
import {
|
|
38
|
+
import { taskDeliveryIdentity } from "../workspace_scope";
|
|
46
39
|
import { batchReason } from "../unattended/batch_reasons";
|
|
47
|
-
import { startConfirmationDeadline } from "../unattended/confirmation_deadline";
|
|
48
40
|
import { deriveAuthorizationOperation } from "../authorization_operation";
|
|
49
41
|
import { LITERAL_USER_ACTOR_ID, canonicalActorId } from "../kernel/actor_identity";
|
|
50
42
|
import {
|
|
@@ -57,20 +49,16 @@ import { readTaskTombstone } from "../kernel/backend_claim";
|
|
|
57
49
|
import {
|
|
58
50
|
authorizeBatch,
|
|
59
51
|
projectBatchPreflight,
|
|
60
|
-
|
|
61
|
-
isOwnBatchClaim,
|
|
52
|
+
findResumableBatchSlugForTask,
|
|
62
53
|
} from "../unattended/batch_preflight";
|
|
63
54
|
import {
|
|
64
55
|
startBatch,
|
|
65
|
-
batchQaFailureFacts,
|
|
66
56
|
type BatchRunnerKernelPort,
|
|
67
57
|
type BatchRunReport,
|
|
68
58
|
} from "../unattended/batch_runner";
|
|
59
|
+
import { createBatchKernelPort } from "../unattended/batch_kernel_port";
|
|
69
60
|
import {
|
|
70
61
|
createBatchAuthorityRegistry,
|
|
71
|
-
deriveChildEnrollment,
|
|
72
|
-
type BatchAuthorityRegistry,
|
|
73
|
-
type BatchAuthorizationBinding,
|
|
74
62
|
} from "../kernel/batch_authority";
|
|
75
63
|
import {
|
|
76
64
|
type BatchRunnerGitPort,
|
|
@@ -96,11 +84,7 @@ export function diffSnapshotOf(root: string, record: TaskRecord): {
|
|
|
96
84
|
diff_hash: string;
|
|
97
85
|
changed_paths: readonly string[];
|
|
98
86
|
} {
|
|
99
|
-
|
|
100
|
-
if (!record.git_base_head) throw new Error("TaskRecord v4 is missing git_base_head");
|
|
101
|
-
return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head, record.task_id);
|
|
102
|
-
}
|
|
103
|
-
return taskDiffIdentity(root, record.intent_snapshot.scope_hint, record.task_id);
|
|
87
|
+
return taskDeliveryIdentity(root, record);
|
|
104
88
|
}
|
|
105
89
|
|
|
106
90
|
export function diffHashOf(root: string, record: TaskRecord): string {
|
|
@@ -144,6 +128,21 @@ function verdictFingerprint(raw: Record<string, unknown>): string {
|
|
|
144
128
|
});
|
|
145
129
|
}
|
|
146
130
|
|
|
131
|
+
const RELEASED_REVIEW_RECOVERY =
|
|
132
|
+
"Call advance_assurance to obtain a new Review reservation, then dispatch one fresh reviewer with the returned envelope unchanged";
|
|
133
|
+
const RETAINED_REVIEW_RECOVERY =
|
|
134
|
+
"Wait for the dispatched reviewer to finish, then call submit_review again with its verdict; do not dispatch or continue another reviewer";
|
|
135
|
+
const MISMATCH_REVIEW_RECOVERY =
|
|
136
|
+
"Resubmit the reviewer's verdict exactly as the reviewer returned it";
|
|
137
|
+
|
|
138
|
+
function withReviewRecovery(
|
|
139
|
+
result: AssuranceSubmitReviewResult,
|
|
140
|
+
recovery_action: string,
|
|
141
|
+
): AssuranceSubmitReviewResult {
|
|
142
|
+
if (result.state !== "blocked" || result.code === "verdict_invalid") return result;
|
|
143
|
+
return { ...result, recovery_action };
|
|
144
|
+
}
|
|
145
|
+
|
|
147
146
|
export async function submitClaudeReview(
|
|
148
147
|
host: ClaudeReviewHost,
|
|
149
148
|
coordinator: AssuranceCoordinator,
|
|
@@ -154,19 +153,19 @@ export async function submitClaudeReview(
|
|
|
154
153
|
if (verdictInput === undefined) throw new Error("verdict is required");
|
|
155
154
|
const observed = host.inspectReviewForTask(taskId);
|
|
156
155
|
if (!observed.ok) {
|
|
157
|
-
if (observed.release) return coordinator.abandonReview(taskId, observed.reason);
|
|
158
|
-
return { state: "blocked", reason: observed.reason };
|
|
156
|
+
if (observed.release) return withReviewRecovery(coordinator.abandonReview(taskId, observed.reason), RELEASED_REVIEW_RECOVERY);
|
|
157
|
+
return { state: "blocked", reason: observed.reason, recovery_action: RETAINED_REVIEW_RECOVERY };
|
|
159
158
|
}
|
|
160
159
|
const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
|
|
161
160
|
if (!parentValid) return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
162
161
|
const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
|
|
163
162
|
if (!receiptValid) {
|
|
164
|
-
return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
|
|
163
|
+
return withReviewRecovery(coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict"), RELEASED_REVIEW_RECOVERY);
|
|
165
164
|
}
|
|
166
165
|
const parentJson = extractVerdictJson(verdictInput)!;
|
|
167
166
|
const receiptJson = extractVerdictJson(observed.receipt.result)!;
|
|
168
167
|
if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
|
|
169
|
-
return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
|
|
168
|
+
return { state: "blocked", reason: "parent verdict does not match reviewer receipt", recovery_action: MISMATCH_REVIEW_RECOVERY };
|
|
170
169
|
}
|
|
171
170
|
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
172
171
|
}
|
|
@@ -200,150 +199,6 @@ function assertProjectionBinding(before: AssuranceProjectionResult, after: Assur
|
|
|
200
199
|
}
|
|
201
200
|
}
|
|
202
201
|
|
|
203
|
-
function qaOutcomes(record: { attestations: Array<{ kind: string; acceptance_results: Array<{ acceptance_id: string; status: "passed" | "failed" | "blocked"; summary: string }> }> }) {
|
|
204
|
-
return Object.fromEntries(
|
|
205
|
-
record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results)
|
|
206
|
-
.map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Publish the task-scoped synthetic revision for a v4 record and return the
|
|
212
|
-
* exact identity the Review snapshot binds.
|
|
213
|
-
*
|
|
214
|
-
* `submitReview` re-derives this identity and compares all four fields —
|
|
215
|
-
* `manifest_digest` included — against the reservation. Returning the bare
|
|
216
|
-
* commit identity therefore compared a real digest against `undefined` and
|
|
217
|
-
* failed every v4 submission with "Review revision changed before submission",
|
|
218
|
-
* so the manifest is recomputed here rather than only the commit. The outcomes
|
|
219
|
-
* come from the same `qaOutcomes` the Review snapshot is built from, which
|
|
220
|
-
* makes the two digests equal by construction instead of by coincidence.
|
|
221
|
-
*
|
|
222
|
-
* v3 records keep the legacy full-source bundle and return null.
|
|
223
|
-
*/
|
|
224
|
-
export async function ensureClaudeReviewRevision(
|
|
225
|
-
root: string,
|
|
226
|
-
taskId: string,
|
|
227
|
-
projection: AssuranceProjectionResult,
|
|
228
|
-
): Promise<ReviewRevision | null> {
|
|
229
|
-
const current = await readTaskRecord(root, taskId);
|
|
230
|
-
const record = current.record;
|
|
231
|
-
if (!record) throw new Error(`task ${taskId} has no TaskRecord`);
|
|
232
|
-
if (current.revision !== projection.projection.record_revision)
|
|
233
|
-
throw new Error("TaskRecord changed before Review revision preparation");
|
|
234
|
-
if (record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
235
|
-
if (!record.git_base_head)
|
|
236
|
-
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
237
|
-
const manifest = captureReviewManifest(root, {
|
|
238
|
-
taskId,
|
|
239
|
-
baseHead: record.git_base_head,
|
|
240
|
-
scopeHint: record.intent_snapshot.scope_hint,
|
|
241
|
-
expectedDiffHash: projection.projection.diff_hash,
|
|
242
|
-
intentRevision: projection.projection.intent_revision,
|
|
243
|
-
intentContentHash: projection.projection.intent_content_hash,
|
|
244
|
-
recordRevision: projection.projection.record_revision,
|
|
245
|
-
workspaceRevision: projection.projection.workspace_revision,
|
|
246
|
-
lifecycle: projection.projection.lifecycle,
|
|
247
|
-
artifactState: projection.projection.artifact_state,
|
|
248
|
-
risk: record.intent_snapshot.risk,
|
|
249
|
-
outcomes: qaOutcomes(record),
|
|
250
|
-
});
|
|
251
|
-
return {
|
|
252
|
-
contract: "assurance_kernel/review_revision/v1",
|
|
253
|
-
base_head: manifest.base_head,
|
|
254
|
-
review_tree: manifest.review_tree,
|
|
255
|
-
review_commit: manifest.review_commit,
|
|
256
|
-
review_ref: manifest.review_ref,
|
|
257
|
-
diff_hash: manifest.diff_hash,
|
|
258
|
-
manifest_digest: manifest.manifest_digest,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function buildAssuranceSnapshot(
|
|
263
|
-
root: string,
|
|
264
|
-
taskId: string,
|
|
265
|
-
role: "qa" | "review",
|
|
266
|
-
projection: AssuranceProjectionResult,
|
|
267
|
-
) {
|
|
268
|
-
const read = await readTaskRecord(root, taskId);
|
|
269
|
-
const record = read.record;
|
|
270
|
-
if (!record || read.revision !== projection.projection.record_revision) throw new Error("TaskRecord changed before assurance snapshot capture");
|
|
271
|
-
const intent = record.intent_snapshot;
|
|
272
|
-
const descriptors = new Map<string, VerificationDescriptor>();
|
|
273
|
-
for (const item of role === "qa" ? intent.acceptance : []) {
|
|
274
|
-
const descriptor = parseVerificationDescriptor(item.verification);
|
|
275
|
-
descriptors.set(item.id, descriptor);
|
|
276
|
-
}
|
|
277
|
-
const reviewBundle = null;
|
|
278
|
-
const reviewManifest = role === "review"
|
|
279
|
-
? captureReviewManifest(root, {
|
|
280
|
-
taskId,
|
|
281
|
-
baseHead: record.git_base_head,
|
|
282
|
-
scopeHint: intent.scope_hint,
|
|
283
|
-
expectedDiffHash: projection.projection.diff_hash,
|
|
284
|
-
intentRevision: projection.projection.intent_revision,
|
|
285
|
-
intentContentHash: projection.projection.intent_content_hash,
|
|
286
|
-
recordRevision: projection.projection.record_revision,
|
|
287
|
-
workspaceRevision: projection.projection.workspace_revision,
|
|
288
|
-
lifecycle: projection.projection.lifecycle,
|
|
289
|
-
artifactState: projection.projection.artifact_state,
|
|
290
|
-
risk: intent.risk,
|
|
291
|
-
outcomes: qaOutcomes(record),
|
|
292
|
-
})
|
|
293
|
-
: null;
|
|
294
|
-
const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : [];
|
|
295
|
-
const snapshot: SnapshotDescriptor = {
|
|
296
|
-
contract: "assurance_kernel/assurance_snapshot/v2",
|
|
297
|
-
task_id: taskId,
|
|
298
|
-
run_id: projection.projection.run_id,
|
|
299
|
-
role,
|
|
300
|
-
record_revision: projection.projection.record_revision,
|
|
301
|
-
workspace_revision: projection.projection.workspace_revision,
|
|
302
|
-
intent_revision: projection.projection.intent_revision,
|
|
303
|
-
intent_content_hash: projection.projection.intent_content_hash,
|
|
304
|
-
diff_hash: projection.projection.diff_hash,
|
|
305
|
-
lifecycle: projection.projection.lifecycle,
|
|
306
|
-
artifact_state: projection.projection.artifact_state,
|
|
307
|
-
risk: intent.risk,
|
|
308
|
-
fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
|
|
309
|
-
missing_acceptance_ids: projection.projection.missing_acceptance_ids,
|
|
310
|
-
stale_attestation_ids: projection.projection.stale_attestation_ids,
|
|
311
|
-
acceptance: intent.acceptance,
|
|
312
|
-
dirty_files: dirtyFiles,
|
|
313
|
-
review_bundle_digest: reviewManifest?.manifest_digest ?? null,
|
|
314
|
-
root,
|
|
315
|
-
...(reviewManifest
|
|
316
|
-
? {
|
|
317
|
-
review_revision: {
|
|
318
|
-
contract: "assurance_kernel/review_revision_identity/v1",
|
|
319
|
-
base_head: reviewManifest.base_head,
|
|
320
|
-
review_commit: reviewManifest.review_commit,
|
|
321
|
-
review_tree: reviewManifest.review_tree,
|
|
322
|
-
manifest_digest: reviewManifest.manifest_digest,
|
|
323
|
-
},
|
|
324
|
-
}
|
|
325
|
-
: {}),
|
|
326
|
-
};
|
|
327
|
-
return { snapshot, descriptors, reviewBundle, reviewManifest };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function stagePlanningArtifactTransition(root: string, record: { intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } }): void {
|
|
331
|
-
const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
|
|
332
|
-
const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
|
|
333
|
-
const specActive = record.intent_snapshot.scope_hint.find((path) =>
|
|
334
|
-
/^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
|
|
335
|
-
&& record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
|
|
336
|
-
);
|
|
337
|
-
const candidates = [
|
|
338
|
-
intentActive,
|
|
339
|
-
intentArchive,
|
|
340
|
-
...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
|
|
341
|
-
];
|
|
342
|
-
const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
|
|
343
|
-
if (paths.length === 0) return;
|
|
344
|
-
execFileSync("git", ["add", "--", ...paths], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
202
|
async function mintCapability(
|
|
348
203
|
registry: MutationAuthorityRegistry,
|
|
349
204
|
input: {
|
|
@@ -391,7 +246,6 @@ async function mintCapability(
|
|
|
391
246
|
diff_hash: input.diff_hash,
|
|
392
247
|
actor_id: input.actor_id,
|
|
393
248
|
confirmation_ref: input.confirmation_ref,
|
|
394
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
395
249
|
findings_digest: input.action_kind === "request_rework"
|
|
396
250
|
? findingsDigestV2(input.findings as TaskFinding[])
|
|
397
251
|
: null,
|
|
@@ -414,6 +268,8 @@ export interface ClaudeRuntimeOptions {
|
|
|
414
268
|
* actually wires was never constructed once.
|
|
415
269
|
*/
|
|
416
270
|
ports?: Partial<AssuranceCoordinatorPorts>;
|
|
271
|
+
/** Optional authority test seam, separate from host ports. */
|
|
272
|
+
authorityOverrides?: Partial<VerdictAuthority>;
|
|
417
273
|
interactive?: boolean;
|
|
418
274
|
permissionMode?: PermissionMode;
|
|
419
275
|
requestConfirmation?: NativeConfirmationPort;
|
|
@@ -470,7 +326,8 @@ export class ClaudeRuntime {
|
|
|
470
326
|
...this.createKernelPorts(),
|
|
471
327
|
...options.ports,
|
|
472
328
|
host: this.host,
|
|
473
|
-
|
|
329
|
+
confirmationReference: ({ actorId }) => `claude:${actorId}`,
|
|
330
|
+
}, options.authorityOverrides);
|
|
474
331
|
}
|
|
475
332
|
|
|
476
333
|
observe(event: ClaudeHookEvent): void {
|
|
@@ -503,14 +360,12 @@ export class ClaudeRuntime {
|
|
|
503
360
|
private createKernelPorts(): AssuranceCoordinatorPorts {
|
|
504
361
|
return {
|
|
505
362
|
host: this.host,
|
|
363
|
+
confirmationReference: ({ actorId }) => `claude:${actorId}`,
|
|
506
364
|
projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
|
|
507
|
-
readTaskRecord: async (root, taskId) =>
|
|
365
|
+
readTaskRecord: async (root, taskId) => readTaskRecordRaw(root, taskId),
|
|
508
366
|
readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
509
|
-
buildAssurance: (root, taskId, role, projection) => buildAssuranceSnapshot(root, taskId, role, projection),
|
|
510
|
-
ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
|
|
511
367
|
runQa: (snapshot, descriptors, options) => runDeterministicQa(snapshot, descriptors, options),
|
|
512
368
|
writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
|
|
513
|
-
applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
|
|
514
369
|
applyOrdinaryOperation: (ctx, input) => this.executeOrdinary(ctx, input),
|
|
515
370
|
};
|
|
516
371
|
}
|
|
@@ -587,44 +442,74 @@ export class ClaudeRuntime {
|
|
|
587
442
|
preparation_digest: preparation.digest,
|
|
588
443
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
589
444
|
confirmation_ref: gate.confirmation_ref,
|
|
590
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
591
445
|
nonce,
|
|
592
446
|
};
|
|
593
|
-
|
|
594
|
-
const input = {
|
|
595
|
-
task_id: taskId,
|
|
596
|
-
intent_path: binding.intent_path,
|
|
597
|
-
intent_revision: binding.intent_revision,
|
|
598
|
-
preparation_digest: binding.preparation_digest,
|
|
599
|
-
capability,
|
|
600
|
-
capability_binding: binding,
|
|
601
|
-
now,
|
|
602
|
-
};
|
|
603
|
-
const rehearsal = runEnrollmentRehearsal(this.cwd, input, capability, this.enrollmentRegistry);
|
|
604
|
-
if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
|
|
605
|
-
throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
|
|
606
|
-
}
|
|
607
|
-
return enrollCanaryTask(this.cwd, input, this.enrollmentRegistry);
|
|
447
|
+
return await enrollTask(this.cwd, this.enrollmentRegistry, { binding, now });
|
|
608
448
|
} catch (error) {
|
|
609
449
|
if (gitBaseNote) throw new Error(`${error instanceof Error ? error.message : String(error)}${gitBaseNote}`);
|
|
610
450
|
throw error;
|
|
611
451
|
}
|
|
612
452
|
}
|
|
613
453
|
|
|
614
|
-
async advance(taskId: string, signal?: AbortSignal) {
|
|
615
|
-
return this.
|
|
454
|
+
async advance(taskId: string, signal?: AbortSignal, meta?: ToolMeta) {
|
|
455
|
+
return this.closeOutBatchChild(
|
|
616
456
|
taskId,
|
|
617
|
-
await this.coordinator.advance(taskId, { cwd: this.cwd }, signal),
|
|
457
|
+
await this.withTerminalTracker(taskId, await this.coordinator.advance(taskId, { cwd: this.cwd }, signal)),
|
|
458
|
+
meta,
|
|
618
459
|
);
|
|
619
460
|
}
|
|
620
461
|
|
|
621
|
-
async submitReview(taskId: string, verdictInput: unknown) {
|
|
622
|
-
return this.
|
|
462
|
+
async submitReview(taskId: string, verdictInput: unknown, meta?: ToolMeta) {
|
|
463
|
+
return this.closeOutBatchChild(
|
|
623
464
|
taskId,
|
|
624
|
-
await
|
|
465
|
+
await this.withTerminalTracker(
|
|
466
|
+
taskId,
|
|
467
|
+
await submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput),
|
|
468
|
+
),
|
|
469
|
+
meta,
|
|
625
470
|
);
|
|
626
471
|
}
|
|
627
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Gate-free closeout: a batch child that reached Kernel `done` in the
|
|
475
|
+
* foreground re-enters the batch in the same call, reusing its authorization.
|
|
476
|
+
* Only `done` continues; a stopped child, a parked child and a live Review
|
|
477
|
+
* obligation end where they do today. The continuation is transport: the
|
|
478
|
+
* Kernel result is returned unchanged, and any failure is reported beside it
|
|
479
|
+
* with one retry action, never thrown into a path that could read as "the
|
|
480
|
+
* mutation did not happen".
|
|
481
|
+
*/
|
|
482
|
+
private async closeOutBatchChild<T>(taskId: string, result: T, meta?: ToolMeta): Promise<T> {
|
|
483
|
+
if (!meta || result === null || typeof result !== "object") return result;
|
|
484
|
+
const lifecycle = (result as { record?: { lifecycle?: unknown } }).record?.lifecycle;
|
|
485
|
+
if ((result as { state?: unknown }).state !== "completed" && lifecycle !== "done") return result;
|
|
486
|
+
let slug: string | null;
|
|
487
|
+
try {
|
|
488
|
+
slug = findResumableBatchSlugForTask(this.cwd, taskId);
|
|
489
|
+
} catch {
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
if (!slug) return result;
|
|
493
|
+
try {
|
|
494
|
+
// The batch commit takes exactly the scope envelope plus this task's own
|
|
495
|
+
// audit evidence, and its preflight refuses unstaged changes: stage only
|
|
496
|
+
// that evidence, the step an Executor otherwise does by hand.
|
|
497
|
+
if (existsSync(join(this.cwd, ".imm", "audit", taskId)))
|
|
498
|
+
execFileSync("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
|
|
499
|
+
const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
|
|
500
|
+
return { ...(result as object), batch } as T;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return {
|
|
503
|
+
...(result as object),
|
|
504
|
+
batch: {
|
|
505
|
+
state: "rejected",
|
|
506
|
+
reason: `batch continuation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
507
|
+
recovery_action: "call start_unattended_batch with the same Initiative to continue the batch",
|
|
508
|
+
},
|
|
509
|
+
} as T;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
628
513
|
/**
|
|
629
514
|
* Post-settlement GitHub tracker projection: the same shared step the Pi Host
|
|
630
515
|
* runs, so an opted-in terminal projection no longer depends on which Host
|
|
@@ -723,7 +608,7 @@ export class ClaudeRuntime {
|
|
|
723
608
|
const derived = deriveAuthorizationOperation({ readiness: projection.projection.authorization });
|
|
724
609
|
if ("blocked" in derived) throw new Error(derived.blocked);
|
|
725
610
|
if (derived.operation === "resolve-user-decision") {
|
|
726
|
-
const record = await
|
|
611
|
+
const record = await readTaskRecordRaw(this.cwd, taskId);
|
|
727
612
|
const open = (record.record?.findings ?? []).filter(
|
|
728
613
|
(finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
|
|
729
614
|
);
|
|
@@ -757,7 +642,7 @@ export class ClaudeRuntime {
|
|
|
757
642
|
if (sidecar && nextIntent) {
|
|
758
643
|
writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
|
|
759
644
|
execFileSync("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
760
|
-
const preparedRecord = await
|
|
645
|
+
const preparedRecord = await readTaskRecordRaw(this.cwd, taskId);
|
|
761
646
|
if (!preparedRecord.record) {
|
|
762
647
|
throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
|
|
763
648
|
}
|
|
@@ -780,7 +665,7 @@ export class ClaudeRuntime {
|
|
|
780
665
|
});
|
|
781
666
|
} catch (error) {
|
|
782
667
|
if (stagedSnapshot) {
|
|
783
|
-
const current = await
|
|
668
|
+
const current = await readTaskRecordRaw(this.cwd, taskId);
|
|
784
669
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
|
|
785
670
|
restoreStagedIntent();
|
|
786
671
|
}
|
|
@@ -842,7 +727,7 @@ export class ClaudeRuntime {
|
|
|
842
727
|
) stagePlanningArtifactTransition(this.cwd, result.record);
|
|
843
728
|
} catch (error) {
|
|
844
729
|
if (stagedSnapshot) {
|
|
845
|
-
const current = await
|
|
730
|
+
const current = await readTaskRecordRaw(this.cwd, taskId);
|
|
846
731
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
|
|
847
732
|
restoreStagedIntent();
|
|
848
733
|
}
|
|
@@ -856,94 +741,6 @@ export class ClaudeRuntime {
|
|
|
856
741
|
return this.withTerminalTracker(taskId, committed);
|
|
857
742
|
}
|
|
858
743
|
|
|
859
|
-
private async applyVerdict(
|
|
860
|
-
ctx: HostContext,
|
|
861
|
-
input: {
|
|
862
|
-
taskId: string;
|
|
863
|
-
snapshot: SnapshotDescriptor;
|
|
864
|
-
verdict: AssuranceVerdict;
|
|
865
|
-
invocation: { /* token */ };
|
|
866
|
-
actorId: string;
|
|
867
|
-
hooks?: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> };
|
|
868
|
-
},
|
|
869
|
-
): Promise<void> {
|
|
870
|
-
const { registry, app } = await this.authority();
|
|
871
|
-
const priorIntentToken = (await readTaskIntentForRecord(ctx.cwd, input.taskId)).token;
|
|
872
|
-
const now = new Date().toISOString();
|
|
873
|
-
const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
|
|
874
|
-
this.coordinator.commitInvocation(input.invocation as never);
|
|
875
|
-
const settlement = apply();
|
|
876
|
-
input.hooks?.onCommit?.();
|
|
877
|
-
const result = await settlement;
|
|
878
|
-
await input.hooks?.afterCommit?.();
|
|
879
|
-
return result;
|
|
880
|
-
};
|
|
881
|
-
if (input.verdict.decision === "rework") {
|
|
882
|
-
const findings = reviewReworkFindings(input.verdict);
|
|
883
|
-
const capability = await mintCapability(registry, {
|
|
884
|
-
authority_kind: input.snapshot.role,
|
|
885
|
-
task_id: input.taskId,
|
|
886
|
-
run_id: input.snapshot.run_id,
|
|
887
|
-
action_kind: "request_rework",
|
|
888
|
-
expected_record_hash: input.snapshot.record_revision,
|
|
889
|
-
intent_revision: input.snapshot.intent_revision,
|
|
890
|
-
intent_content_hash: input.snapshot.intent_content_hash,
|
|
891
|
-
diff_hash: input.snapshot.diff_hash,
|
|
892
|
-
actor_id: input.actorId,
|
|
893
|
-
findings,
|
|
894
|
-
now,
|
|
895
|
-
confirmation_ref: `claude:${input.actorId}`,
|
|
896
|
-
});
|
|
897
|
-
await input.hooks?.beforeCommit?.();
|
|
898
|
-
const result = await commitAndApply(async () => app.execute({
|
|
899
|
-
root: ctx.cwd,
|
|
900
|
-
task_id: input.taskId,
|
|
901
|
-
operation: { op: "request_rework", capability, findings: findings as never[], actor_id: input.actorId },
|
|
902
|
-
prior_intent_token: priorIntentToken,
|
|
903
|
-
diffProvider: diffSnapshotOf,
|
|
904
|
-
now,
|
|
905
|
-
}));
|
|
906
|
-
stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
const advisories = input.verdict.decision === "pass" ? reviewAdvisoryRecords(input.verdict) : [];
|
|
910
|
-
const approval: TaskApprovalV2 = {
|
|
911
|
-
id: `approval-${input.snapshot.role}-${randomUUID().slice(0, 8)}`,
|
|
912
|
-
kind: input.snapshot.role === "qa" ? "qa" : "review",
|
|
913
|
-
authority_role: input.snapshot.role === "qa" ? "qa" : "reviewer",
|
|
914
|
-
task_revision: input.snapshot.intent_revision,
|
|
915
|
-
intent_content_hash: input.snapshot.intent_content_hash,
|
|
916
|
-
diff_hash: input.snapshot.diff_hash,
|
|
917
|
-
actor_id: input.actorId,
|
|
918
|
-
summary: input.verdict.approval!.summary,
|
|
919
|
-
...(input.snapshot.role === "review" && input.snapshot.review_revision ? { review_revision: input.snapshot.review_revision } : {}),
|
|
920
|
-
...(input.snapshot.role === "review" && advisories.length > 0 ? { advisory_findings: advisories } : {}),
|
|
921
|
-
};
|
|
922
|
-
const capability = await mintCapability(registry, {
|
|
923
|
-
authority_kind: input.snapshot.role,
|
|
924
|
-
task_id: input.taskId,
|
|
925
|
-
run_id: input.snapshot.run_id,
|
|
926
|
-
action_kind: "record_approval",
|
|
927
|
-
expected_record_hash: input.snapshot.record_revision,
|
|
928
|
-
intent_revision: input.snapshot.intent_revision,
|
|
929
|
-
intent_content_hash: input.snapshot.intent_content_hash,
|
|
930
|
-
diff_hash: input.snapshot.diff_hash,
|
|
931
|
-
actor_id: input.actorId,
|
|
932
|
-
approval,
|
|
933
|
-
now,
|
|
934
|
-
confirmation_ref: `claude:${input.actorId}`,
|
|
935
|
-
});
|
|
936
|
-
await input.hooks?.beforeCommit?.();
|
|
937
|
-
await commitAndApply(async () => app.execute({
|
|
938
|
-
root: ctx.cwd,
|
|
939
|
-
task_id: input.taskId,
|
|
940
|
-
operation: { op: "record_approval", capability, approval, actor_id: input.actorId },
|
|
941
|
-
prior_intent_token: priorIntentToken,
|
|
942
|
-
diffProvider: diffSnapshotOf,
|
|
943
|
-
now,
|
|
944
|
-
}));
|
|
945
|
-
}
|
|
946
|
-
|
|
947
744
|
private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown; finding_id?: string; attestation_id?: string } }) {
|
|
948
745
|
const { app } = await this.authority();
|
|
949
746
|
const operation = input.operation.op === "revise_intent"
|
|
@@ -976,7 +773,7 @@ export class ClaudeRuntime {
|
|
|
976
773
|
return result;
|
|
977
774
|
} catch (error) {
|
|
978
775
|
if (priorStaged) {
|
|
979
|
-
const current = await
|
|
776
|
+
const current = await readTaskRecordRaw(ctx.cwd, input.taskId);
|
|
980
777
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision)
|
|
981
778
|
restoreStagedIntentShared(ctx.cwd, priorStaged);
|
|
982
779
|
}
|
|
@@ -987,15 +784,18 @@ export class ClaudeRuntime {
|
|
|
987
784
|
async startUnattendedBatch(
|
|
988
785
|
initiativeSlug: string,
|
|
989
786
|
meta: ToolMeta,
|
|
787
|
+
options: { reuseOnly?: boolean } = {},
|
|
990
788
|
): Promise<ClaudeBatchStartResult> {
|
|
991
789
|
throwIfCancelled(meta.signal);
|
|
790
|
+
const reuseOnly = options.reuseOnly === true;
|
|
992
791
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
993
792
|
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
793
|
+
// A reuse-only continuation never opens a gate, so it needs no confirmation port.
|
|
994
794
|
const interactive = meta.interactive ?? this.interactive;
|
|
995
|
-
if (!interactive) throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
795
|
+
if (!reuseOnly && !interactive) throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
996
796
|
// The text is table-owned; only the form (a thrown native error rather than
|
|
997
797
|
// a returned envelope) is this Host's.
|
|
998
|
-
if (!this.requestConfirmation)
|
|
798
|
+
if (!reuseOnly && !this.requestConfirmation)
|
|
999
799
|
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
1000
800
|
|
|
1001
801
|
// 1. Host-independent batch preflight: claim ownership, branch
|
|
@@ -1022,7 +822,7 @@ export class ClaudeRuntime {
|
|
|
1022
822
|
const budget = preflight.projection.budget;
|
|
1023
823
|
const planDigest = preflight.projection.plan_digest;
|
|
1024
824
|
const recoveryChildren = preflight.projection.recovery_children;
|
|
1025
|
-
// 5. Literal-user gate plus the shared reuse
|
|
825
|
+
// 5. Literal-user gate plus the shared reuse decision, the post-gate
|
|
1026
826
|
// claim/drift cascade, and the Batch Authorization binding. The Host supplies
|
|
1027
827
|
// only its gate, its confirmation reference, and its binding nonce.
|
|
1028
828
|
const authorization = await authorizeBatch<ClaudeBatchStartResult>({
|
|
@@ -1033,9 +833,19 @@ export class ClaudeRuntime {
|
|
|
1033
833
|
readInitiative: this.readInitiative ?? observeGithubInitiative,
|
|
1034
834
|
nonce: enrollmentNonce(),
|
|
1035
835
|
gate: async (facts) => {
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
|
|
836
|
+
// A continuation after a foreground child never opens a gate: when the
|
|
837
|
+
// authorization cannot be reused, the user re-enters the batch tool.
|
|
838
|
+
if (reuseOnly)
|
|
839
|
+
return {
|
|
840
|
+
kind: "host_rejection",
|
|
841
|
+
value: {
|
|
842
|
+
state: "blocked",
|
|
843
|
+
reason: `batch authorization cannot be reused (${facts.reuse_blockers.join(", ")})`,
|
|
844
|
+
recovery_action: "call start_unattended_batch with the same Initiative to confirm a fresh authorization",
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
// The gate settles only on the literal user's answer or the caller's
|
|
848
|
+
// cancellation signal.
|
|
1039
849
|
let confirmationResult: { decision: NativeDecision; requestId: string };
|
|
1040
850
|
try {
|
|
1041
851
|
confirmationResult = await this.requestConfirmation!({
|
|
@@ -1057,7 +867,6 @@ export class ClaudeRuntime {
|
|
|
1057
867
|
reason: child.reason,
|
|
1058
868
|
})),
|
|
1059
869
|
budget: facts.budget,
|
|
1060
|
-
expires_at: facts.expires_at,
|
|
1061
870
|
...(facts.reuse_blockers.length > 0
|
|
1062
871
|
? {
|
|
1063
872
|
re_confirmation_required: facts.reuse_blockers,
|
|
@@ -1065,10 +874,9 @@ export class ClaudeRuntime {
|
|
|
1065
874
|
}
|
|
1066
875
|
: {}),
|
|
1067
876
|
},
|
|
1068
|
-
signal:
|
|
877
|
+
signal: meta.signal,
|
|
1069
878
|
});
|
|
1070
879
|
} catch (err) {
|
|
1071
|
-
if (deadline.timedOut()) return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
|
|
1072
880
|
if (meta.signal?.aborted)
|
|
1073
881
|
return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
|
|
1074
882
|
if (err instanceof NativeAuthorityError) {
|
|
@@ -1088,13 +896,8 @@ export class ClaudeRuntime {
|
|
|
1088
896
|
kind: "host_rejection",
|
|
1089
897
|
value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err)),
|
|
1090
898
|
};
|
|
1091
|
-
} finally {
|
|
1092
|
-
deadline.clear();
|
|
1093
899
|
}
|
|
1094
900
|
|
|
1095
|
-
// A transport that answers "cancel" on abort is still a timeout, not a user
|
|
1096
|
-
// decision: the Host that owns the transport reports which one it was.
|
|
1097
|
-
if (deadline.timedOut()) return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
|
|
1098
901
|
if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
|
|
1099
902
|
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
1100
903
|
if (meta.signal?.aborted)
|
|
@@ -1122,27 +925,38 @@ export class ClaudeRuntime {
|
|
|
1122
925
|
});
|
|
1123
926
|
if (authorization.outcome === "host_rejection") return authorization.value;
|
|
1124
927
|
if (authorization.outcome === "rejected") return authorization.rejection;
|
|
1125
|
-
const { binding, batch_id: batchId
|
|
928
|
+
const { binding, batch_id: batchId } = authorization;
|
|
1126
929
|
|
|
1127
930
|
// review-1: verify cancellation signal right before authority issuance and startBatch
|
|
1128
931
|
if (meta.signal?.aborted) return batchReason("cancelled_before_execution");
|
|
1129
932
|
|
|
1130
933
|
now = new Date().toISOString();
|
|
1131
|
-
if (Date.parse(expiresAt) <= Date.parse(now))
|
|
1132
|
-
return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
|
|
1133
934
|
const capability = this.batchRegistry.issue(binding, recoveryChildren as any, now);
|
|
1134
935
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
936
|
+
// review-batch-partial-port-fabricates-enrollment: the production child
|
|
937
|
+
// Kernel port is the shared runtime/unattended one; this Host supplies
|
|
938
|
+
// only its own advanceTask progression seam (D5).
|
|
939
|
+
const advanceTask: BatchRunnerKernelPort["advanceTask"] = async (root, taskId) => {
|
|
940
|
+
const result = await this.coordinator.advance(taskId, { cwd: root });
|
|
941
|
+
const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
|
|
942
|
+
if (result.state === "completed") return { state: "completed", ...facts };
|
|
943
|
+
if (result.state === "stopped") return { state: "stopped", ...facts };
|
|
944
|
+
if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
|
|
945
|
+
if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, ...facts };
|
|
946
|
+
if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
|
|
947
|
+
if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
|
|
948
|
+
return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
|
|
1145
949
|
};
|
|
950
|
+
const kernelPort = createBatchKernelPort({
|
|
951
|
+
root: this.cwd,
|
|
952
|
+
enrollmentRegistry: this.enrollmentRegistry,
|
|
953
|
+
registry: this.batchRegistry,
|
|
954
|
+
capability,
|
|
955
|
+
binding,
|
|
956
|
+
advanceTask,
|
|
957
|
+
resume: { isResuming, existingBatch, batchBranch },
|
|
958
|
+
overrides: this.batchKernel,
|
|
959
|
+
});
|
|
1146
960
|
const report = await startBatch({
|
|
1147
961
|
root: this.cwd,
|
|
1148
962
|
batch_id: batchId,
|
|
@@ -1153,7 +967,6 @@ export class ClaudeRuntime {
|
|
|
1153
967
|
plan_digest: planDigest,
|
|
1154
968
|
base_head: binding.base_head,
|
|
1155
969
|
confirmation_time: now,
|
|
1156
|
-
authorization_expires_at: expiresAt,
|
|
1157
970
|
budget,
|
|
1158
971
|
now,
|
|
1159
972
|
kernel: kernelPort,
|
|
@@ -1169,80 +982,6 @@ export class ClaudeRuntime {
|
|
|
1169
982
|
report,
|
|
1170
983
|
};
|
|
1171
984
|
}
|
|
1172
|
-
|
|
1173
|
-
private createBatchKernelPort(
|
|
1174
|
-
registry: BatchAuthorityRegistry,
|
|
1175
|
-
capability: object,
|
|
1176
|
-
binding: BatchAuthorizationBinding,
|
|
1177
|
-
): BatchRunnerKernelPort {
|
|
1178
|
-
// review-batch-partial-port-fabricates-enrollment: construct real production port first, never fabricate stubs
|
|
1179
|
-
const realPort: BatchRunnerKernelPort = {
|
|
1180
|
-
enrollTask: async ({ root, task_id, batch }) => {
|
|
1181
|
-
const now = new Date().toISOString();
|
|
1182
|
-
const derived = deriveChildEnrollment(root, batch.registry, {
|
|
1183
|
-
capability: batch.capability,
|
|
1184
|
-
binding,
|
|
1185
|
-
task_id,
|
|
1186
|
-
expected_head: batch.binding.expected_head,
|
|
1187
|
-
now,
|
|
1188
|
-
});
|
|
1189
|
-
const enrollmentCapability = this.enrollmentRegistry.issue(derived.binding);
|
|
1190
|
-
const input = {
|
|
1191
|
-
task_id,
|
|
1192
|
-
intent_path: derived.binding.intent_path,
|
|
1193
|
-
intent_revision: derived.binding.intent_revision,
|
|
1194
|
-
preparation_digest: derived.binding.preparation_digest,
|
|
1195
|
-
capability: enrollmentCapability,
|
|
1196
|
-
capability_binding: derived.binding,
|
|
1197
|
-
batch: {
|
|
1198
|
-
registry: batch.registry,
|
|
1199
|
-
capability: batch.capability,
|
|
1200
|
-
binding,
|
|
1201
|
-
expected_head: batch.binding.expected_head,
|
|
1202
|
-
},
|
|
1203
|
-
now,
|
|
1204
|
-
};
|
|
1205
|
-
const rehearsal = runEnrollmentRehearsal(root, input, enrollmentCapability, this.enrollmentRegistry);
|
|
1206
|
-
if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
|
|
1207
|
-
throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
|
|
1208
|
-
}
|
|
1209
|
-
// The enrollment itself is the effect; its record is re-read below.
|
|
1210
|
-
enrollCanaryTask(root, input, this.enrollmentRegistry);
|
|
1211
|
-
const recordRaw = readTaskRecordRaw(root, task_id);
|
|
1212
|
-
return { record_revision: recordRaw.revision };
|
|
1213
|
-
},
|
|
1214
|
-
advanceTask: async (root, taskId) => {
|
|
1215
|
-
const result = await this.coordinator.advance(taskId, { cwd: root });
|
|
1216
|
-
const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
|
|
1217
|
-
if (result.state === "completed") return { state: "completed", ...facts };
|
|
1218
|
-
if (result.state === "stopped") return { state: "stopped", ...facts };
|
|
1219
|
-
if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
|
|
1220
|
-
if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, ...facts };
|
|
1221
|
-
if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
|
|
1222
|
-
if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
|
|
1223
|
-
return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
|
|
1224
|
-
},
|
|
1225
|
-
projectTask: async (root, taskId) => {
|
|
1226
|
-
const fresh = await projectAssurance(root, taskId, diffSnapshotOf);
|
|
1227
|
-
if (!fresh.claim || fresh.projection.lifecycle !== "active" || fresh.error !== null) return fresh;
|
|
1228
|
-
const { record, revision } = readTaskRecordRaw(root, taskId);
|
|
1229
|
-
if (!record || revision !== fresh.projection.record_revision) throw new Error(`Kernel recovery projection changed for ${taskId}`);
|
|
1230
|
-
return { ...fresh, ...batchQaFailureFacts(record) };
|
|
1231
|
-
},
|
|
1232
|
-
ownsTaskClaim: (taskId) => {
|
|
1233
|
-
return registry.isChildConsumed(capability, taskId);
|
|
1234
|
-
},
|
|
1235
|
-
validateBatchAuthorization: (input) => {
|
|
1236
|
-
return input.registry.inspect(input.capability, input.binding as never);
|
|
1237
|
-
},
|
|
1238
|
-
};
|
|
1239
|
-
|
|
1240
|
-
if (!this.batchKernel) return realPort;
|
|
1241
|
-
return {
|
|
1242
|
-
...realPort,
|
|
1243
|
-
...this.batchKernel,
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1246
985
|
}
|
|
1247
986
|
|
|
1248
987
|
export interface ToolMeta {
|