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,3 +1,7 @@
|
|
|
1
|
+
import { buildAssuranceSnapshot, stagePlanningArtifactTransition } from "../runtime/assurance/verdict_authority";
|
|
2
|
+
export { buildSnapshot, ensureReviewRevision as ensureTaskReviewRevision } from "../runtime/assurance/verdict_authority";
|
|
3
|
+
export type { SnapshotDescriptorInput } from "../runtime/assurance/verdict_authority";
|
|
4
|
+
import type { ConfirmationReferenceSource } from "../runtime/assurance/host_port";
|
|
1
5
|
// P3 Pi lifecycle extension: the only production route for Kernel canary
|
|
2
6
|
// assurance after enrollment.
|
|
3
7
|
//
|
|
@@ -21,19 +25,14 @@ import { join, resolve } from "node:path";
|
|
|
21
25
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
26
|
import { Type } from "typebox";
|
|
23
27
|
import { PLUGIN_VERSION } from "../runtime/plugin_version";
|
|
28
|
+
import { findResumableBatchSlugForTask } from "../runtime/unattended/batch_preflight";
|
|
29
|
+
import { executePiUnattendedBatch } from "./imm-unattended-batch";
|
|
24
30
|
import {
|
|
25
|
-
parseVerificationDescriptor,
|
|
26
|
-
type VerificationDescriptor,
|
|
27
31
|
} from "./pi-canary-verification";
|
|
28
32
|
import {
|
|
29
|
-
captureReviewBundle,
|
|
30
|
-
captureReviewManifest,
|
|
31
33
|
listReviewRefs,
|
|
32
34
|
reconcileReviewRefs,
|
|
33
35
|
writeNativeReviewEvidence,
|
|
34
|
-
type ReviewBundle,
|
|
35
|
-
type ReviewManifestV5,
|
|
36
|
-
type ReviewRevision,
|
|
37
36
|
} from "./pi-canary-review-bundle";
|
|
38
37
|
import type { InvocationToken } from "./pi-canary-invocations";
|
|
39
38
|
import { runDeterministicQa } from "../runtime/assurance/qa";
|
|
@@ -44,7 +43,6 @@ import {
|
|
|
44
43
|
import {
|
|
45
44
|
renderCanaryCall,
|
|
46
45
|
renderCanaryResult,
|
|
47
|
-
type AssuranceRole,
|
|
48
46
|
} from "./pi-canary-assurance";
|
|
49
47
|
import {
|
|
50
48
|
USER_ATTENTION_EVENT,
|
|
@@ -69,8 +67,7 @@ import { resolveUxLanguage, uxText } from "./ux-language";
|
|
|
69
67
|
|
|
70
68
|
/** Host-native UI language; see ux-language.ts. Resolved once per process. */
|
|
71
69
|
const UX_LANG = resolveUxLanguage();
|
|
72
|
-
import {
|
|
73
|
-
import { reviewAdvisoryRecords, reviewReworkFindings } from "../runtime/assurance/coordinator";
|
|
70
|
+
import { taskDeliveryIdentity } from "../runtime/workspace_scope";
|
|
74
71
|
import {
|
|
75
72
|
AssuranceProgression,
|
|
76
73
|
buildReviewPrompt,
|
|
@@ -85,44 +82,54 @@ import {
|
|
|
85
82
|
REVIEW_TIMING_PROFILES,
|
|
86
83
|
REVIEW_VERDICT_VALIDATION_TIMEOUT_MS,
|
|
87
84
|
type AssuranceProgressionPorts,
|
|
88
|
-
type AssuranceVerdict,
|
|
89
85
|
type HostContext,
|
|
90
|
-
type SnapshotDescriptor,
|
|
91
86
|
} from "./pi-canary-assurance-progression";
|
|
92
87
|
|
|
93
|
-
// The
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
88
|
+
// The Pi adapter imports the host-neutral runtime modules statically, exactly
|
|
89
|
+
// as the Claude adapter does; the retired dynamic-import isolation layer is
|
|
90
|
+
// gone. The adapter-owned logic that used to live behind it (the
|
|
91
|
+
// record-aware intent read, the tracker terminal wrapper, the shared batch
|
|
92
|
+
// progression accessors) is defined below, next to the machinery it uses.
|
|
93
|
+
import { LITERAL_USER_ACTOR_ID } from "../runtime/kernel/actor_identity";
|
|
97
94
|
import {
|
|
98
|
-
createMutationAuthorityRegistry,
|
|
99
|
-
createCanaryApplication,
|
|
100
|
-
buildLoopAction,
|
|
101
|
-
buildLoopRoleDispatch,
|
|
102
|
-
readBackendClaim,
|
|
103
|
-
readTaskTombstone,
|
|
104
|
-
markGithubTaskTerminal,
|
|
105
95
|
reconcileKernelAuthority,
|
|
106
96
|
repairKernelAuthority,
|
|
107
|
-
|
|
97
|
+
readTaskRecordRaw,
|
|
108
98
|
withKernelStoreLockForTask,
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
} from "../runtime/kernel/storage";
|
|
100
|
+
import { inspectStorageLayout } from "../runtime/kernel/storage_paths";
|
|
101
|
+
import { migrateLegacyLayout } from "../runtime/kernel/storage_layout_migration";
|
|
102
|
+
import {
|
|
111
103
|
readTaskIntent,
|
|
112
104
|
parseTaskIntentV1,
|
|
113
105
|
canonicalIntentHash,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
type ReadTaskIntentResult,
|
|
107
|
+
} from "../runtime/kernel/intent";
|
|
108
|
+
import { readBackendClaim, readTaskTombstone } from "../runtime/kernel/backend_claim";
|
|
109
|
+
import {
|
|
110
|
+
createMutationAuthorityRegistry,
|
|
117
111
|
digestOfAction,
|
|
118
|
-
type AssuranceProjectionResult,
|
|
119
|
-
type TaskRecordRead,
|
|
120
|
-
type CanaryApplication,
|
|
121
|
-
type MutationAuthorityRegistry,
|
|
122
112
|
type CapabilityBindingV2,
|
|
123
|
-
|
|
124
|
-
} from "
|
|
125
|
-
import {
|
|
113
|
+
type MutationAuthorityRegistry,
|
|
114
|
+
} from "../runtime/kernel/authority_port";
|
|
115
|
+
import {
|
|
116
|
+
createCanaryApplication,
|
|
117
|
+
capabilityActionFor,
|
|
118
|
+
type CanaryApplication,
|
|
119
|
+
} from "../runtime/kernel/canary_application";
|
|
120
|
+
import { findingsDigestV2 } from "../runtime/kernel/reducer";
|
|
121
|
+
import {
|
|
122
|
+
projectAssurance,
|
|
123
|
+
type AssuranceProjectionResult,
|
|
124
|
+
} from "../runtime/kernel/assurance_projection";
|
|
125
|
+
import type { TaskRecord } from "../runtime/kernel/types";
|
|
126
|
+
import type { RoleDelegationContext } from "../runtime/role_prompt_bridge";
|
|
127
|
+
import { buildLoopAction, buildLoopRoleDispatch } from "../runtime/loop_contract";
|
|
128
|
+
import {
|
|
129
|
+
runGithubTrackerOperation,
|
|
130
|
+
type GithubTrackerResult,
|
|
131
|
+
} from "../runtime/github_issue_tracker";
|
|
132
|
+
import { reviewReworkFindings } from "../runtime/assurance/coordinator";
|
|
126
133
|
|
|
127
134
|
const LOOP_OWNERS = ["plan", "kernel", "brainstorm", "planner", "loop"] as const;
|
|
128
135
|
const LOOP_TARGETS = [
|
|
@@ -176,60 +183,6 @@ export type AuthorizeOperation =
|
|
|
176
183
|
| "authorize-rework"
|
|
177
184
|
| "stop";
|
|
178
185
|
|
|
179
|
-
export interface SnapshotDescriptorInput {
|
|
180
|
-
root: string;
|
|
181
|
-
task_id: string;
|
|
182
|
-
role: AssuranceRole;
|
|
183
|
-
run_id?: string | null;
|
|
184
|
-
record_revision: string;
|
|
185
|
-
workspace_revision: string;
|
|
186
|
-
intent_revision: number;
|
|
187
|
-
intent_content_hash: string;
|
|
188
|
-
diff_hash: string;
|
|
189
|
-
lifecycle: string;
|
|
190
|
-
artifact_state: string;
|
|
191
|
-
risk?: "routine" | "material" | "critical";
|
|
192
|
-
fresh_acceptance_ids: string[];
|
|
193
|
-
missing_acceptance_ids: string[];
|
|
194
|
-
stale_attestation_ids: string[];
|
|
195
|
-
acceptance: Array<{ id: string; assertion: string; verification: string }>;
|
|
196
|
-
dirty_files?: string[];
|
|
197
|
-
review_bundle_digest?: string | null;
|
|
198
|
-
/** Present only when Review authority binds an immutable Git revision (v4). */
|
|
199
|
-
review_revision?: {
|
|
200
|
-
contract: "assurance_kernel/review_revision_identity/v1";
|
|
201
|
-
base_head: string;
|
|
202
|
-
review_commit: string;
|
|
203
|
-
review_tree: string;
|
|
204
|
-
manifest_digest: string;
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function buildSnapshot(input: SnapshotDescriptorInput): SnapshotDescriptor {
|
|
209
|
-
return {
|
|
210
|
-
contract: "assurance_kernel/assurance_snapshot/v2",
|
|
211
|
-
task_id: input.task_id,
|
|
212
|
-
run_id: input.run_id ?? null,
|
|
213
|
-
role: input.role,
|
|
214
|
-
record_revision: input.record_revision,
|
|
215
|
-
workspace_revision: input.workspace_revision,
|
|
216
|
-
intent_revision: input.intent_revision,
|
|
217
|
-
intent_content_hash: input.intent_content_hash,
|
|
218
|
-
diff_hash: input.diff_hash,
|
|
219
|
-
lifecycle: input.lifecycle,
|
|
220
|
-
artifact_state: input.artifact_state,
|
|
221
|
-
risk: input.risk ?? "material",
|
|
222
|
-
fresh_acceptance_ids: input.fresh_acceptance_ids,
|
|
223
|
-
missing_acceptance_ids: input.missing_acceptance_ids,
|
|
224
|
-
stale_attestation_ids: input.stale_attestation_ids,
|
|
225
|
-
acceptance: input.acceptance,
|
|
226
|
-
dirty_files: [...(input.dirty_files ?? [])].sort(),
|
|
227
|
-
review_bundle_digest: input.review_bundle_digest ?? null,
|
|
228
|
-
...(input.review_revision ? { review_revision: input.review_revision } : {}),
|
|
229
|
-
root: resolve(input.root),
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
186
|
export interface CanaryWorkExtensionDependencies {
|
|
234
187
|
buildAssurance?: typeof buildAssuranceSnapshot;
|
|
235
188
|
runQa?: typeof runDeterministicQa;
|
|
@@ -275,25 +228,15 @@ export function createPiAssuranceProgressionPorts(
|
|
|
275
228
|
dependencies: CanaryWorkExtensionDependencies = {},
|
|
276
229
|
): AssuranceProgressionPorts {
|
|
277
230
|
return {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
ensureReviewRevision: (root, taskId, projection) => ensureTaskReviewRevision(root, taskId, projection),
|
|
231
|
+
confirmationReference: piConfirmationReference,
|
|
232
|
+
onReworkApplied: (ctx, taskId, count) => notifyHost(ctx, `rework-parked:${taskId}`, `rework applied: review parked for replan with ${count} finding(s)`, "warning"),
|
|
233
|
+
projectTask: (root, taskId) => projectAssuranceForTask(root, taskId),
|
|
234
|
+
readTaskRecord: async (root, taskId) => readTaskRecordRaw(root, taskId),
|
|
235
|
+
readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
284
236
|
runQa: (snapshot, descriptors, options) =>
|
|
285
237
|
(dependencies.runQa ?? runDeterministicQa)(snapshot, descriptors, options),
|
|
286
238
|
writeReviewEvidence: (input) =>
|
|
287
239
|
(dependencies.writeReviewEvidence ?? writeNativeReviewEvidence)(input),
|
|
288
|
-
applyVerdict: (ctx, input) =>
|
|
289
|
-
applyAssuranceVerdict(
|
|
290
|
-
ctx,
|
|
291
|
-
input.snapshot,
|
|
292
|
-
input.verdict,
|
|
293
|
-
input.invocation,
|
|
294
|
-
input.actorId,
|
|
295
|
-
input.hooks,
|
|
296
|
-
),
|
|
297
240
|
applyOrdinaryOperation: (ctx, input) => executeOrdinaryOperation(ctx, input),
|
|
298
241
|
advanceBeforeProjection: dependencies.advanceBeforeProjection,
|
|
299
242
|
qaBeforeProjection: dependencies.qaBeforeProjection,
|
|
@@ -301,11 +244,46 @@ export function createPiAssuranceProgressionPorts(
|
|
|
301
244
|
qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
|
|
302
245
|
qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
|
|
303
246
|
qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
|
|
247
|
+
...(dependencies.buildAssurance ? { authorityOverrides: { buildAssurance: dependencies.buildAssurance } } : {}),
|
|
304
248
|
} satisfies AssuranceProgressionPorts;
|
|
305
249
|
}
|
|
306
250
|
|
|
307
251
|
const GLOBAL_PI_PROGRESSION_KEY = Symbol.for("immune_brain.pi_assurance_progression");
|
|
308
252
|
|
|
253
|
+
/**
|
|
254
|
+
* The shared session progression the batch adapter advances children through.
|
|
255
|
+
* The work extension publishes the instance at load; this accessor is the
|
|
256
|
+
* lazy fallback that builds one from the production ports when no extension
|
|
257
|
+
* factory has run (tests, direct batch drives). Both paths publish under the
|
|
258
|
+
* same global key, so a batch Review reservation and the session's
|
|
259
|
+
* submit_review always share one progression instance.
|
|
260
|
+
*/
|
|
261
|
+
export async function getSharedPiProgression(): Promise<AssuranceProgression> {
|
|
262
|
+
let progression = (globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] as AssuranceProgression | undefined;
|
|
263
|
+
if (!progression) {
|
|
264
|
+
progression = new AssuranceProgression(createPiAssuranceProgressionPorts());
|
|
265
|
+
(globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] = progression;
|
|
266
|
+
}
|
|
267
|
+
return progression;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Advance one task through the shared session progression and translate the
|
|
272
|
+
* progression result into the batch runner's advance report shape.
|
|
273
|
+
*/
|
|
274
|
+
export async function advancePiTask(root: string, taskId: string): Promise<any> {
|
|
275
|
+
const progression = await getSharedPiProgression();
|
|
276
|
+
const result = await progression.advance(taskId, { cwd: root });
|
|
277
|
+
const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
|
|
278
|
+
if (result.state === "completed") return { state: "completed", ...facts };
|
|
279
|
+
if (result.state === "stopped") return { state: "stopped", ...facts };
|
|
280
|
+
if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
|
|
281
|
+
if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, agent_params: result.agent_params, ...facts };
|
|
282
|
+
if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
|
|
283
|
+
if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
|
|
284
|
+
return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
|
|
285
|
+
}
|
|
286
|
+
|
|
309
287
|
export default function (
|
|
310
288
|
pi: ExtensionAPI,
|
|
311
289
|
dependencies: CanaryWorkExtensionDependencies = {},
|
|
@@ -321,7 +299,7 @@ export default function (
|
|
|
321
299
|
try {
|
|
322
300
|
const claim = await readBackendClaim(ctx.cwd);
|
|
323
301
|
if (!claim) return;
|
|
324
|
-
const projection = await
|
|
302
|
+
const projection = await projectAssuranceForTask(ctx.cwd, claim.task_id);
|
|
325
303
|
if (projection.error) {
|
|
326
304
|
presentTaskRail(ctx, {
|
|
327
305
|
task_id: claim.task_id,
|
|
@@ -581,12 +559,16 @@ export default function (
|
|
|
581
559
|
: action.op === "request_stop"
|
|
582
560
|
? await authorizeExactOperation(taskId, "stop", { ...ctx, signal: signal && ctx.signal ? AbortSignal.any([signal, ctx.signal]) : signal ?? ctx.signal })
|
|
583
561
|
: await requestAuthorization(taskId, ctx);
|
|
584
|
-
const enriched = await
|
|
562
|
+
const enriched = await closeOutBatchChild(
|
|
563
|
+
ctx,
|
|
564
|
+
taskId,
|
|
565
|
+
await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>),
|
|
566
|
+
);
|
|
585
567
|
presentTaskRailResult(ctx, taskId, enriched);
|
|
586
568
|
throwIfCanaryToolFailure(taskId, action.op, enriched);
|
|
587
569
|
return toolResult(JSON.stringify(enriched, null, 2), enriched);
|
|
588
570
|
}
|
|
589
|
-
const projection = await
|
|
571
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
590
572
|
if (projection.error) {
|
|
591
573
|
const nextAction = recoveryActionForAssuranceFailure(projection.error) ?? "inspect authority state";
|
|
592
574
|
const details = {
|
|
@@ -628,7 +610,7 @@ export default function (
|
|
|
628
610
|
taskId,
|
|
629
611
|
operation: toCanaryOperation(action, "executor") as { op: string; actor_id: string },
|
|
630
612
|
})) as unknown as { revision: string; record: { lifecycle: string; artifact_state: string } };
|
|
631
|
-
const updated = await
|
|
613
|
+
const updated = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
632
614
|
const taskState = updated.error
|
|
633
615
|
? { lifecycle: result.record.lifecycle, artifact_state: result.record.artifact_state }
|
|
634
616
|
: updated.projection;
|
|
@@ -704,15 +686,18 @@ export default function (
|
|
|
704
686
|
_ctx: ExtensionContext,
|
|
705
687
|
) => {
|
|
706
688
|
const { action } = params;
|
|
689
|
+
// The Tool schema is intentionally broader than loop_contract's input
|
|
690
|
+
// types (model-supplied JSON); loop_contract narrows and validates at
|
|
691
|
+
// runtime, so these boundary casts are type-level only.
|
|
707
692
|
const result = action.op === "route"
|
|
708
693
|
? await buildLoopAction({
|
|
709
694
|
ownership: action.ownership,
|
|
710
695
|
target: action.target,
|
|
711
|
-
context: action.context,
|
|
696
|
+
context: action.context as RoleDelegationContext | undefined,
|
|
712
697
|
scope_expansion: action.scope_expansion,
|
|
713
|
-
kernel_operation: action.kernel_operation,
|
|
698
|
+
kernel_operation: action.kernel_operation as Parameters<typeof buildLoopAction>[0]["kernel_operation"],
|
|
714
699
|
})
|
|
715
|
-
: await buildLoopRoleDispatch({ role: action.role, context: action.context });
|
|
700
|
+
: await buildLoopRoleDispatch({ role: action.role, context: action.context as RoleDelegationContext });
|
|
716
701
|
const details = loopResultDetails(result, action.op);
|
|
717
702
|
return toolResult(JSON.stringify(result, null, 2), details);
|
|
718
703
|
},
|
|
@@ -769,14 +754,14 @@ export default function (
|
|
|
769
754
|
return { state: "blocked", reason };
|
|
770
755
|
}
|
|
771
756
|
try {
|
|
772
|
-
const projection = await
|
|
757
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
773
758
|
if (projection.error || !projection.claim) {
|
|
774
759
|
const reason = projection.error ?? "no active backend claim";
|
|
775
760
|
notifyOnce(ctx, `authorization-claim:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
|
|
776
761
|
progression.closeInvocation(invocation);
|
|
777
762
|
return { state: "blocked", reason };
|
|
778
763
|
}
|
|
779
|
-
const priorIntent = await
|
|
764
|
+
const priorIntent = await readTaskIntentForRecord(ctx.cwd, taskId);
|
|
780
765
|
const sidecar = nextIntent ? join(ctx.cwd, priorIntent.intent_ref.path) : undefined;
|
|
781
766
|
const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
|
|
782
767
|
const stagedSnapshot = sidecar ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : undefined;
|
|
@@ -797,10 +782,10 @@ export default function (
|
|
|
797
782
|
cwd: ctx.cwd,
|
|
798
783
|
stdio: ["ignore", "pipe", "pipe"],
|
|
799
784
|
});
|
|
800
|
-
const stagedRecord = await
|
|
785
|
+
const stagedRecord = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
801
786
|
if (!stagedRecord.record || stagedRecord.revision !== projection.projection.record_revision)
|
|
802
787
|
throw new Error("TaskRecord changed while preparing the breaking revision");
|
|
803
|
-
stagedNextDiffHash =
|
|
788
|
+
stagedNextDiffHash = taskDeliveryIdentity(ctx.cwd, stagedRecord.record).diff_hash;
|
|
804
789
|
} catch (error) {
|
|
805
790
|
let restoreError: unknown;
|
|
806
791
|
try { restoreStagedIntent(); } catch (err) { restoreError = err; }
|
|
@@ -815,7 +800,7 @@ export default function (
|
|
|
815
800
|
let userDecisionOperation: ReturnType<typeof buildUserDecisionOperation> | undefined;
|
|
816
801
|
if (operation === "resolve-user-decision") {
|
|
817
802
|
try {
|
|
818
|
-
const current = await
|
|
803
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
819
804
|
if (!current.record) throw new Error(`task ${taskId} has no TaskRecord v2`);
|
|
820
805
|
if (current.revision !== projection.projection.record_revision)
|
|
821
806
|
throw new Error("task record changed while preparing user decision");
|
|
@@ -971,11 +956,11 @@ export default function (
|
|
|
971
956
|
if (nextIntent) await dependencies.authorizationAfterSidecarStage?.();
|
|
972
957
|
// Re-read the owner record after confirmation and compare the staged
|
|
973
958
|
// candidate digest immediately before capability issuance.
|
|
974
|
-
const liveRecord = nextIntent ? await
|
|
959
|
+
const liveRecord = nextIntent ? await readTaskRecordRaw(ctx.cwd, taskId) : null;
|
|
975
960
|
if (nextIntent && (!liveRecord?.record || liveRecord.revision !== projection.projection.record_revision || !stagedNextDiffHash))
|
|
976
961
|
throw new Error("TaskRecord changed before the breaking revision digest");
|
|
977
962
|
const operationDiffHash = liveRecord?.record
|
|
978
|
-
?
|
|
963
|
+
? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
|
|
979
964
|
: projection.projection.diff_hash;
|
|
980
965
|
if (nextIntent && operationDiffHash !== stagedNextDiffHash)
|
|
981
966
|
throw new Error("staged next-state diff changed after native confirmation");
|
|
@@ -1003,7 +988,7 @@ export default function (
|
|
|
1003
988
|
task_id: taskId,
|
|
1004
989
|
operation: { ...exactOperation, capability, actor_id: LITERAL_USER_ACTOR_ID } as never,
|
|
1005
990
|
prior_intent_token: priorIntent.token,
|
|
1006
|
-
diffProvider: (root: string, record:
|
|
991
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1007
992
|
now,
|
|
1008
993
|
})) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
|
|
1009
994
|
if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
|
|
@@ -1015,7 +1000,7 @@ export default function (
|
|
|
1015
1000
|
return { state: "applied", operation, lifecycle: result.record.lifecycle };
|
|
1016
1001
|
} catch (error) {
|
|
1017
1002
|
if (stagedSnapshot) {
|
|
1018
|
-
const current = await
|
|
1003
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1019
1004
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) restoreStagedIntent();
|
|
1020
1005
|
}
|
|
1021
1006
|
throw error;
|
|
@@ -1023,7 +1008,7 @@ export default function (
|
|
|
1023
1008
|
} catch (error) {
|
|
1024
1009
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1025
1010
|
if (operation === "stop") {
|
|
1026
|
-
const terminal = await
|
|
1011
|
+
const terminal = await projectAssuranceForTask(ctx.cwd, taskId).catch(() => null);
|
|
1027
1012
|
if (terminal && !terminal.error && terminal.projection.lifecycle === "stopped") {
|
|
1028
1013
|
progression.releaseStoppedReview(taskId);
|
|
1029
1014
|
return { state: "applied", operation, lifecycle: "stopped", delivery_error: reason };
|
|
@@ -1040,11 +1025,11 @@ export default function (
|
|
|
1040
1025
|
if (ctx.mode !== "tui") return { state: "blocked", reason: "imm_kernel_canary mutation is TUI-only" };
|
|
1041
1026
|
if (progression.isInvocationOpen(taskId))
|
|
1042
1027
|
return { state: "blocked", reason: `task ${taskId} already has an open invocation; concurrent assure/authorize is rejected` };
|
|
1043
|
-
const projection = await
|
|
1028
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
1044
1029
|
if (projection.error || !projection.claim)
|
|
1045
1030
|
return { state: "blocked", reason: projection.error ?? "no active backend claim" };
|
|
1046
1031
|
await dependencies.authorizationBeforeRecordRead?.();
|
|
1047
|
-
const read = await
|
|
1032
|
+
const read = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1048
1033
|
if (!read.record) return { state: "blocked", reason: `task ${taskId} has no TaskRecord v3` };
|
|
1049
1034
|
if (read.revision !== projection.projection.record_revision)
|
|
1050
1035
|
return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
|
|
@@ -1108,7 +1093,7 @@ export async function recordCancelledUserDecision(
|
|
|
1108
1093
|
snapshotDigestRef: string,
|
|
1109
1094
|
): Promise<{ recorded: boolean; finding_id: string }> {
|
|
1110
1095
|
const findingId = `user-decision-${operation}`;
|
|
1111
|
-
const current = await
|
|
1096
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1112
1097
|
const openDecision = current.record?.findings.find(
|
|
1113
1098
|
(finding) =>
|
|
1114
1099
|
finding.kind === "unresolved_user_decision" && finding.status === "open",
|
|
@@ -1130,8 +1115,8 @@ export async function recordCancelledUserDecision(
|
|
|
1130
1115
|
},
|
|
1131
1116
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
1132
1117
|
} as never,
|
|
1133
|
-
prior_intent_token: (await
|
|
1134
|
-
diffProvider: (root: string, record:
|
|
1118
|
+
prior_intent_token: (await readTaskIntentForRecord(ctx.cwd, taskId)).token,
|
|
1119
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1135
1120
|
now: new Date().toISOString(),
|
|
1136
1121
|
});
|
|
1137
1122
|
return { recorded: true, finding_id: findingId };
|
|
@@ -1153,24 +1138,36 @@ export function buildUserDecisionOperation(record: {
|
|
|
1153
1138
|
}
|
|
1154
1139
|
|
|
1155
1140
|
/**
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1141
|
+
* Read the TaskIntent through the TaskRecord's `intent_ref.path`.
|
|
1142
|
+
*
|
|
1143
|
+
* `freeze_artifacts` binds the sidecar in place, so a post-freeze read must
|
|
1144
|
+
* follow the record instead of guessing a default path; with no record yet
|
|
1145
|
+
* (pre-enrollment) the Kernel resolves the sidecar that exists. Same contract
|
|
1146
|
+
* as the Claude adapter's `readTaskIntentForRecord`.
|
|
1147
|
+
*/
|
|
1148
|
+
export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskIntentResult {
|
|
1149
|
+
const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref.path;
|
|
1150
|
+
return readTaskIntent(root, taskId, currentPath);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function markGithubTaskTerminal(
|
|
1154
|
+
root: string,
|
|
1155
|
+
input: { task_id: string; phase: "done" | "stopped"; terminal_event_id: string },
|
|
1156
|
+
): Promise<GithubTrackerResult> {
|
|
1157
|
+
return runGithubTrackerOperation(root, { op: "mark-terminal", ...input });
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* The shared delivery identity selector: one function in workspace_scope.ts
|
|
1162
|
+
* picks the identity family per TaskRecord contract. This adapter-level alias
|
|
1163
|
+
* only narrows the record type for the Kernel ports that expect it; the dual-host
|
|
1164
|
+
* conformance seam imports it so both hosts' reportable identity is comparable.
|
|
1159
1165
|
*/
|
|
1160
|
-
function diffSnapshotOf(root: string, record:
|
|
1166
|
+
export function diffSnapshotOf(root: string, record: TaskRecord): {
|
|
1161
1167
|
diff_hash: string;
|
|
1162
1168
|
changed_paths: string[];
|
|
1163
1169
|
} {
|
|
1164
|
-
|
|
1165
|
-
if (!record.git_base_head)
|
|
1166
|
-
throw new Error("TaskRecord v4 is missing git_base_head");
|
|
1167
|
-
return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head, record.task_id);
|
|
1168
|
-
}
|
|
1169
|
-
return taskDiffIdentity(root, record.intent_snapshot.scope_hint, record.task_id);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
function diffHashOf(root: string, record: NonNullable<TaskRecordRead["record"]>): string {
|
|
1173
|
-
return diffSnapshotOf(root, record).diff_hash;
|
|
1170
|
+
return taskDeliveryIdentity(root, record);
|
|
1174
1171
|
}
|
|
1175
1172
|
|
|
1176
1173
|
// Translation-only adapter for the internal Kernel assurance projection. All
|
|
@@ -1190,7 +1187,7 @@ async function buildTaskOverview(root: string): Promise<{
|
|
|
1190
1187
|
const claim = await readBackendClaim(root);
|
|
1191
1188
|
let active: TaskOverviewEntry | null = null;
|
|
1192
1189
|
if (claim) {
|
|
1193
|
-
const projection = await
|
|
1190
|
+
const projection = await projectAssuranceForTask(root, claim.task_id);
|
|
1194
1191
|
if (!projection.error) {
|
|
1195
1192
|
const state = projection.projection;
|
|
1196
1193
|
const obligation = String(state.next_obligation);
|
|
@@ -1241,56 +1238,14 @@ function overviewRailState(lifecycle: string, obligation: string): TaskRailState
|
|
|
1241
1238
|
return "Working";
|
|
1242
1239
|
}
|
|
1243
1240
|
|
|
1244
|
-
async function
|
|
1245
|
-
return projectAssurance(root, taskId
|
|
1241
|
+
export async function projectAssuranceForTask(root: string, taskId: string): Promise<AssuranceProjectionResult> {
|
|
1242
|
+
return projectAssurance(root, taskId);
|
|
1246
1243
|
}
|
|
1247
1244
|
|
|
1248
1245
|
/**
|
|
1249
1246
|
* Publish and prove the task-scoped synthetic revision for a v4 record. v3
|
|
1250
1247
|
* records keep the legacy full-source bundle and return null here.
|
|
1251
1248
|
*/
|
|
1252
|
-
export async function ensureTaskReviewRevision(
|
|
1253
|
-
root: string,
|
|
1254
|
-
taskId: string,
|
|
1255
|
-
projection: AssuranceProjectionResult,
|
|
1256
|
-
): Promise<ReviewRevision | null> {
|
|
1257
|
-
const current = await readTaskRecord(root, taskId);
|
|
1258
|
-
const record = current.record;
|
|
1259
|
-
if (!record) throw new Error(`task ${taskId} has no TaskRecord before Review preparation`);
|
|
1260
|
-
if (current.revision !== projection.projection.record_revision)
|
|
1261
|
-
throw new Error("TaskRecord changed before Review preparation");
|
|
1262
|
-
if (record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
1263
|
-
if (!record.git_base_head)
|
|
1264
|
-
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
1265
|
-
const manifest = captureReviewManifest(root, {
|
|
1266
|
-
taskId,
|
|
1267
|
-
baseHead: record.git_base_head,
|
|
1268
|
-
scopeHint: record.intent_snapshot.scope_hint,
|
|
1269
|
-
expectedDiffHash: projection.projection.diff_hash,
|
|
1270
|
-
intentRevision: projection.projection.intent_revision,
|
|
1271
|
-
intentContentHash: projection.projection.intent_content_hash,
|
|
1272
|
-
recordRevision: projection.projection.record_revision,
|
|
1273
|
-
workspaceRevision: projection.projection.workspace_revision,
|
|
1274
|
-
lifecycle: projection.projection.lifecycle,
|
|
1275
|
-
artifactState: projection.projection.artifact_state,
|
|
1276
|
-
risk: record.intent_snapshot.risk,
|
|
1277
|
-
// The same outcomes the Review snapshot is built from. A preflight stand-in
|
|
1278
|
-
// only matched the settled QA attestation because deterministic QA happens to
|
|
1279
|
-
// write that exact summary, so the submit-time digest comparison held by
|
|
1280
|
-
// coincidence rather than by construction.
|
|
1281
|
-
outcomes: qaOutcomes(record),
|
|
1282
|
-
});
|
|
1283
|
-
return {
|
|
1284
|
-
contract: "assurance_kernel/review_revision/v1",
|
|
1285
|
-
base_head: manifest.base_head,
|
|
1286
|
-
review_tree: manifest.review_tree,
|
|
1287
|
-
review_commit: manifest.review_commit,
|
|
1288
|
-
review_ref: manifest.review_ref,
|
|
1289
|
-
diff_hash: manifest.diff_hash,
|
|
1290
|
-
manifest_digest: manifest.manifest_digest,
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
1249
|
/**
|
|
1295
1250
|
* Review refs are reconstructible evidence transport, never workflow authority.
|
|
1296
1251
|
* A ref survives only while its task owns a nonterminal TaskRecord.
|
|
@@ -1308,7 +1263,7 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
|
|
|
1308
1263
|
for (const entry of listed) {
|
|
1309
1264
|
if (live.has(entry.taskId)) continue;
|
|
1310
1265
|
try {
|
|
1311
|
-
const record = await
|
|
1266
|
+
const record = await readTaskRecordRaw(root, entry.taskId);
|
|
1312
1267
|
if (record.record && record.record.lifecycle === "active" && claim?.task_id === entry.taskId)
|
|
1313
1268
|
live.add(entry.taskId);
|
|
1314
1269
|
} catch {
|
|
@@ -1334,267 +1289,13 @@ function notifyHost(ctx: HostContext, key: string, message: string, level: "warn
|
|
|
1334
1289
|
if (ui) notifyOnce({ ui }, key, message, level);
|
|
1335
1290
|
}
|
|
1336
1291
|
|
|
1337
|
-
async function applyAssuranceVerdict(
|
|
1338
|
-
// The coordinator port hands these a `HostContext`, not the Pi
|
|
1339
|
-
// `ExtensionContext`. Both functions only ever read `cwd`; declaring the
|
|
1340
|
-
// wider host type made the port assignment unsound.
|
|
1341
|
-
ctx: HostContext,
|
|
1342
|
-
snapshot: SnapshotDescriptor,
|
|
1343
|
-
verdict: AssuranceVerdict,
|
|
1344
|
-
invocation: InvocationToken,
|
|
1345
|
-
actorId: string,
|
|
1346
|
-
hooks: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> } = {},
|
|
1347
|
-
authorityKind: "qa" | "review" | "user" = snapshot.role,
|
|
1348
|
-
): Promise<void> {
|
|
1349
|
-
const fresh = await projectAssuranceState(ctx.cwd, snapshot.task_id);
|
|
1350
|
-
if (
|
|
1351
|
-
fresh.error ||
|
|
1352
|
-
fresh.claim?.task_id !== snapshot.task_id ||
|
|
1353
|
-
fresh.projection.record_revision !== snapshot.record_revision ||
|
|
1354
|
-
fresh.projection.workspace_revision !== snapshot.workspace_revision ||
|
|
1355
|
-
fresh.projection.intent_revision !== snapshot.intent_revision ||
|
|
1356
|
-
fresh.projection.intent_content_hash !== snapshot.intent_content_hash ||
|
|
1357
|
-
fresh.projection.diff_hash !== snapshot.diff_hash ||
|
|
1358
|
-
fresh.projection.lifecycle !== snapshot.lifecycle ||
|
|
1359
|
-
fresh.projection.artifact_state !== snapshot.artifact_state
|
|
1360
|
-
) {
|
|
1361
|
-
throw new Error(`assurance snapshot changed before authority application: ${[
|
|
1362
|
-
fresh.error,
|
|
1363
|
-
fresh.claim?.task_id !== snapshot.task_id ? "claim" : null,
|
|
1364
|
-
fresh.projection.record_revision !== snapshot.record_revision ? "record_revision" : null,
|
|
1365
|
-
fresh.projection.workspace_revision !== snapshot.workspace_revision ? "workspace_revision" : null,
|
|
1366
|
-
fresh.projection.intent_revision !== snapshot.intent_revision ? "intent_revision" : null,
|
|
1367
|
-
fresh.projection.intent_content_hash !== snapshot.intent_content_hash ? "intent_content_hash" : null,
|
|
1368
|
-
fresh.projection.diff_hash !== snapshot.diff_hash ? "diff_hash" : null,
|
|
1369
|
-
fresh.projection.lifecycle !== snapshot.lifecycle ? `lifecycle(${snapshot.lifecycle}->${fresh.projection.lifecycle})` : null,
|
|
1370
|
-
fresh.projection.artifact_state !== snapshot.artifact_state ? `artifact_state(${snapshot.artifact_state}->${fresh.projection.artifact_state})` : null,
|
|
1371
|
-
].filter(Boolean).join(", ")}`);
|
|
1372
|
-
}
|
|
1373
|
-
const { registry, app } = await authorityPair();
|
|
1374
|
-
const priorIntentToken = (await readTaskIntent(ctx.cwd, snapshot.task_id)).token;
|
|
1375
|
-
const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
|
|
1376
|
-
invocationRegistry.commit(invocation);
|
|
1377
|
-
const settlement = apply();
|
|
1378
|
-
let hookError: unknown;
|
|
1379
|
-
try { hooks.onCommit?.(); } catch (error) { hookError = error; }
|
|
1380
|
-
const result = await settlement;
|
|
1381
|
-
try { await hooks.afterCommit?.(); } catch (error) { hookError ??= error; }
|
|
1382
|
-
if (hookError) throw hookError;
|
|
1383
|
-
return result;
|
|
1384
|
-
};
|
|
1385
|
-
if (verdict.decision === "rework") {
|
|
1386
|
-
const findings = reviewReworkFindings(verdict);
|
|
1387
|
-
const now = new Date().toISOString();
|
|
1388
|
-
const capability = await mintCapability(registry, {
|
|
1389
|
-
authority_kind: authorityKind,
|
|
1390
|
-
task_id: snapshot.task_id,
|
|
1391
|
-
run_id: snapshot.run_id,
|
|
1392
|
-
action_kind: "request_rework",
|
|
1393
|
-
expected_record_hash: snapshot.record_revision,
|
|
1394
|
-
intent_revision: snapshot.intent_revision,
|
|
1395
|
-
intent_content_hash: snapshot.intent_content_hash,
|
|
1396
|
-
diff_hash: snapshot.diff_hash,
|
|
1397
|
-
actor_id: actorId,
|
|
1398
|
-
findings,
|
|
1399
|
-
now,
|
|
1400
|
-
});
|
|
1401
|
-
await hooks.beforeCommit?.();
|
|
1402
|
-
const result = (await commitAndApply(async () => app.execute({
|
|
1403
|
-
root: ctx.cwd,
|
|
1404
|
-
task_id: snapshot.task_id,
|
|
1405
|
-
operation: {
|
|
1406
|
-
op: "request_rework",
|
|
1407
|
-
capability,
|
|
1408
|
-
findings: findings as never[],
|
|
1409
|
-
actor_id: actorId,
|
|
1410
|
-
},
|
|
1411
|
-
prior_intent_token: priorIntentToken,
|
|
1412
|
-
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
1413
|
-
now,
|
|
1414
|
-
}))) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] }; findings?: Array<{ kind: string; status: string }> } };
|
|
1415
|
-
stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
1416
|
-
const parked = (result.record as { findings?: Array<{ kind: string; status: string }> }).findings?.some(
|
|
1417
|
-
(finding) => finding.kind === "replan_required" && finding.status === "open",
|
|
1418
|
-
);
|
|
1419
|
-
if (parked) notifyHost(
|
|
1420
|
-
ctx,
|
|
1421
|
-
`rework-parked:${snapshot.task_id}`,
|
|
1422
|
-
`rework applied: review parked for replan with ${findings.length} finding(s)`,
|
|
1423
|
-
"warning",
|
|
1424
|
-
);
|
|
1425
|
-
return;
|
|
1426
|
-
}
|
|
1427
|
-
const now = new Date().toISOString();
|
|
1428
|
-
const advisories = verdict.decision === "pass" ? reviewAdvisoryRecords(verdict) : [];
|
|
1429
|
-
const approval = {
|
|
1430
|
-
id: `approval-${snapshot.role}-${randomUUID().slice(0, 8)}`,
|
|
1431
|
-
kind: snapshot.role === "qa" ? "qa" : "review",
|
|
1432
|
-
authority_role: snapshot.role === "qa" ? "qa" : "reviewer",
|
|
1433
|
-
task_revision: snapshot.intent_revision,
|
|
1434
|
-
intent_content_hash: snapshot.intent_content_hash,
|
|
1435
|
-
diff_hash: snapshot.diff_hash,
|
|
1436
|
-
actor_id: actorId,
|
|
1437
|
-
summary: verdict.approval!.summary,
|
|
1438
|
-
// Trusted revision identity comes from the host-verified snapshot, never
|
|
1439
|
-
// from the reviewer payload.
|
|
1440
|
-
...(snapshot.role === "review" && snapshot.review_revision
|
|
1441
|
-
? { review_revision: snapshot.review_revision }
|
|
1442
|
-
: {}),
|
|
1443
|
-
...(snapshot.role === "review" && advisories.length > 0
|
|
1444
|
-
? { advisory_findings: advisories }
|
|
1445
|
-
: {}),
|
|
1446
|
-
};
|
|
1447
|
-
const capability = await mintCapability(registry, {
|
|
1448
|
-
authority_kind: snapshot.role,
|
|
1449
|
-
task_id: snapshot.task_id,
|
|
1450
|
-
run_id: snapshot.run_id,
|
|
1451
|
-
action_kind: "record_approval",
|
|
1452
|
-
expected_record_hash: snapshot.record_revision,
|
|
1453
|
-
intent_revision: snapshot.intent_revision,
|
|
1454
|
-
intent_content_hash: snapshot.intent_content_hash,
|
|
1455
|
-
diff_hash: snapshot.diff_hash,
|
|
1456
|
-
actor_id: actorId,
|
|
1457
|
-
approval,
|
|
1458
|
-
now,
|
|
1459
|
-
});
|
|
1460
|
-
await hooks.beforeCommit?.();
|
|
1461
|
-
await commitAndApply(async () => app.execute({
|
|
1462
|
-
root: ctx.cwd,
|
|
1463
|
-
task_id: snapshot.task_id,
|
|
1464
|
-
operation: { op: "record_approval", capability, approval, actor_id: actorId },
|
|
1465
|
-
prior_intent_token: priorIntentToken,
|
|
1466
|
-
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
1467
|
-
now,
|
|
1468
|
-
}));
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
async function buildAssuranceSnapshot(
|
|
1472
|
-
root: string,
|
|
1473
|
-
taskId: string,
|
|
1474
|
-
role: AssuranceRole,
|
|
1475
|
-
projection: AssuranceProjectionResult,
|
|
1476
|
-
): Promise<{
|
|
1477
|
-
snapshot: SnapshotDescriptor;
|
|
1478
|
-
descriptors: Map<string, VerificationDescriptor>;
|
|
1479
|
-
reviewBundle: ReviewBundle | null;
|
|
1480
|
-
reviewManifest: ReviewManifestV5 | null;
|
|
1481
|
-
}> {
|
|
1482
|
-
const record = await readTaskRecord(root, taskId);
|
|
1483
|
-
if (
|
|
1484
|
-
!record.record ||
|
|
1485
|
-
record.revision !== projection.projection.record_revision ||
|
|
1486
|
-
record.record.intent_snapshot.revision !== projection.projection.intent_revision ||
|
|
1487
|
-
record.record.intent_ref.content_hash !== projection.projection.intent_content_hash
|
|
1488
|
-
) {
|
|
1489
|
-
throw new Error("TaskRecord changed before assurance snapshot capture");
|
|
1490
|
-
}
|
|
1491
|
-
const intent = record.record.intent_snapshot;
|
|
1492
|
-
const acceptance = intent.acceptance;
|
|
1493
|
-
const descriptors = new Map<string, VerificationDescriptor>();
|
|
1494
|
-
// Review resumes from settled outcomes without resolving verification tools.
|
|
1495
|
-
for (const item of role === "qa" ? acceptance : []) {
|
|
1496
|
-
const descriptor = parseVerificationDescriptor(item.verification);
|
|
1497
|
-
descriptors.set(item.id, descriptor);
|
|
1498
|
-
}
|
|
1499
|
-
// `git_base_head` is optional on the read shape because v3 records carry
|
|
1500
|
-
// none, so the contract test alone does not prove it is present.
|
|
1501
|
-
const baseHead = record.record.contract === "assurance_kernel/task_record/v4"
|
|
1502
|
-
? record.record.git_base_head
|
|
1503
|
-
: undefined;
|
|
1504
|
-
if (record.record.contract === "assurance_kernel/task_record/v4" && !baseHead)
|
|
1505
|
-
throw new Error("TaskRecord v4 is missing its Enrollment git_base_head");
|
|
1506
|
-
const reviewRevision = baseHead
|
|
1507
|
-
? {
|
|
1508
|
-
contract: "assurance_kernel/review_revision_identity/v1" as const,
|
|
1509
|
-
base_head: baseHead,
|
|
1510
|
-
review_commit: "",
|
|
1511
|
-
review_tree: "",
|
|
1512
|
-
manifest_digest: "",
|
|
1513
|
-
}
|
|
1514
|
-
: null;
|
|
1515
|
-
const reviewBundle = role === "review" && !reviewRevision
|
|
1516
|
-
? captureReviewBundle(
|
|
1517
|
-
root,
|
|
1518
|
-
intent.scope_hint,
|
|
1519
|
-
projection.projection.diff_hash,
|
|
1520
|
-
qaOutcomes(record.record),
|
|
1521
|
-
)
|
|
1522
|
-
: null;
|
|
1523
|
-
const reviewManifest = role === "review" && reviewRevision
|
|
1524
|
-
? captureReviewManifest(root, {
|
|
1525
|
-
taskId,
|
|
1526
|
-
baseHead: reviewRevision.base_head,
|
|
1527
|
-
scopeHint: intent.scope_hint,
|
|
1528
|
-
expectedDiffHash: projection.projection.diff_hash,
|
|
1529
|
-
intentRevision: projection.projection.intent_revision,
|
|
1530
|
-
intentContentHash: projection.projection.intent_content_hash,
|
|
1531
|
-
recordRevision: projection.projection.record_revision,
|
|
1532
|
-
workspaceRevision: projection.projection.workspace_revision,
|
|
1533
|
-
lifecycle: projection.projection.lifecycle,
|
|
1534
|
-
artifactState: projection.projection.artifact_state,
|
|
1535
|
-
risk: intent.risk,
|
|
1536
|
-
outcomes: qaOutcomes(record.record),
|
|
1537
|
-
})
|
|
1538
|
-
: null;
|
|
1539
|
-
const taskSnapshot = !reviewBundle && !reviewManifest
|
|
1540
|
-
? captureGitTaskSnapshot(root, intent.scope_hint, taskId)
|
|
1541
|
-
: null;
|
|
1542
|
-
const dirtyFiles = reviewManifest
|
|
1543
|
-
? Object.keys(reviewManifest.changed_paths)
|
|
1544
|
-
: reviewBundle
|
|
1545
|
-
? Object.keys(reviewBundle.dirty_files)
|
|
1546
|
-
: Object.keys(taskSnapshot!.staged_files);
|
|
1547
|
-
return {
|
|
1548
|
-
snapshot: buildSnapshot({
|
|
1549
|
-
root,
|
|
1550
|
-
task_id: taskId,
|
|
1551
|
-
role,
|
|
1552
|
-
run_id: projection.projection.run_id,
|
|
1553
|
-
record_revision: projection.projection.record_revision,
|
|
1554
|
-
workspace_revision: projection.projection.workspace_revision,
|
|
1555
|
-
intent_revision: projection.projection.intent_revision,
|
|
1556
|
-
intent_content_hash: projection.projection.intent_content_hash,
|
|
1557
|
-
diff_hash: projection.projection.diff_hash,
|
|
1558
|
-
lifecycle: projection.projection.lifecycle,
|
|
1559
|
-
artifact_state: projection.projection.artifact_state,
|
|
1560
|
-
risk: intent.risk,
|
|
1561
|
-
fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
|
|
1562
|
-
missing_acceptance_ids: projection.projection.missing_acceptance_ids,
|
|
1563
|
-
stale_attestation_ids: projection.projection.stale_attestation_ids,
|
|
1564
|
-
acceptance,
|
|
1565
|
-
dirty_files: dirtyFiles,
|
|
1566
|
-
review_bundle_digest: reviewManifest?.manifest_digest ?? reviewBundle?.bundle_digest ?? null,
|
|
1567
|
-
review_revision: reviewManifest
|
|
1568
|
-
? {
|
|
1569
|
-
contract: "assurance_kernel/review_revision_identity/v1",
|
|
1570
|
-
base_head: reviewManifest.base_head,
|
|
1571
|
-
review_commit: reviewManifest.review_commit,
|
|
1572
|
-
review_tree: reviewManifest.review_tree,
|
|
1573
|
-
manifest_digest: reviewManifest.manifest_digest,
|
|
1574
|
-
}
|
|
1575
|
-
: undefined,
|
|
1576
|
-
}),
|
|
1577
|
-
descriptors,
|
|
1578
|
-
reviewBundle,
|
|
1579
|
-
reviewManifest,
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
function qaOutcomes(
|
|
1584
|
-
record: NonNullable<TaskRecordRead["record"]>,
|
|
1585
|
-
): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
|
|
1586
|
-
return Object.fromEntries(
|
|
1587
|
-
record.attestations
|
|
1588
|
-
.filter((item) => item.kind === "qa")
|
|
1589
|
-
.flatMap((item) => item.acceptance_results)
|
|
1590
|
-
.map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
|
|
1591
|
-
);
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
1292
|
// The invocation registry is shared with the progression module's
|
|
1595
1293
|
// module-scoped registry (see the top-level import above); commit/cancel
|
|
1596
1294
|
// semantics are identical to the previous extension implementation.
|
|
1597
1295
|
|
|
1296
|
+
const piConfirmationReference: ConfirmationReferenceSource = ({ snapshot, now }) =>
|
|
1297
|
+
`pi-confirm-${createHash("sha256").update(`${snapshot.task_id}\0${now}\0${snapshot.intent_revision}\0${snapshot.intent_content_hash}\0${snapshot.diff_hash}`).digest("hex").slice(0, 16)}`;
|
|
1298
|
+
|
|
1598
1299
|
async function mintCapability(
|
|
1599
1300
|
registry: MutationAuthorityRegistry,
|
|
1600
1301
|
input: {
|
|
@@ -1648,7 +1349,6 @@ async function mintCapability(
|
|
|
1648
1349
|
confirmation_ref: `pi-confirm-${createHash("sha256").update(
|
|
1649
1350
|
`${input.task_id}\0${now}\0${input.intent_revision}\0${input.intent_content_hash}\0${input.diff_hash}`,
|
|
1650
1351
|
).digest("hex").slice(0, 16)}`,
|
|
1651
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
1652
1352
|
findings_digest:
|
|
1653
1353
|
input.action_kind === "request_rework"
|
|
1654
1354
|
? await findingsDigestV2(input.findings as never[])
|
|
@@ -1677,7 +1377,7 @@ async function executeOrdinaryOperation(
|
|
|
1677
1377
|
const operation = input.operation.op === "revise_intent"
|
|
1678
1378
|
? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
|
|
1679
1379
|
: input.operation;
|
|
1680
|
-
const priorIntent = await
|
|
1380
|
+
const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
|
|
1681
1381
|
const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
|
|
1682
1382
|
const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
|
|
1683
1383
|
// A content-changing revision writes the sidecar before the kernel's drift
|
|
@@ -1698,7 +1398,7 @@ async function executeOrdinaryOperation(
|
|
|
1698
1398
|
task_id: input.taskId,
|
|
1699
1399
|
operation: operation as never,
|
|
1700
1400
|
prior_intent_token: priorIntent.token,
|
|
1701
|
-
diffProvider: (root: string, record:
|
|
1401
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1702
1402
|
now: new Date().toISOString(),
|
|
1703
1403
|
});
|
|
1704
1404
|
if (operation.op === "freeze_artifacts" || operation.op === "stop")
|
|
@@ -1709,7 +1409,7 @@ async function executeOrdinaryOperation(
|
|
|
1709
1409
|
return result;
|
|
1710
1410
|
} catch (error) {
|
|
1711
1411
|
if (priorBytes) {
|
|
1712
|
-
const current = await
|
|
1412
|
+
const current = await readTaskRecordRaw(ctx.cwd, input.taskId);
|
|
1713
1413
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision && priorStaged) {
|
|
1714
1414
|
restoreStagedIntentShared(ctx.cwd, priorStaged);
|
|
1715
1415
|
}
|
|
@@ -1732,6 +1432,46 @@ async function reconcileRefsQuietly(root: string): Promise<void> {
|
|
|
1732
1432
|
}
|
|
1733
1433
|
}
|
|
1734
1434
|
|
|
1435
|
+
/**
|
|
1436
|
+
* Gate-free closeout: a batch child that reached Kernel `done` in the foreground
|
|
1437
|
+
* re-enters the batch in the same call, reusing its authorization. Only `done`
|
|
1438
|
+
* continues; stopped, parked and Review-pending children end where they do
|
|
1439
|
+
* today. The continuation is transport: the Kernel result is returned
|
|
1440
|
+
* unchanged, and any failure is reported beside it with one retry action.
|
|
1441
|
+
*/
|
|
1442
|
+
async function closeOutBatchChild(
|
|
1443
|
+
ctx: ExtensionContext,
|
|
1444
|
+
taskId: string,
|
|
1445
|
+
enriched: Record<string, unknown>,
|
|
1446
|
+
): Promise<Record<string, unknown>> {
|
|
1447
|
+
const taskState = enriched.task_state as { lifecycle?: unknown } | undefined;
|
|
1448
|
+
if (taskState?.lifecycle !== "done") return enriched;
|
|
1449
|
+
let slug: string | null;
|
|
1450
|
+
try {
|
|
1451
|
+
slug = findResumableBatchSlugForTask(ctx.cwd, taskId);
|
|
1452
|
+
} catch {
|
|
1453
|
+
return enriched;
|
|
1454
|
+
}
|
|
1455
|
+
if (!slug) return enriched;
|
|
1456
|
+
try {
|
|
1457
|
+
// The batch commit takes exactly the scope envelope plus this task's own
|
|
1458
|
+
// audit evidence and refuses unstaged changes: stage only that evidence.
|
|
1459
|
+
if (existsSync(join(ctx.cwd, ".imm", "audit", taskId)))
|
|
1460
|
+
execFileSync("git", ["-C", ctx.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
|
|
1461
|
+
const batch = await executePiUnattendedBatch({ root: ctx.cwd, initiativeSlug: slug, reuseOnly: true, signal: ctx.signal });
|
|
1462
|
+
return { ...enriched, batch };
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
return {
|
|
1465
|
+
...enriched,
|
|
1466
|
+
batch: {
|
|
1467
|
+
state: "rejected",
|
|
1468
|
+
reason: `batch continuation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1469
|
+
recovery_action: "call start_unattended_batch with the same Initiative to continue the batch",
|
|
1470
|
+
},
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1735
1475
|
async function enrichAssuranceResult(
|
|
1736
1476
|
ctx: ExtensionContext,
|
|
1737
1477
|
taskId: string,
|
|
@@ -1742,7 +1482,7 @@ async function enrichAssuranceResult(
|
|
|
1742
1482
|
const taskState = { error: typeof result.reason === "string" ? result.reason : "authority projection unavailable" };
|
|
1743
1483
|
return { ...result, task_state: taskState, next_action: nextActionForAssuranceResult(result, taskState) };
|
|
1744
1484
|
}
|
|
1745
|
-
const projection = await
|
|
1485
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
1746
1486
|
const taskState: AssuranceTaskState = projection.error
|
|
1747
1487
|
? { error: projection.error }
|
|
1748
1488
|
: projection.projection;
|
|
@@ -1843,33 +1583,6 @@ function prepareActionArgs<Params>(args: unknown): Params {
|
|
|
1843
1583
|
return input as Params;
|
|
1844
1584
|
}
|
|
1845
1585
|
|
|
1846
|
-
function stagePlanningArtifactTransition(root: string, record: {
|
|
1847
|
-
intent_ref: { path: string };
|
|
1848
|
-
intent_snapshot: { scope_hint: string[] };
|
|
1849
|
-
}): void {
|
|
1850
|
-
const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
|
|
1851
|
-
const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
|
|
1852
|
-
const specActive = record.intent_snapshot.scope_hint.find((path) =>
|
|
1853
|
-
/^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
|
|
1854
|
-
&& record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
|
|
1855
|
-
);
|
|
1856
|
-
const candidates = [
|
|
1857
|
-
intentActive,
|
|
1858
|
-
intentArchive,
|
|
1859
|
-
...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
|
|
1860
|
-
];
|
|
1861
|
-
const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync(
|
|
1862
|
-
"git",
|
|
1863
|
-
["ls-files", "--cached", "--", path],
|
|
1864
|
-
{ cwd: root, encoding: "utf8" },
|
|
1865
|
-
).trim().length > 0);
|
|
1866
|
-
if (paths.length === 0) return;
|
|
1867
|
-
execFileSync("git", ["add", "--", ...paths], {
|
|
1868
|
-
cwd: root,
|
|
1869
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
1586
|
function failCanaryTool(
|
|
1874
1587
|
taskId: string,
|
|
1875
1588
|
operation: string,
|