immune-brain 4.6.0 → 5.1.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 +374 -46
- package/README.zh-CN.md +384 -47
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +42 -44
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +143 -474
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +78 -87
- 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 +8601 -6975
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
- 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} +231 -10
- 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/dist/role-prompts/lane-steward.md +44 -0
- 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/interaction.ts +6 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +78 -373
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +26 -2
- package/plugins/immune-brain/runtime/claude/review_host.ts +48 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +84 -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/local_initiative.ts +94 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +77 -24
- 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/prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +42 -2
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +57 -27
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +909 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +53 -36
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +34 -29
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +46 -91
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +356 -8
- package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
- 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 +11 -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 = [
|
|
@@ -134,6 +139,7 @@ const LOOP_TARGETS = [
|
|
|
134
139
|
"architecture-exploration",
|
|
135
140
|
"advisory-review",
|
|
136
141
|
"compounder",
|
|
142
|
+
"lane-supply",
|
|
137
143
|
] as const;
|
|
138
144
|
const LOOP_DIRECT_ROLES = ["qa", "code-review", "ui-review"] as const;
|
|
139
145
|
const KERNEL_OPERATIONS = [
|
|
@@ -178,60 +184,6 @@ export type AuthorizeOperation =
|
|
|
178
184
|
| "authorize-rework"
|
|
179
185
|
| "stop";
|
|
180
186
|
|
|
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
187
|
export interface CanaryWorkExtensionDependencies {
|
|
236
188
|
buildAssurance?: typeof buildAssuranceSnapshot;
|
|
237
189
|
runQa?: typeof runDeterministicQa;
|
|
@@ -277,25 +229,15 @@ export function createPiAssuranceProgressionPorts(
|
|
|
277
229
|
dependencies: CanaryWorkExtensionDependencies = {},
|
|
278
230
|
): AssuranceProgressionPorts {
|
|
279
231
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
ensureReviewRevision: (root, taskId, projection) => ensureTaskReviewRevision(root, taskId, projection),
|
|
232
|
+
confirmationReference: piConfirmationReference,
|
|
233
|
+
onReworkApplied: (ctx, taskId, count) => notifyHost(ctx, `rework-parked:${taskId}`, `rework applied: review parked for replan with ${count} finding(s)`, "warning"),
|
|
234
|
+
projectTask: (root, taskId) => projectAssuranceForTask(root, taskId),
|
|
235
|
+
readTaskRecord: async (root, taskId) => readTaskRecordRaw(root, taskId),
|
|
236
|
+
readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
286
237
|
runQa: (snapshot, descriptors, options) =>
|
|
287
238
|
(dependencies.runQa ?? runDeterministicQa)(snapshot, descriptors, options),
|
|
288
239
|
writeReviewEvidence: (input) =>
|
|
289
240
|
(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
241
|
applyOrdinaryOperation: (ctx, input) => executeOrdinaryOperation(ctx, input),
|
|
300
242
|
advanceBeforeProjection: dependencies.advanceBeforeProjection,
|
|
301
243
|
qaBeforeProjection: dependencies.qaBeforeProjection,
|
|
@@ -303,11 +245,46 @@ export function createPiAssuranceProgressionPorts(
|
|
|
303
245
|
qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
|
|
304
246
|
qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
|
|
305
247
|
qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
|
|
248
|
+
...(dependencies.buildAssurance ? { authorityOverrides: { buildAssurance: dependencies.buildAssurance } } : {}),
|
|
306
249
|
} satisfies AssuranceProgressionPorts;
|
|
307
250
|
}
|
|
308
251
|
|
|
309
252
|
const GLOBAL_PI_PROGRESSION_KEY = Symbol.for("immune_brain.pi_assurance_progression");
|
|
310
253
|
|
|
254
|
+
/**
|
|
255
|
+
* The shared session progression the batch adapter advances children through.
|
|
256
|
+
* The work extension publishes the instance at load; this accessor is the
|
|
257
|
+
* lazy fallback that builds one from the production ports when no extension
|
|
258
|
+
* factory has run (tests, direct batch drives). Both paths publish under the
|
|
259
|
+
* same global key, so a batch Review reservation and the session's
|
|
260
|
+
* submit_review always share one progression instance.
|
|
261
|
+
*/
|
|
262
|
+
export async function getSharedPiProgression(): Promise<AssuranceProgression> {
|
|
263
|
+
let progression = (globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] as AssuranceProgression | undefined;
|
|
264
|
+
if (!progression) {
|
|
265
|
+
progression = new AssuranceProgression(createPiAssuranceProgressionPorts());
|
|
266
|
+
(globalThis as any)[GLOBAL_PI_PROGRESSION_KEY] = progression;
|
|
267
|
+
}
|
|
268
|
+
return progression;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Advance one task through the shared session progression and translate the
|
|
273
|
+
* progression result into the batch runner's advance report shape.
|
|
274
|
+
*/
|
|
275
|
+
export async function advancePiTask(root: string, taskId: string): Promise<any> {
|
|
276
|
+
const progression = await getSharedPiProgression();
|
|
277
|
+
const result = await progression.advance(taskId, { cwd: root });
|
|
278
|
+
const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
|
|
279
|
+
if (result.state === "completed") return { state: "completed", ...facts };
|
|
280
|
+
if (result.state === "stopped") return { state: "stopped", ...facts };
|
|
281
|
+
if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
|
|
282
|
+
if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, agent_params: result.agent_params, ...facts };
|
|
283
|
+
if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
|
|
284
|
+
if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
|
|
285
|
+
return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
|
|
286
|
+
}
|
|
287
|
+
|
|
311
288
|
export default function (
|
|
312
289
|
pi: ExtensionAPI,
|
|
313
290
|
dependencies: CanaryWorkExtensionDependencies = {},
|
|
@@ -323,7 +300,7 @@ export default function (
|
|
|
323
300
|
try {
|
|
324
301
|
const claim = await readBackendClaim(ctx.cwd);
|
|
325
302
|
if (!claim) return;
|
|
326
|
-
const projection = await
|
|
303
|
+
const projection = await projectAssuranceForTask(ctx.cwd, claim.task_id);
|
|
327
304
|
if (projection.error) {
|
|
328
305
|
presentTaskRail(ctx, {
|
|
329
306
|
task_id: claim.task_id,
|
|
@@ -592,7 +569,7 @@ export default function (
|
|
|
592
569
|
throwIfCanaryToolFailure(taskId, action.op, enriched);
|
|
593
570
|
return toolResult(JSON.stringify(enriched, null, 2), enriched);
|
|
594
571
|
}
|
|
595
|
-
const projection = await
|
|
572
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
596
573
|
if (projection.error) {
|
|
597
574
|
const nextAction = recoveryActionForAssuranceFailure(projection.error) ?? "inspect authority state";
|
|
598
575
|
const details = {
|
|
@@ -634,7 +611,7 @@ export default function (
|
|
|
634
611
|
taskId,
|
|
635
612
|
operation: toCanaryOperation(action, "executor") as { op: string; actor_id: string },
|
|
636
613
|
})) as unknown as { revision: string; record: { lifecycle: string; artifact_state: string } };
|
|
637
|
-
const updated = await
|
|
614
|
+
const updated = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
638
615
|
const taskState = updated.error
|
|
639
616
|
? { lifecycle: result.record.lifecycle, artifact_state: result.record.artifact_state }
|
|
640
617
|
: updated.projection;
|
|
@@ -710,15 +687,18 @@ export default function (
|
|
|
710
687
|
_ctx: ExtensionContext,
|
|
711
688
|
) => {
|
|
712
689
|
const { action } = params;
|
|
690
|
+
// The Tool schema is intentionally broader than loop_contract's input
|
|
691
|
+
// types (model-supplied JSON); loop_contract narrows and validates at
|
|
692
|
+
// runtime, so these boundary casts are type-level only.
|
|
713
693
|
const result = action.op === "route"
|
|
714
694
|
? await buildLoopAction({
|
|
715
695
|
ownership: action.ownership,
|
|
716
696
|
target: action.target,
|
|
717
|
-
context: action.context,
|
|
697
|
+
context: action.context as RoleDelegationContext | undefined,
|
|
718
698
|
scope_expansion: action.scope_expansion,
|
|
719
|
-
kernel_operation: action.kernel_operation,
|
|
699
|
+
kernel_operation: action.kernel_operation as Parameters<typeof buildLoopAction>[0]["kernel_operation"],
|
|
720
700
|
})
|
|
721
|
-
: await buildLoopRoleDispatch({ role: action.role, context: action.context });
|
|
701
|
+
: await buildLoopRoleDispatch({ role: action.role, context: action.context as RoleDelegationContext });
|
|
722
702
|
const details = loopResultDetails(result, action.op);
|
|
723
703
|
return toolResult(JSON.stringify(result, null, 2), details);
|
|
724
704
|
},
|
|
@@ -775,14 +755,14 @@ export default function (
|
|
|
775
755
|
return { state: "blocked", reason };
|
|
776
756
|
}
|
|
777
757
|
try {
|
|
778
|
-
const projection = await
|
|
758
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
779
759
|
if (projection.error || !projection.claim) {
|
|
780
760
|
const reason = projection.error ?? "no active backend claim";
|
|
781
761
|
notifyOnce(ctx, `authorization-claim:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
|
|
782
762
|
progression.closeInvocation(invocation);
|
|
783
763
|
return { state: "blocked", reason };
|
|
784
764
|
}
|
|
785
|
-
const priorIntent = await
|
|
765
|
+
const priorIntent = await readTaskIntentForRecord(ctx.cwd, taskId);
|
|
786
766
|
const sidecar = nextIntent ? join(ctx.cwd, priorIntent.intent_ref.path) : undefined;
|
|
787
767
|
const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
|
|
788
768
|
const stagedSnapshot = sidecar ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : undefined;
|
|
@@ -803,10 +783,10 @@ export default function (
|
|
|
803
783
|
cwd: ctx.cwd,
|
|
804
784
|
stdio: ["ignore", "pipe", "pipe"],
|
|
805
785
|
});
|
|
806
|
-
const stagedRecord = await
|
|
786
|
+
const stagedRecord = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
807
787
|
if (!stagedRecord.record || stagedRecord.revision !== projection.projection.record_revision)
|
|
808
788
|
throw new Error("TaskRecord changed while preparing the breaking revision");
|
|
809
|
-
stagedNextDiffHash =
|
|
789
|
+
stagedNextDiffHash = taskDeliveryIdentity(ctx.cwd, stagedRecord.record).diff_hash;
|
|
810
790
|
} catch (error) {
|
|
811
791
|
let restoreError: unknown;
|
|
812
792
|
try { restoreStagedIntent(); } catch (err) { restoreError = err; }
|
|
@@ -821,7 +801,7 @@ export default function (
|
|
|
821
801
|
let userDecisionOperation: ReturnType<typeof buildUserDecisionOperation> | undefined;
|
|
822
802
|
if (operation === "resolve-user-decision") {
|
|
823
803
|
try {
|
|
824
|
-
const current = await
|
|
804
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
825
805
|
if (!current.record) throw new Error(`task ${taskId} has no TaskRecord v2`);
|
|
826
806
|
if (current.revision !== projection.projection.record_revision)
|
|
827
807
|
throw new Error("task record changed while preparing user decision");
|
|
@@ -977,11 +957,11 @@ export default function (
|
|
|
977
957
|
if (nextIntent) await dependencies.authorizationAfterSidecarStage?.();
|
|
978
958
|
// Re-read the owner record after confirmation and compare the staged
|
|
979
959
|
// candidate digest immediately before capability issuance.
|
|
980
|
-
const liveRecord = nextIntent ? await
|
|
960
|
+
const liveRecord = nextIntent ? await readTaskRecordRaw(ctx.cwd, taskId) : null;
|
|
981
961
|
if (nextIntent && (!liveRecord?.record || liveRecord.revision !== projection.projection.record_revision || !stagedNextDiffHash))
|
|
982
962
|
throw new Error("TaskRecord changed before the breaking revision digest");
|
|
983
963
|
const operationDiffHash = liveRecord?.record
|
|
984
|
-
?
|
|
964
|
+
? taskDeliveryIdentity(ctx.cwd, liveRecord.record).diff_hash
|
|
985
965
|
: projection.projection.diff_hash;
|
|
986
966
|
if (nextIntent && operationDiffHash !== stagedNextDiffHash)
|
|
987
967
|
throw new Error("staged next-state diff changed after native confirmation");
|
|
@@ -1009,7 +989,7 @@ export default function (
|
|
|
1009
989
|
task_id: taskId,
|
|
1010
990
|
operation: { ...exactOperation, capability, actor_id: LITERAL_USER_ACTOR_ID } as never,
|
|
1011
991
|
prior_intent_token: priorIntent.token,
|
|
1012
|
-
diffProvider: (root: string, record:
|
|
992
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1013
993
|
now,
|
|
1014
994
|
})) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
|
|
1015
995
|
if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
|
|
@@ -1021,7 +1001,7 @@ export default function (
|
|
|
1021
1001
|
return { state: "applied", operation, lifecycle: result.record.lifecycle };
|
|
1022
1002
|
} catch (error) {
|
|
1023
1003
|
if (stagedSnapshot) {
|
|
1024
|
-
const current = await
|
|
1004
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1025
1005
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) restoreStagedIntent();
|
|
1026
1006
|
}
|
|
1027
1007
|
throw error;
|
|
@@ -1029,7 +1009,7 @@ export default function (
|
|
|
1029
1009
|
} catch (error) {
|
|
1030
1010
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1031
1011
|
if (operation === "stop") {
|
|
1032
|
-
const terminal = await
|
|
1012
|
+
const terminal = await projectAssuranceForTask(ctx.cwd, taskId).catch(() => null);
|
|
1033
1013
|
if (terminal && !terminal.error && terminal.projection.lifecycle === "stopped") {
|
|
1034
1014
|
progression.releaseStoppedReview(taskId);
|
|
1035
1015
|
return { state: "applied", operation, lifecycle: "stopped", delivery_error: reason };
|
|
@@ -1046,11 +1026,11 @@ export default function (
|
|
|
1046
1026
|
if (ctx.mode !== "tui") return { state: "blocked", reason: "imm_kernel_canary mutation is TUI-only" };
|
|
1047
1027
|
if (progression.isInvocationOpen(taskId))
|
|
1048
1028
|
return { state: "blocked", reason: `task ${taskId} already has an open invocation; concurrent assure/authorize is rejected` };
|
|
1049
|
-
const projection = await
|
|
1029
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
1050
1030
|
if (projection.error || !projection.claim)
|
|
1051
1031
|
return { state: "blocked", reason: projection.error ?? "no active backend claim" };
|
|
1052
1032
|
await dependencies.authorizationBeforeRecordRead?.();
|
|
1053
|
-
const read = await
|
|
1033
|
+
const read = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1054
1034
|
if (!read.record) return { state: "blocked", reason: `task ${taskId} has no TaskRecord v3` };
|
|
1055
1035
|
if (read.revision !== projection.projection.record_revision)
|
|
1056
1036
|
return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
|
|
@@ -1114,7 +1094,7 @@ export async function recordCancelledUserDecision(
|
|
|
1114
1094
|
snapshotDigestRef: string,
|
|
1115
1095
|
): Promise<{ recorded: boolean; finding_id: string }> {
|
|
1116
1096
|
const findingId = `user-decision-${operation}`;
|
|
1117
|
-
const current = await
|
|
1097
|
+
const current = await readTaskRecordRaw(ctx.cwd, taskId);
|
|
1118
1098
|
const openDecision = current.record?.findings.find(
|
|
1119
1099
|
(finding) =>
|
|
1120
1100
|
finding.kind === "unresolved_user_decision" && finding.status === "open",
|
|
@@ -1136,8 +1116,8 @@ export async function recordCancelledUserDecision(
|
|
|
1136
1116
|
},
|
|
1137
1117
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
1138
1118
|
} as never,
|
|
1139
|
-
prior_intent_token: (await
|
|
1140
|
-
diffProvider: (root: string, record:
|
|
1119
|
+
prior_intent_token: (await readTaskIntentForRecord(ctx.cwd, taskId)).token,
|
|
1120
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1141
1121
|
now: new Date().toISOString(),
|
|
1142
1122
|
});
|
|
1143
1123
|
return { recorded: true, finding_id: findingId };
|
|
@@ -1159,24 +1139,36 @@ export function buildUserDecisionOperation(record: {
|
|
|
1159
1139
|
}
|
|
1160
1140
|
|
|
1161
1141
|
/**
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1142
|
+
* Read the TaskIntent through the TaskRecord's `intent_ref.path`.
|
|
1143
|
+
*
|
|
1144
|
+
* `freeze_artifacts` binds the sidecar in place, so a post-freeze read must
|
|
1145
|
+
* follow the record instead of guessing a default path; with no record yet
|
|
1146
|
+
* (pre-enrollment) the Kernel resolves the sidecar that exists. Same contract
|
|
1147
|
+
* as the Claude adapter's `readTaskIntentForRecord`.
|
|
1148
|
+
*/
|
|
1149
|
+
export function readTaskIntentForRecord(root: string, taskId: string): ReadTaskIntentResult {
|
|
1150
|
+
const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref.path;
|
|
1151
|
+
return readTaskIntent(root, taskId, currentPath);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function markGithubTaskTerminal(
|
|
1155
|
+
root: string,
|
|
1156
|
+
input: { task_id: string; phase: "done" | "stopped"; terminal_event_id: string },
|
|
1157
|
+
): Promise<GithubTrackerResult> {
|
|
1158
|
+
return runGithubTrackerOperation(root, { op: "mark-terminal", ...input });
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* The shared delivery identity selector: one function in workspace_scope.ts
|
|
1163
|
+
* picks the identity family per TaskRecord contract. This adapter-level alias
|
|
1164
|
+
* only narrows the record type for the Kernel ports that expect it; the dual-host
|
|
1165
|
+
* conformance seam imports it so both hosts' reportable identity is comparable.
|
|
1165
1166
|
*/
|
|
1166
|
-
function diffSnapshotOf(root: string, record:
|
|
1167
|
+
export function diffSnapshotOf(root: string, record: TaskRecord): {
|
|
1167
1168
|
diff_hash: string;
|
|
1168
1169
|
changed_paths: string[];
|
|
1169
1170
|
} {
|
|
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;
|
|
1171
|
+
return taskDeliveryIdentity(root, record);
|
|
1180
1172
|
}
|
|
1181
1173
|
|
|
1182
1174
|
// Translation-only adapter for the internal Kernel assurance projection. All
|
|
@@ -1196,7 +1188,7 @@ async function buildTaskOverview(root: string): Promise<{
|
|
|
1196
1188
|
const claim = await readBackendClaim(root);
|
|
1197
1189
|
let active: TaskOverviewEntry | null = null;
|
|
1198
1190
|
if (claim) {
|
|
1199
|
-
const projection = await
|
|
1191
|
+
const projection = await projectAssuranceForTask(root, claim.task_id);
|
|
1200
1192
|
if (!projection.error) {
|
|
1201
1193
|
const state = projection.projection;
|
|
1202
1194
|
const obligation = String(state.next_obligation);
|
|
@@ -1247,56 +1239,14 @@ function overviewRailState(lifecycle: string, obligation: string): TaskRailState
|
|
|
1247
1239
|
return "Working";
|
|
1248
1240
|
}
|
|
1249
1241
|
|
|
1250
|
-
async function
|
|
1251
|
-
return projectAssurance(root, taskId
|
|
1242
|
+
export async function projectAssuranceForTask(root: string, taskId: string): Promise<AssuranceProjectionResult> {
|
|
1243
|
+
return projectAssurance(root, taskId);
|
|
1252
1244
|
}
|
|
1253
1245
|
|
|
1254
1246
|
/**
|
|
1255
1247
|
* Publish and prove the task-scoped synthetic revision for a v4 record. v3
|
|
1256
1248
|
* records keep the legacy full-source bundle and return null here.
|
|
1257
1249
|
*/
|
|
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
1250
|
/**
|
|
1301
1251
|
* Review refs are reconstructible evidence transport, never workflow authority.
|
|
1302
1252
|
* A ref survives only while its task owns a nonterminal TaskRecord.
|
|
@@ -1314,7 +1264,7 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
|
|
|
1314
1264
|
for (const entry of listed) {
|
|
1315
1265
|
if (live.has(entry.taskId)) continue;
|
|
1316
1266
|
try {
|
|
1317
|
-
const record = await
|
|
1267
|
+
const record = await readTaskRecordRaw(root, entry.taskId);
|
|
1318
1268
|
if (record.record && record.record.lifecycle === "active" && claim?.task_id === entry.taskId)
|
|
1319
1269
|
live.add(entry.taskId);
|
|
1320
1270
|
} catch {
|
|
@@ -1340,267 +1290,13 @@ function notifyHost(ctx: HostContext, key: string, message: string, level: "warn
|
|
|
1340
1290
|
if (ui) notifyOnce({ ui }, key, message, level);
|
|
1341
1291
|
}
|
|
1342
1292
|
|
|
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
1293
|
// The invocation registry is shared with the progression module's
|
|
1601
1294
|
// module-scoped registry (see the top-level import above); commit/cancel
|
|
1602
1295
|
// semantics are identical to the previous extension implementation.
|
|
1603
1296
|
|
|
1297
|
+
const piConfirmationReference: ConfirmationReferenceSource = ({ snapshot, now }) =>
|
|
1298
|
+
`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)}`;
|
|
1299
|
+
|
|
1604
1300
|
async function mintCapability(
|
|
1605
1301
|
registry: MutationAuthorityRegistry,
|
|
1606
1302
|
input: {
|
|
@@ -1682,7 +1378,7 @@ async function executeOrdinaryOperation(
|
|
|
1682
1378
|
const operation = input.operation.op === "revise_intent"
|
|
1683
1379
|
? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
|
|
1684
1380
|
: input.operation;
|
|
1685
|
-
const priorIntent = await
|
|
1381
|
+
const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
|
|
1686
1382
|
const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
|
|
1687
1383
|
const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
|
|
1688
1384
|
// A content-changing revision writes the sidecar before the kernel's drift
|
|
@@ -1703,7 +1399,7 @@ async function executeOrdinaryOperation(
|
|
|
1703
1399
|
task_id: input.taskId,
|
|
1704
1400
|
operation: operation as never,
|
|
1705
1401
|
prior_intent_token: priorIntent.token,
|
|
1706
|
-
diffProvider: (root: string, record:
|
|
1402
|
+
diffProvider: (root: string, record: TaskRecord) => diffSnapshotOf(root, record),
|
|
1707
1403
|
now: new Date().toISOString(),
|
|
1708
1404
|
});
|
|
1709
1405
|
if (operation.op === "freeze_artifacts" || operation.op === "stop")
|
|
@@ -1714,7 +1410,7 @@ async function executeOrdinaryOperation(
|
|
|
1714
1410
|
return result;
|
|
1715
1411
|
} catch (error) {
|
|
1716
1412
|
if (priorBytes) {
|
|
1717
|
-
const current = await
|
|
1413
|
+
const current = await readTaskRecordRaw(ctx.cwd, input.taskId);
|
|
1718
1414
|
if (current.record?.intent_snapshot.revision === priorIntent.intent.revision && priorStaged) {
|
|
1719
1415
|
restoreStagedIntentShared(ctx.cwd, priorStaged);
|
|
1720
1416
|
}
|
|
@@ -1787,7 +1483,7 @@ async function enrichAssuranceResult(
|
|
|
1787
1483
|
const taskState = { error: typeof result.reason === "string" ? result.reason : "authority projection unavailable" };
|
|
1788
1484
|
return { ...result, task_state: taskState, next_action: nextActionForAssuranceResult(result, taskState) };
|
|
1789
1485
|
}
|
|
1790
|
-
const projection = await
|
|
1486
|
+
const projection = await projectAssuranceForTask(ctx.cwd, taskId);
|
|
1791
1487
|
const taskState: AssuranceTaskState = projection.error
|
|
1792
1488
|
? { error: projection.error }
|
|
1793
1489
|
: projection.projection;
|
|
@@ -1888,33 +1584,6 @@ function prepareActionArgs<Params>(args: unknown): Params {
|
|
|
1888
1584
|
return input as Params;
|
|
1889
1585
|
}
|
|
1890
1586
|
|
|
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
1587
|
function failCanaryTool(
|
|
1919
1588
|
taskId: string,
|
|
1920
1589
|
operation: string,
|