immune-brain 4.6.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 +154 -37
- 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 -44
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +142 -474
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +54 -84
- 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 +7369 -7016
- 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 +23 -8
- 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} +33 -7
- 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 +83 -67
- 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/verdict_authority.ts +347 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +62 -369
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +2 -0
- package/plugins/immune-brain/runtime/claude/review_host.ts +48 -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/kernel/application.ts +6 -3
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +5 -3
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +149 -5
- 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/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 +57 -27
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +12 -14
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +34 -29
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +26 -90
- package/plugins/immune-brain/runtime/workspace_scope.ts +34 -0
- 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 -581
|
@@ -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
|
//
|
|
@@ -24,18 +28,11 @@ import { PLUGIN_VERSION } from "../runtime/plugin_version";
|
|
|
24
28
|
import { findResumableBatchSlugForTask } from "../runtime/unattended/batch_preflight";
|
|
25
29
|
import { executePiUnattendedBatch } from "./imm-unattended-batch";
|
|
26
30
|
import {
|
|
27
|
-
parseVerificationDescriptor,
|
|
28
|
-
type VerificationDescriptor,
|
|
29
31
|
} from "./pi-canary-verification";
|
|
30
32
|
import {
|
|
31
|
-
captureReviewBundle,
|
|
32
|
-
captureReviewManifest,
|
|
33
33
|
listReviewRefs,
|
|
34
34
|
reconcileReviewRefs,
|
|
35
35
|
writeNativeReviewEvidence,
|
|
36
|
-
type ReviewBundle,
|
|
37
|
-
type ReviewManifestV5,
|
|
38
|
-
type ReviewRevision,
|
|
39
36
|
} from "./pi-canary-review-bundle";
|
|
40
37
|
import type { InvocationToken } from "./pi-canary-invocations";
|
|
41
38
|
import { runDeterministicQa } from "../runtime/assurance/qa";
|
|
@@ -46,7 +43,6 @@ import {
|
|
|
46
43
|
import {
|
|
47
44
|
renderCanaryCall,
|
|
48
45
|
renderCanaryResult,
|
|
49
|
-
type AssuranceRole,
|
|
50
46
|
} from "./pi-canary-assurance";
|
|
51
47
|
import {
|
|
52
48
|
USER_ATTENTION_EVENT,
|
|
@@ -71,8 +67,7 @@ import { resolveUxLanguage, uxText } from "./ux-language";
|
|
|
71
67
|
|
|
72
68
|
/** Host-native UI language; see ux-language.ts. Resolved once per process. */
|
|
73
69
|
const UX_LANG = resolveUxLanguage();
|
|
74
|
-
import {
|
|
75
|
-
import { reviewAdvisoryRecords, reviewReworkFindings } from "../runtime/assurance/coordinator";
|
|
70
|
+
import { taskDeliveryIdentity } from "../runtime/workspace_scope";
|
|
76
71
|
import {
|
|
77
72
|
AssuranceProgression,
|
|
78
73
|
buildReviewPrompt,
|
|
@@ -87,44 +82,54 @@ import {
|
|
|
87
82
|
REVIEW_TIMING_PROFILES,
|
|
88
83
|
REVIEW_VERDICT_VALIDATION_TIMEOUT_MS,
|
|
89
84
|
type AssuranceProgressionPorts,
|
|
90
|
-
type AssuranceVerdict,
|
|
91
85
|
type HostContext,
|
|
92
|
-
type SnapshotDescriptor,
|
|
93
86
|
} from "./pi-canary-assurance-progression";
|
|
94
87
|
|
|
95
|
-
// The
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
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";
|
|
99
94
|
import {
|
|
100
|
-
createMutationAuthorityRegistry,
|
|
101
|
-
createCanaryApplication,
|
|
102
|
-
buildLoopAction,
|
|
103
|
-
buildLoopRoleDispatch,
|
|
104
|
-
readBackendClaim,
|
|
105
|
-
readTaskTombstone,
|
|
106
|
-
markGithubTaskTerminal,
|
|
107
95
|
reconcileKernelAuthority,
|
|
108
96
|
repairKernelAuthority,
|
|
109
|
-
|
|
97
|
+
readTaskRecordRaw,
|
|
110
98
|
withKernelStoreLockForTask,
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
} from "../runtime/kernel/storage";
|
|
100
|
+
import { inspectStorageLayout } from "../runtime/kernel/storage_paths";
|
|
101
|
+
import { migrateLegacyLayout } from "../runtime/kernel/storage_layout_migration";
|
|
102
|
+
import {
|
|
113
103
|
readTaskIntent,
|
|
114
104
|
parseTaskIntentV1,
|
|
115
105
|
canonicalIntentHash,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
type ReadTaskIntentResult,
|
|
107
|
+
} from "../runtime/kernel/intent";
|
|
108
|
+
import { readBackendClaim, readTaskTombstone } from "../runtime/kernel/backend_claim";
|
|
109
|
+
import {
|
|
110
|
+
createMutationAuthorityRegistry,
|
|
119
111
|
digestOfAction,
|
|
120
|
-
type AssuranceProjectionResult,
|
|
121
|
-
type TaskRecordRead,
|
|
122
|
-
type CanaryApplication,
|
|
123
|
-
type MutationAuthorityRegistry,
|
|
124
112
|
type CapabilityBindingV2,
|
|
125
|
-
|
|
126
|
-
} from "
|
|
127
|
-
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";
|
|
128
133
|
|
|
129
134
|
const LOOP_OWNERS = ["plan", "kernel", "brainstorm", "planner", "loop"] as const;
|
|
130
135
|
const LOOP_TARGETS = [
|
|
@@ -178,60 +183,6 @@ export type AuthorizeOperation =
|
|
|
178
183
|
| "authorize-rework"
|
|
179
184
|
| "stop";
|
|
180
185
|
|
|
181
|
-
export interface SnapshotDescriptorInput {
|
|
182
|
-
root: string;
|
|
183
|
-
task_id: string;
|
|
184
|
-
role: AssuranceRole;
|
|
185
|
-
run_id?: string | null;
|
|
186
|
-
record_revision: string;
|
|
187
|
-
workspace_revision: string;
|
|
188
|
-
intent_revision: number;
|
|
189
|
-
intent_content_hash: string;
|
|
190
|
-
diff_hash: string;
|
|
191
|
-
lifecycle: string;
|
|
192
|
-
artifact_state: string;
|
|
193
|
-
risk?: "routine" | "material" | "critical";
|
|
194
|
-
fresh_acceptance_ids: string[];
|
|
195
|
-
missing_acceptance_ids: string[];
|
|
196
|
-
stale_attestation_ids: string[];
|
|
197
|
-
acceptance: Array<{ id: string; assertion: string; verification: string }>;
|
|
198
|
-
dirty_files?: string[];
|
|
199
|
-
review_bundle_digest?: string | null;
|
|
200
|
-
/** Present only when Review authority binds an immutable Git revision (v4). */
|
|
201
|
-
review_revision?: {
|
|
202
|
-
contract: "assurance_kernel/review_revision_identity/v1";
|
|
203
|
-
base_head: string;
|
|
204
|
-
review_commit: string;
|
|
205
|
-
review_tree: string;
|
|
206
|
-
manifest_digest: string;
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function buildSnapshot(input: SnapshotDescriptorInput): SnapshotDescriptor {
|
|
211
|
-
return {
|
|
212
|
-
contract: "assurance_kernel/assurance_snapshot/v2",
|
|
213
|
-
task_id: input.task_id,
|
|
214
|
-
run_id: input.run_id ?? null,
|
|
215
|
-
role: input.role,
|
|
216
|
-
record_revision: input.record_revision,
|
|
217
|
-
workspace_revision: input.workspace_revision,
|
|
218
|
-
intent_revision: input.intent_revision,
|
|
219
|
-
intent_content_hash: input.intent_content_hash,
|
|
220
|
-
diff_hash: input.diff_hash,
|
|
221
|
-
lifecycle: input.lifecycle,
|
|
222
|
-
artifact_state: input.artifact_state,
|
|
223
|
-
risk: input.risk ?? "material",
|
|
224
|
-
fresh_acceptance_ids: input.fresh_acceptance_ids,
|
|
225
|
-
missing_acceptance_ids: input.missing_acceptance_ids,
|
|
226
|
-
stale_attestation_ids: input.stale_attestation_ids,
|
|
227
|
-
acceptance: input.acceptance,
|
|
228
|
-
dirty_files: [...(input.dirty_files ?? [])].sort(),
|
|
229
|
-
review_bundle_digest: input.review_bundle_digest ?? null,
|
|
230
|
-
...(input.review_revision ? { review_revision: input.review_revision } : {}),
|
|
231
|
-
root: resolve(input.root),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
186
|
export interface CanaryWorkExtensionDependencies {
|
|
236
187
|
buildAssurance?: typeof buildAssuranceSnapshot;
|
|
237
188
|
runQa?: typeof runDeterministicQa;
|
|
@@ -277,25 +228,15 @@ export function createPiAssuranceProgressionPorts(
|
|
|
277
228
|
dependencies: CanaryWorkExtensionDependencies = {},
|
|
278
229
|
): AssuranceProgressionPorts {
|
|
279
230
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
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),
|
|
286
236
|
runQa: (snapshot, descriptors, options) =>
|
|
287
237
|
(dependencies.runQa ?? runDeterministicQa)(snapshot, descriptors, options),
|
|
288
238
|
writeReviewEvidence: (input) =>
|
|
289
239
|
(dependencies.writeReviewEvidence ?? writeNativeReviewEvidence)(input),
|
|
290
|
-
applyVerdict: (ctx, input) =>
|
|
291
|
-
applyAssuranceVerdict(
|
|
292
|
-
ctx,
|
|
293
|
-
input.snapshot,
|
|
294
|
-
input.verdict,
|
|
295
|
-
input.invocation,
|
|
296
|
-
input.actorId,
|
|
297
|
-
input.hooks,
|
|
298
|
-
),
|
|
299
240
|
applyOrdinaryOperation: (ctx, input) => executeOrdinaryOperation(ctx, input),
|
|
300
241
|
advanceBeforeProjection: dependencies.advanceBeforeProjection,
|
|
301
242
|
qaBeforeProjection: dependencies.qaBeforeProjection,
|
|
@@ -303,11 +244,46 @@ export function createPiAssuranceProgressionPorts(
|
|
|
303
244
|
qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
|
|
304
245
|
qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
|
|
305
246
|
qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
|
|
247
|
+
...(dependencies.buildAssurance ? { authorityOverrides: { buildAssurance: dependencies.buildAssurance } } : {}),
|
|
306
248
|
} satisfies AssuranceProgressionPorts;
|
|
307
249
|
}
|
|
308
250
|
|
|
309
251
|
const GLOBAL_PI_PROGRESSION_KEY = Symbol.for("immune_brain.pi_assurance_progression");
|
|
310
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
|
+
|
|
311
287
|
export default function (
|
|
312
288
|
pi: ExtensionAPI,
|
|
313
289
|
dependencies: CanaryWorkExtensionDependencies = {},
|
|
@@ -323,7 +299,7 @@ export default function (
|
|
|
323
299
|
try {
|
|
324
300
|
const claim = await readBackendClaim(ctx.cwd);
|
|
325
301
|
if (!claim) return;
|
|
326
|
-
const projection = await
|
|
302
|
+
const projection = await projectAssuranceForTask(ctx.cwd, claim.task_id);
|
|
327
303
|
if (projection.error) {
|
|
328
304
|
presentTaskRail(ctx, {
|
|
329
305
|
task_id: claim.task_id,
|
|
@@ -592,7 +568,7 @@ export default function (
|
|
|
592
568
|
throwIfCanaryToolFailure(taskId, action.op, enriched);
|
|
593
569
|
return toolResult(JSON.stringify(enriched, null, 2), enriched);
|
|
594
570
|
}
|
|
595
|
-
const projection = await
|
|
571
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
596
572
|
if (projection.error) {
|
|
597
573
|
const nextAction = recoveryActionForAssuranceFailure(projection.error) ?? "inspect authority state";
|
|
598
574
|
const details = {
|
|
@@ -634,7 +610,7 @@ export default function (
|
|
|
634
610
|
taskId,
|
|
635
611
|
operation: toCanaryOperation(action, "executor") as { op: string; actor_id: string },
|
|
636
612
|
})) as unknown as { revision: string; record: { lifecycle: string; artifact_state: string } };
|
|
637
|
-
const updated = await
|
|
613
|
+
const updated = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
638
614
|
const taskState = updated.error
|
|
639
615
|
? { lifecycle: result.record.lifecycle, artifact_state: result.record.artifact_state }
|
|
640
616
|
: updated.projection;
|
|
@@ -710,15 +686,18 @@ export default function (
|
|
|
710
686
|
_ctx: ExtensionContext,
|
|
711
687
|
) => {
|
|
712
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.
|
|
713
692
|
const result = action.op === "route"
|
|
714
693
|
? await buildLoopAction({
|
|
715
694
|
ownership: action.ownership,
|
|
716
695
|
target: action.target,
|
|
717
|
-
context: action.context,
|
|
696
|
+
context: action.context as RoleDelegationContext | undefined,
|
|
718
697
|
scope_expansion: action.scope_expansion,
|
|
719
|
-
kernel_operation: action.kernel_operation,
|
|
698
|
+
kernel_operation: action.kernel_operation as Parameters<typeof buildLoopAction>[0]["kernel_operation"],
|
|
720
699
|
})
|
|
721
|
-
: await buildLoopRoleDispatch({ role: action.role, context: action.context });
|
|
700
|
+
: await buildLoopRoleDispatch({ role: action.role, context: action.context as RoleDelegationContext });
|
|
722
701
|
const details = loopResultDetails(result, action.op);
|
|
723
702
|
return toolResult(JSON.stringify(result, null, 2), details);
|
|
724
703
|
},
|
|
@@ -775,14 +754,14 @@ export default function (
|
|
|
775
754
|
return { state: "blocked", reason };
|
|
776
755
|
}
|
|
777
756
|
try {
|
|
778
|
-
const projection = await
|
|
757
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
779
758
|
if (projection.error || !projection.claim) {
|
|
780
759
|
const reason = projection.error ?? "no active backend claim";
|
|
781
760
|
notifyOnce(ctx, `authorization-claim:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
|
|
782
761
|
progression.closeInvocation(invocation);
|
|
783
762
|
return { state: "blocked", reason };
|
|
784
763
|
}
|
|
785
|
-
const priorIntent = await
|
|
764
|
+
const priorIntent = await readTaskIntentForRecord(ctx.cwd, taskId);
|
|
786
765
|
const sidecar = nextIntent ? join(ctx.cwd, priorIntent.intent_ref.path) : undefined;
|
|
787
766
|
const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
|
|
788
767
|
const stagedSnapshot = sidecar ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : undefined;
|
|
@@ -803,10 +782,10 @@ export default function (
|
|
|
803
782
|
cwd: ctx.cwd,
|
|
804
783
|
stdio: ["ignore", "pipe", "pipe"],
|
|
805
784
|
});
|
|
806
|
-
const stagedRecord = await
|
|
785
|
+
const stagedRecord = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
807
786
|
if (!stagedRecord.record || stagedRecord.revision !== projection.projection.record_revision)
|
|
808
787
|
throw new Error("TaskRecord changed while preparing the breaking revision");
|
|
809
|
-
stagedNextDiffHash =
|
|
788
|
+
stagedNextDiffHash = taskDeliveryIdentity(ctx.cwd, stagedRecord.record).diff_hash;
|
|
810
789
|
} catch (error) {
|
|
811
790
|
let restoreError: unknown;
|
|
812
791
|
try { restoreStagedIntent(); } catch (err) { restoreError = err; }
|
|
@@ -821,7 +800,7 @@ export default function (
|
|
|
821
800
|
let userDecisionOperation: ReturnType<typeof buildUserDecisionOperation> | undefined;
|
|
822
801
|
if (operation === "resolve-user-decision") {
|
|
823
802
|
try {
|
|
824
|
-
const current = await
|
|
803
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
825
804
|
if (!current.record) throw new Error(`task ${taskId} has no TaskRecord v2`);
|
|
826
805
|
if (current.revision !== projection.projection.record_revision)
|
|
827
806
|
throw new Error("task record changed while preparing user decision");
|
|
@@ -977,11 +956,11 @@ export default function (
|
|
|
977
956
|
if (nextIntent) await dependencies.authorizationAfterSidecarStage?.();
|
|
978
957
|
// Re-read the owner record after confirmation and compare the staged
|
|
979
958
|
// candidate digest immediately before capability issuance.
|
|
980
|
-
const liveRecord = nextIntent ? await
|
|
959
|
+
const liveRecord = nextIntent ? await readTaskRecordRaw(ctx.cwd, taskId) : null;
|
|
981
960
|
if (nextIntent && (!liveRecord?.record || liveRecord.revision !== projection.projection.record_revision || !stagedNextDiffHash))
|
|
982
961
|
throw new Error("TaskRecord changed before the breaking revision digest");
|
|
983
962
|
const operationDiffHash = liveRecord?.record
|
|
984
|
-
?
|
|
963
|
+
? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
|
|
985
964
|
: projection.projection.diff_hash;
|
|
986
965
|
if (nextIntent && operationDiffHash !== stagedNextDiffHash)
|
|
987
966
|
throw new Error("staged next-state diff changed after native confirmation");
|
|
@@ -1009,7 +988,7 @@ export default function (
|
|
|
1009
988
|
task_id: taskId,
|
|
1010
989
|
operation: { ...exactOperation, capability, actor_id: LITERAL_USER_ACTOR_ID } as never,
|
|
1011
990
|
prior_intent_token: priorIntent.token,
|
|
1012
|
-
diffProvider: (root: string, record:
|
|
991
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1013
992
|
now,
|
|
1014
993
|
})) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
|
|
1015
994
|
if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
|
|
@@ -1021,7 +1000,7 @@ export default function (
|
|
|
1021
1000
|
return { state: "applied", operation, lifecycle: result.record.lifecycle };
|
|
1022
1001
|
} catch (error) {
|
|
1023
1002
|
if (stagedSnapshot) {
|
|
1024
|
-
const current = await
|
|
1003
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1025
1004
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) restoreStagedIntent();
|
|
1026
1005
|
}
|
|
1027
1006
|
throw error;
|
|
@@ -1029,7 +1008,7 @@ export default function (
|
|
|
1029
1008
|
} catch (error) {
|
|
1030
1009
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1031
1010
|
if (operation === "stop") {
|
|
1032
|
-
const terminal = await
|
|
1011
|
+
const terminal = await projectAssuranceForTask(ctx.cwd, taskId).catch(() => null);
|
|
1033
1012
|
if (terminal && !terminal.error && terminal.projection.lifecycle === "stopped") {
|
|
1034
1013
|
progression.releaseStoppedReview(taskId);
|
|
1035
1014
|
return { state: "applied", operation, lifecycle: "stopped", delivery_error: reason };
|
|
@@ -1046,11 +1025,11 @@ export default function (
|
|
|
1046
1025
|
if (ctx.mode !== "tui") return { state: "blocked", reason: "imm_kernel_canary mutation is TUI-only" };
|
|
1047
1026
|
if (progression.isInvocationOpen(taskId))
|
|
1048
1027
|
return { state: "blocked", reason: `task ${taskId} already has an open invocation; concurrent assure/authorize is rejected` };
|
|
1049
|
-
const projection = await
|
|
1028
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
1050
1029
|
if (projection.error || !projection.claim)
|
|
1051
1030
|
return { state: "blocked", reason: projection.error ?? "no active backend claim" };
|
|
1052
1031
|
await dependencies.authorizationBeforeRecordRead?.();
|
|
1053
|
-
const read = await
|
|
1032
|
+
const read = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1054
1033
|
if (!read.record) return { state: "blocked", reason: `task ${taskId} has no TaskRecord v3` };
|
|
1055
1034
|
if (read.revision !== projection.projection.record_revision)
|
|
1056
1035
|
return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
|
|
@@ -1114,7 +1093,7 @@ export async function recordCancelledUserDecision(
|
|
|
1114
1093
|
snapshotDigestRef: string,
|
|
1115
1094
|
): Promise<{ recorded: boolean; finding_id: string }> {
|
|
1116
1095
|
const findingId = `user-decision-${operation}`;
|
|
1117
|
-
const current = await
|
|
1096
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1118
1097
|
const openDecision = current.record?.findings.find(
|
|
1119
1098
|
(finding) =>
|
|
1120
1099
|
finding.kind === "unresolved_user_decision" && finding.status === "open",
|
|
@@ -1136,8 +1115,8 @@ export async function recordCancelledUserDecision(
|
|
|
1136
1115
|
},
|
|
1137
1116
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
1138
1117
|
} as never,
|
|
1139
|
-
prior_intent_token: (await
|
|
1140
|
-
diffProvider: (root: string, record:
|
|
1118
|
+
prior_intent_token: (await readTaskIntentForRecord(ctx.cwd, taskId)).token,
|
|
1119
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1141
1120
|
now: new Date().toISOString(),
|
|
1142
1121
|
});
|
|
1143
1122
|
return { recorded: true, finding_id: findingId };
|
|
@@ -1159,24 +1138,36 @@ export function buildUserDecisionOperation(record: {
|
|
|
1159
1138
|
}
|
|
1160
1139
|
|
|
1161
1140
|
/**
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
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.
|
|
1165
1165
|
*/
|
|
1166
|
-
function diffSnapshotOf(root: string, record:
|
|
1166
|
+
export function diffSnapshotOf(root: string, record: TaskRecord): {
|
|
1167
1167
|
diff_hash: string;
|
|
1168
1168
|
changed_paths: string[];
|
|
1169
1169
|
} {
|
|
1170
|
-
|
|
1171
|
-
if (!record.git_base_head)
|
|
1172
|
-
throw new Error("TaskRecord v4 is missing git_base_head");
|
|
1173
|
-
return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head, record.task_id);
|
|
1174
|
-
}
|
|
1175
|
-
return taskDiffIdentity(root, record.intent_snapshot.scope_hint, record.task_id);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function diffHashOf(root: string, record: NonNullable<TaskRecordRead["record"]>): string {
|
|
1179
|
-
return diffSnapshotOf(root, record).diff_hash;
|
|
1170
|
+
return taskDeliveryIdentity(root, record);
|
|
1180
1171
|
}
|
|
1181
1172
|
|
|
1182
1173
|
// Translation-only adapter for the internal Kernel assurance projection. All
|
|
@@ -1196,7 +1187,7 @@ async function buildTaskOverview(root: string): Promise<{
|
|
|
1196
1187
|
const claim = await readBackendClaim(root);
|
|
1197
1188
|
let active: TaskOverviewEntry | null = null;
|
|
1198
1189
|
if (claim) {
|
|
1199
|
-
const projection = await
|
|
1190
|
+
const projection = await projectAssuranceForTask(root, claim.task_id);
|
|
1200
1191
|
if (!projection.error) {
|
|
1201
1192
|
const state = projection.projection;
|
|
1202
1193
|
const obligation = String(state.next_obligation);
|
|
@@ -1247,56 +1238,14 @@ function overviewRailState(lifecycle: string, obligation: string): TaskRailState
|
|
|
1247
1238
|
return "Working";
|
|
1248
1239
|
}
|
|
1249
1240
|
|
|
1250
|
-
async function
|
|
1251
|
-
return projectAssurance(root, taskId
|
|
1241
|
+
export async function projectAssuranceForTask(root: string, taskId: string): Promise<AssuranceProjectionResult> {
|
|
1242
|
+
return projectAssurance(root, taskId);
|
|
1252
1243
|
}
|
|
1253
1244
|
|
|
1254
1245
|
/**
|
|
1255
1246
|
* Publish and prove the task-scoped synthetic revision for a v4 record. v3
|
|
1256
1247
|
* records keep the legacy full-source bundle and return null here.
|
|
1257
1248
|
*/
|
|
1258
|
-
export async function ensureTaskReviewRevision(
|
|
1259
|
-
root: string,
|
|
1260
|
-
taskId: string,
|
|
1261
|
-
projection: AssuranceProjectionResult,
|
|
1262
|
-
): Promise<ReviewRevision | null> {
|
|
1263
|
-
const current = await readTaskRecord(root, taskId);
|
|
1264
|
-
const record = current.record;
|
|
1265
|
-
if (!record) throw new Error(`task ${taskId} has no TaskRecord before Review preparation`);
|
|
1266
|
-
if (current.revision !== projection.projection.record_revision)
|
|
1267
|
-
throw new Error("TaskRecord changed before Review preparation");
|
|
1268
|
-
if (record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
1269
|
-
if (!record.git_base_head)
|
|
1270
|
-
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
1271
|
-
const manifest = captureReviewManifest(root, {
|
|
1272
|
-
taskId,
|
|
1273
|
-
baseHead: record.git_base_head,
|
|
1274
|
-
scopeHint: record.intent_snapshot.scope_hint,
|
|
1275
|
-
expectedDiffHash: projection.projection.diff_hash,
|
|
1276
|
-
intentRevision: projection.projection.intent_revision,
|
|
1277
|
-
intentContentHash: projection.projection.intent_content_hash,
|
|
1278
|
-
recordRevision: projection.projection.record_revision,
|
|
1279
|
-
workspaceRevision: projection.projection.workspace_revision,
|
|
1280
|
-
lifecycle: projection.projection.lifecycle,
|
|
1281
|
-
artifactState: projection.projection.artifact_state,
|
|
1282
|
-
risk: record.intent_snapshot.risk,
|
|
1283
|
-
// The same outcomes the Review snapshot is built from. A preflight stand-in
|
|
1284
|
-
// only matched the settled QA attestation because deterministic QA happens to
|
|
1285
|
-
// write that exact summary, so the submit-time digest comparison held by
|
|
1286
|
-
// coincidence rather than by construction.
|
|
1287
|
-
outcomes: qaOutcomes(record),
|
|
1288
|
-
});
|
|
1289
|
-
return {
|
|
1290
|
-
contract: "assurance_kernel/review_revision/v1",
|
|
1291
|
-
base_head: manifest.base_head,
|
|
1292
|
-
review_tree: manifest.review_tree,
|
|
1293
|
-
review_commit: manifest.review_commit,
|
|
1294
|
-
review_ref: manifest.review_ref,
|
|
1295
|
-
diff_hash: manifest.diff_hash,
|
|
1296
|
-
manifest_digest: manifest.manifest_digest,
|
|
1297
|
-
};
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
1249
|
/**
|
|
1301
1250
|
* Review refs are reconstructible evidence transport, never workflow authority.
|
|
1302
1251
|
* A ref survives only while its task owns a nonterminal TaskRecord.
|
|
@@ -1314,7 +1263,7 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
|
|
|
1314
1263
|
for (const entry of listed) {
|
|
1315
1264
|
if (live.has(entry.taskId)) continue;
|
|
1316
1265
|
try {
|
|
1317
|
-
const record = await
|
|
1266
|
+
const record = await readTaskRecordRaw(root, entry.taskId);
|
|
1318
1267
|
if (record.record && record.record.lifecycle === "active" && claim?.task_id === entry.taskId)
|
|
1319
1268
|
live.add(entry.taskId);
|
|
1320
1269
|
} catch {
|
|
@@ -1340,267 +1289,13 @@ function notifyHost(ctx: HostContext, key: string, message: string, level: "warn
|
|
|
1340
1289
|
if (ui) notifyOnce({ ui }, key, message, level);
|
|
1341
1290
|
}
|
|
1342
1291
|
|
|
1343
|
-
async function applyAssuranceVerdict(
|
|
1344
|
-
// The coordinator port hands these a `HostContext`, not the Pi
|
|
1345
|
-
// `ExtensionContext`. Both functions only ever read `cwd`; declaring the
|
|
1346
|
-
// wider host type made the port assignment unsound.
|
|
1347
|
-
ctx: HostContext,
|
|
1348
|
-
snapshot: SnapshotDescriptor,
|
|
1349
|
-
verdict: AssuranceVerdict,
|
|
1350
|
-
invocation: InvocationToken,
|
|
1351
|
-
actorId: string,
|
|
1352
|
-
hooks: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> } = {},
|
|
1353
|
-
authorityKind: "qa" | "review" | "user" = snapshot.role,
|
|
1354
|
-
): Promise<void> {
|
|
1355
|
-
const fresh = await projectAssuranceState(ctx.cwd, snapshot.task_id);
|
|
1356
|
-
if (
|
|
1357
|
-
fresh.error ||
|
|
1358
|
-
fresh.claim?.task_id !== snapshot.task_id ||
|
|
1359
|
-
fresh.projection.record_revision !== snapshot.record_revision ||
|
|
1360
|
-
fresh.projection.workspace_revision !== snapshot.workspace_revision ||
|
|
1361
|
-
fresh.projection.intent_revision !== snapshot.intent_revision ||
|
|
1362
|
-
fresh.projection.intent_content_hash !== snapshot.intent_content_hash ||
|
|
1363
|
-
fresh.projection.diff_hash !== snapshot.diff_hash ||
|
|
1364
|
-
fresh.projection.lifecycle !== snapshot.lifecycle ||
|
|
1365
|
-
fresh.projection.artifact_state !== snapshot.artifact_state
|
|
1366
|
-
) {
|
|
1367
|
-
throw new Error(`assurance snapshot changed before authority application: ${[
|
|
1368
|
-
fresh.error,
|
|
1369
|
-
fresh.claim?.task_id !== snapshot.task_id ? "claim" : null,
|
|
1370
|
-
fresh.projection.record_revision !== snapshot.record_revision ? "record_revision" : null,
|
|
1371
|
-
fresh.projection.workspace_revision !== snapshot.workspace_revision ? "workspace_revision" : null,
|
|
1372
|
-
fresh.projection.intent_revision !== snapshot.intent_revision ? "intent_revision" : null,
|
|
1373
|
-
fresh.projection.intent_content_hash !== snapshot.intent_content_hash ? "intent_content_hash" : null,
|
|
1374
|
-
fresh.projection.diff_hash !== snapshot.diff_hash ? "diff_hash" : null,
|
|
1375
|
-
fresh.projection.lifecycle !== snapshot.lifecycle ? `lifecycle(${snapshot.lifecycle}->${fresh.projection.lifecycle})` : null,
|
|
1376
|
-
fresh.projection.artifact_state !== snapshot.artifact_state ? `artifact_state(${snapshot.artifact_state}->${fresh.projection.artifact_state})` : null,
|
|
1377
|
-
].filter(Boolean).join(", ")}`);
|
|
1378
|
-
}
|
|
1379
|
-
const { registry, app } = await authorityPair();
|
|
1380
|
-
const priorIntentToken = (await readTaskIntent(ctx.cwd, snapshot.task_id)).token;
|
|
1381
|
-
const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
|
|
1382
|
-
invocationRegistry.commit(invocation);
|
|
1383
|
-
const settlement = apply();
|
|
1384
|
-
let hookError: unknown;
|
|
1385
|
-
try { hooks.onCommit?.(); } catch (error) { hookError = error; }
|
|
1386
|
-
const result = await settlement;
|
|
1387
|
-
try { await hooks.afterCommit?.(); } catch (error) { hookError ??= error; }
|
|
1388
|
-
if (hookError) throw hookError;
|
|
1389
|
-
return result;
|
|
1390
|
-
};
|
|
1391
|
-
if (verdict.decision === "rework") {
|
|
1392
|
-
const findings = reviewReworkFindings(verdict);
|
|
1393
|
-
const now = new Date().toISOString();
|
|
1394
|
-
const capability = await mintCapability(registry, {
|
|
1395
|
-
authority_kind: authorityKind,
|
|
1396
|
-
task_id: snapshot.task_id,
|
|
1397
|
-
run_id: snapshot.run_id,
|
|
1398
|
-
action_kind: "request_rework",
|
|
1399
|
-
expected_record_hash: snapshot.record_revision,
|
|
1400
|
-
intent_revision: snapshot.intent_revision,
|
|
1401
|
-
intent_content_hash: snapshot.intent_content_hash,
|
|
1402
|
-
diff_hash: snapshot.diff_hash,
|
|
1403
|
-
actor_id: actorId,
|
|
1404
|
-
findings,
|
|
1405
|
-
now,
|
|
1406
|
-
});
|
|
1407
|
-
await hooks.beforeCommit?.();
|
|
1408
|
-
const result = (await commitAndApply(async () => app.execute({
|
|
1409
|
-
root: ctx.cwd,
|
|
1410
|
-
task_id: snapshot.task_id,
|
|
1411
|
-
operation: {
|
|
1412
|
-
op: "request_rework",
|
|
1413
|
-
capability,
|
|
1414
|
-
findings: findings as never[],
|
|
1415
|
-
actor_id: actorId,
|
|
1416
|
-
},
|
|
1417
|
-
prior_intent_token: priorIntentToken,
|
|
1418
|
-
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
1419
|
-
now,
|
|
1420
|
-
}))) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] }; findings?: Array<{ kind: string; status: string }> } };
|
|
1421
|
-
stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
1422
|
-
const parked = (result.record as { findings?: Array<{ kind: string; status: string }> }).findings?.some(
|
|
1423
|
-
(finding) => finding.kind === "replan_required" && finding.status === "open",
|
|
1424
|
-
);
|
|
1425
|
-
if (parked) notifyHost(
|
|
1426
|
-
ctx,
|
|
1427
|
-
`rework-parked:${snapshot.task_id}`,
|
|
1428
|
-
`rework applied: review parked for replan with ${findings.length} finding(s)`,
|
|
1429
|
-
"warning",
|
|
1430
|
-
);
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
const now = new Date().toISOString();
|
|
1434
|
-
const advisories = verdict.decision === "pass" ? reviewAdvisoryRecords(verdict) : [];
|
|
1435
|
-
const approval = {
|
|
1436
|
-
id: `approval-${snapshot.role}-${randomUUID().slice(0, 8)}`,
|
|
1437
|
-
kind: snapshot.role === "qa" ? "qa" : "review",
|
|
1438
|
-
authority_role: snapshot.role === "qa" ? "qa" : "reviewer",
|
|
1439
|
-
task_revision: snapshot.intent_revision,
|
|
1440
|
-
intent_content_hash: snapshot.intent_content_hash,
|
|
1441
|
-
diff_hash: snapshot.diff_hash,
|
|
1442
|
-
actor_id: actorId,
|
|
1443
|
-
summary: verdict.approval!.summary,
|
|
1444
|
-
// Trusted revision identity comes from the host-verified snapshot, never
|
|
1445
|
-
// from the reviewer payload.
|
|
1446
|
-
...(snapshot.role === "review" && snapshot.review_revision
|
|
1447
|
-
? { review_revision: snapshot.review_revision }
|
|
1448
|
-
: {}),
|
|
1449
|
-
...(snapshot.role === "review" && advisories.length > 0
|
|
1450
|
-
? { advisory_findings: advisories }
|
|
1451
|
-
: {}),
|
|
1452
|
-
};
|
|
1453
|
-
const capability = await mintCapability(registry, {
|
|
1454
|
-
authority_kind: snapshot.role,
|
|
1455
|
-
task_id: snapshot.task_id,
|
|
1456
|
-
run_id: snapshot.run_id,
|
|
1457
|
-
action_kind: "record_approval",
|
|
1458
|
-
expected_record_hash: snapshot.record_revision,
|
|
1459
|
-
intent_revision: snapshot.intent_revision,
|
|
1460
|
-
intent_content_hash: snapshot.intent_content_hash,
|
|
1461
|
-
diff_hash: snapshot.diff_hash,
|
|
1462
|
-
actor_id: actorId,
|
|
1463
|
-
approval,
|
|
1464
|
-
now,
|
|
1465
|
-
});
|
|
1466
|
-
await hooks.beforeCommit?.();
|
|
1467
|
-
await commitAndApply(async () => app.execute({
|
|
1468
|
-
root: ctx.cwd,
|
|
1469
|
-
task_id: snapshot.task_id,
|
|
1470
|
-
operation: { op: "record_approval", capability, approval, actor_id: actorId },
|
|
1471
|
-
prior_intent_token: priorIntentToken,
|
|
1472
|
-
diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
|
|
1473
|
-
now,
|
|
1474
|
-
}));
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
async function buildAssuranceSnapshot(
|
|
1478
|
-
root: string,
|
|
1479
|
-
taskId: string,
|
|
1480
|
-
role: AssuranceRole,
|
|
1481
|
-
projection: AssuranceProjectionResult,
|
|
1482
|
-
): Promise<{
|
|
1483
|
-
snapshot: SnapshotDescriptor;
|
|
1484
|
-
descriptors: Map<string, VerificationDescriptor>;
|
|
1485
|
-
reviewBundle: ReviewBundle | null;
|
|
1486
|
-
reviewManifest: ReviewManifestV5 | null;
|
|
1487
|
-
}> {
|
|
1488
|
-
const record = await readTaskRecord(root, taskId);
|
|
1489
|
-
if (
|
|
1490
|
-
!record.record ||
|
|
1491
|
-
record.revision !== projection.projection.record_revision ||
|
|
1492
|
-
record.record.intent_snapshot.revision !== projection.projection.intent_revision ||
|
|
1493
|
-
record.record.intent_ref.content_hash !== projection.projection.intent_content_hash
|
|
1494
|
-
) {
|
|
1495
|
-
throw new Error("TaskRecord changed before assurance snapshot capture");
|
|
1496
|
-
}
|
|
1497
|
-
const intent = record.record.intent_snapshot;
|
|
1498
|
-
const acceptance = intent.acceptance;
|
|
1499
|
-
const descriptors = new Map<string, VerificationDescriptor>();
|
|
1500
|
-
// Review resumes from settled outcomes without resolving verification tools.
|
|
1501
|
-
for (const item of role === "qa" ? acceptance : []) {
|
|
1502
|
-
const descriptor = parseVerificationDescriptor(item.verification);
|
|
1503
|
-
descriptors.set(item.id, descriptor);
|
|
1504
|
-
}
|
|
1505
|
-
// `git_base_head` is optional on the read shape because v3 records carry
|
|
1506
|
-
// none, so the contract test alone does not prove it is present.
|
|
1507
|
-
const baseHead = record.record.contract === "assurance_kernel/task_record/v4"
|
|
1508
|
-
? record.record.git_base_head
|
|
1509
|
-
: undefined;
|
|
1510
|
-
if (record.record.contract === "assurance_kernel/task_record/v4" && !baseHead)
|
|
1511
|
-
throw new Error("TaskRecord v4 is missing its Enrollment git_base_head");
|
|
1512
|
-
const reviewRevision = baseHead
|
|
1513
|
-
? {
|
|
1514
|
-
contract: "assurance_kernel/review_revision_identity/v1" as const,
|
|
1515
|
-
base_head: baseHead,
|
|
1516
|
-
review_commit: "",
|
|
1517
|
-
review_tree: "",
|
|
1518
|
-
manifest_digest: "",
|
|
1519
|
-
}
|
|
1520
|
-
: null;
|
|
1521
|
-
const reviewBundle = role === "review" && !reviewRevision
|
|
1522
|
-
? captureReviewBundle(
|
|
1523
|
-
root,
|
|
1524
|
-
intent.scope_hint,
|
|
1525
|
-
projection.projection.diff_hash,
|
|
1526
|
-
qaOutcomes(record.record),
|
|
1527
|
-
)
|
|
1528
|
-
: null;
|
|
1529
|
-
const reviewManifest = role === "review" && reviewRevision
|
|
1530
|
-
? captureReviewManifest(root, {
|
|
1531
|
-
taskId,
|
|
1532
|
-
baseHead: reviewRevision.base_head,
|
|
1533
|
-
scopeHint: intent.scope_hint,
|
|
1534
|
-
expectedDiffHash: projection.projection.diff_hash,
|
|
1535
|
-
intentRevision: projection.projection.intent_revision,
|
|
1536
|
-
intentContentHash: projection.projection.intent_content_hash,
|
|
1537
|
-
recordRevision: projection.projection.record_revision,
|
|
1538
|
-
workspaceRevision: projection.projection.workspace_revision,
|
|
1539
|
-
lifecycle: projection.projection.lifecycle,
|
|
1540
|
-
artifactState: projection.projection.artifact_state,
|
|
1541
|
-
risk: intent.risk,
|
|
1542
|
-
outcomes: qaOutcomes(record.record),
|
|
1543
|
-
})
|
|
1544
|
-
: null;
|
|
1545
|
-
const taskSnapshot = !reviewBundle && !reviewManifest
|
|
1546
|
-
? captureGitTaskSnapshot(root, intent.scope_hint, taskId)
|
|
1547
|
-
: null;
|
|
1548
|
-
const dirtyFiles = reviewManifest
|
|
1549
|
-
? Object.keys(reviewManifest.changed_paths)
|
|
1550
|
-
: reviewBundle
|
|
1551
|
-
? Object.keys(reviewBundle.dirty_files)
|
|
1552
|
-
: Object.keys(taskSnapshot!.staged_files);
|
|
1553
|
-
return {
|
|
1554
|
-
snapshot: buildSnapshot({
|
|
1555
|
-
root,
|
|
1556
|
-
task_id: taskId,
|
|
1557
|
-
role,
|
|
1558
|
-
run_id: projection.projection.run_id,
|
|
1559
|
-
record_revision: projection.projection.record_revision,
|
|
1560
|
-
workspace_revision: projection.projection.workspace_revision,
|
|
1561
|
-
intent_revision: projection.projection.intent_revision,
|
|
1562
|
-
intent_content_hash: projection.projection.intent_content_hash,
|
|
1563
|
-
diff_hash: projection.projection.diff_hash,
|
|
1564
|
-
lifecycle: projection.projection.lifecycle,
|
|
1565
|
-
artifact_state: projection.projection.artifact_state,
|
|
1566
|
-
risk: intent.risk,
|
|
1567
|
-
fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
|
|
1568
|
-
missing_acceptance_ids: projection.projection.missing_acceptance_ids,
|
|
1569
|
-
stale_attestation_ids: projection.projection.stale_attestation_ids,
|
|
1570
|
-
acceptance,
|
|
1571
|
-
dirty_files: dirtyFiles,
|
|
1572
|
-
review_bundle_digest: reviewManifest?.manifest_digest ?? reviewBundle?.bundle_digest ?? null,
|
|
1573
|
-
review_revision: reviewManifest
|
|
1574
|
-
? {
|
|
1575
|
-
contract: "assurance_kernel/review_revision_identity/v1",
|
|
1576
|
-
base_head: reviewManifest.base_head,
|
|
1577
|
-
review_commit: reviewManifest.review_commit,
|
|
1578
|
-
review_tree: reviewManifest.review_tree,
|
|
1579
|
-
manifest_digest: reviewManifest.manifest_digest,
|
|
1580
|
-
}
|
|
1581
|
-
: undefined,
|
|
1582
|
-
}),
|
|
1583
|
-
descriptors,
|
|
1584
|
-
reviewBundle,
|
|
1585
|
-
reviewManifest,
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
function qaOutcomes(
|
|
1590
|
-
record: NonNullable<TaskRecordRead["record"]>,
|
|
1591
|
-
): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
|
|
1592
|
-
return Object.fromEntries(
|
|
1593
|
-
record.attestations
|
|
1594
|
-
.filter((item) => item.kind === "qa")
|
|
1595
|
-
.flatMap((item) => item.acceptance_results)
|
|
1596
|
-
.map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
|
|
1597
|
-
);
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
1292
|
// The invocation registry is shared with the progression module's
|
|
1601
1293
|
// module-scoped registry (see the top-level import above); commit/cancel
|
|
1602
1294
|
// semantics are identical to the previous extension implementation.
|
|
1603
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
|
+
|
|
1604
1299
|
async function mintCapability(
|
|
1605
1300
|
registry: MutationAuthorityRegistry,
|
|
1606
1301
|
input: {
|
|
@@ -1682,7 +1377,7 @@ async function executeOrdinaryOperation(
|
|
|
1682
1377
|
const operation = input.operation.op === "revise_intent"
|
|
1683
1378
|
? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
|
|
1684
1379
|
: input.operation;
|
|
1685
|
-
const priorIntent = await
|
|
1380
|
+
const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
|
|
1686
1381
|
const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
|
|
1687
1382
|
const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
|
|
1688
1383
|
// A content-changing revision writes the sidecar before the kernel's drift
|
|
@@ -1703,7 +1398,7 @@ async function executeOrdinaryOperation(
|
|
|
1703
1398
|
task_id: input.taskId,
|
|
1704
1399
|
operation: operation as never,
|
|
1705
1400
|
prior_intent_token: priorIntent.token,
|
|
1706
|
-
diffProvider: (root: string, record:
|
|
1401
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1707
1402
|
now: new Date().toISOString(),
|
|
1708
1403
|
});
|
|
1709
1404
|
if (operation.op === "freeze_artifacts" || operation.op === "stop")
|
|
@@ -1714,7 +1409,7 @@ async function executeOrdinaryOperation(
|
|
|
1714
1409
|
return result;
|
|
1715
1410
|
} catch (error) {
|
|
1716
1411
|
if (priorBytes) {
|
|
1717
|
-
const current = await
|
|
1412
|
+
const current = await readTaskRecordRaw(ctx.cwd, input.taskId);
|
|
1718
1413
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision && priorStaged) {
|
|
1719
1414
|
restoreStagedIntentShared(ctx.cwd, priorStaged);
|
|
1720
1415
|
}
|
|
@@ -1787,7 +1482,7 @@ async function enrichAssuranceResult(
|
|
|
1787
1482
|
const taskState = { error: typeof result.reason === "string" ? result.reason : "authority projection unavailable" };
|
|
1788
1483
|
return { ...result, task_state: taskState, next_action: nextActionForAssuranceResult(result, taskState) };
|
|
1789
1484
|
}
|
|
1790
|
-
const projection = await
|
|
1485
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
1791
1486
|
const taskState: AssuranceTaskState = projection.error
|
|
1792
1487
|
? { error: projection.error }
|
|
1793
1488
|
: projection.projection;
|
|
@@ -1888,33 +1583,6 @@ function prepareActionArgs<Params>(args: unknown): Params {
|
|
|
1888
1583
|
return input as Params;
|
|
1889
1584
|
}
|
|
1890
1585
|
|
|
1891
|
-
function stagePlanningArtifactTransition(root: string, record: {
|
|
1892
|
-
intent_ref: { path: string };
|
|
1893
|
-
intent_snapshot: { scope_hint: string[] };
|
|
1894
|
-
}): void {
|
|
1895
|
-
const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
|
|
1896
|
-
const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
|
|
1897
|
-
const specActive = record.intent_snapshot.scope_hint.find((path) =>
|
|
1898
|
-
/^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
|
|
1899
|
-
&& record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
|
|
1900
|
-
);
|
|
1901
|
-
const candidates = [
|
|
1902
|
-
intentActive,
|
|
1903
|
-
intentArchive,
|
|
1904
|
-
...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
|
|
1905
|
-
];
|
|
1906
|
-
const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync(
|
|
1907
|
-
"git",
|
|
1908
|
-
["ls-files", "--cached", "--", path],
|
|
1909
|
-
{ cwd: root, encoding: "utf8" },
|
|
1910
|
-
).trim().length > 0);
|
|
1911
|
-
if (paths.length === 0) return;
|
|
1912
|
-
execFileSync("git", ["add", "--", ...paths], {
|
|
1913
|
-
cwd: root,
|
|
1914
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1915
|
-
});
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
1586
|
function failCanaryTool(
|
|
1919
1587
|
taskId: string,
|
|
1920
1588
|
operation: string,
|