immune-brain 2.8.2

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.
Files changed (101) hide show
  1. package/README.md +239 -0
  2. package/README.zh-CN.md +239 -0
  3. package/package.json +84 -0
  4. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
  6. package/plugins/immune-brain/.pi-extension/package.json +11 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
  9. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
  10. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
  11. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
  12. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
  13. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
  14. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
  15. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
  16. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
  17. package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
  18. package/plugins/immune-brain/bin/imm-kernel +4 -0
  19. package/plugins/immune-brain/bin/imm-plan +4 -0
  20. package/plugins/immune-brain/bin/imm-pr-diag +230 -0
  21. package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
  22. package/plugins/immune-brain/bin/imm-retired +4 -0
  23. package/plugins/immune-brain/bin/imm-tracker +4 -0
  24. package/plugins/immune-brain/dist/BASELINE.md +138 -0
  25. package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
  26. package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
  27. package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
  28. package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
  29. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
  30. package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
  31. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
  32. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
  33. package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
  34. package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
  35. package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
  36. package/plugins/immune-brain/dist/imm-loop.md +158 -0
  37. package/plugins/immune-brain/dist/imm-planner.md +387 -0
  38. package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
  39. package/plugins/immune-brain/dist/registry.yaml +49 -0
  40. package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
  41. package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
  42. package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
  43. package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
  44. package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
  45. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
  46. package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
  47. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
  48. package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
  49. package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
  50. package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
  51. package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
  52. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
  53. package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
  54. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
  55. package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
  56. package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
  57. package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
  58. package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
  59. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
  60. package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
  61. package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
  62. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
  63. package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
  64. package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
  65. package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
  66. package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
  67. package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
  68. package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
  69. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
  70. package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
  71. package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
  72. package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
  73. package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
  74. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
  75. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
  76. package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
  77. package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
  78. package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
  79. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
  80. package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
  81. package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
  82. package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
  83. package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
  84. package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
  85. package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
  86. package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
  87. package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
  88. package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
  89. package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
  90. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
  91. package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
  92. package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
  93. package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
  94. package/plugins/immune-brain/skills/.ignore +1 -0
  95. package/plugins/immune-brain/skills/BASELINE.md +138 -0
  96. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
  97. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
  98. package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
  99. package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
  100. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
  101. package/plugins/immune-brain/skills/registry.yaml +49 -0
@@ -0,0 +1,300 @@
1
+ // TaskRecord v3 mutation port. The single locked application path that
2
+ // rereads and consumes Intent identity, validates action/record/diff/CAS
3
+ // identity, consumes authority when privileged, reduces, and commits through
4
+ // the dedicated recoverable workspace transaction. Consumed by the Pi canary
5
+ // host adapter via canary_application.
6
+
7
+ import {
8
+ type MutationAuthorityRegistry,
9
+ type ValidatedAuthorityV2,
10
+ } from "./authority_port";
11
+ import { canonicalIntentHash, readTaskIntent } from "./intent";
12
+ import {
13
+ inspectIntentTokenPair,
14
+ consumeIntentToken,
15
+ type TaskIntentIdentityToken,
16
+ } from "./intent_token_registry";
17
+ import {
18
+ reduceTask,
19
+ canonicalRecordHash,
20
+ findingsDigestV2,
21
+ isReducedMutation,
22
+ } from "./reducer";
23
+ import {
24
+ commitTaskRecordLocked,
25
+ commitTerminalLocked,
26
+ readTaskRecordRaw,
27
+ readWorkspaceStateRaw,
28
+ revisionForContent,
29
+ serializeWorkspace,
30
+ withKernelStoreLock,
31
+ type WorkspaceState,
32
+ type WorkspaceTransactionV2,
33
+ type ArtifactRelocationV1,
34
+ } from "./storage";
35
+ import type {
36
+ AuthorityAuditDescriptor,
37
+ MutationAuthorityCapabilityV2,
38
+ StoredTaskMutationV3,
39
+ TaskAction,
40
+ TaskIntentV1,
41
+ } from "./types";
42
+ import { TASK_TOMBSTONE_CONTRACT, type TaskTombstone } from "./backend_claim";
43
+ import { KernelInvariantError, parseTaskAction } from "./validation";
44
+
45
+ export interface ApplyTaskActionInput {
46
+ root: string;
47
+ task_id: string;
48
+ action: unknown;
49
+ prior_intent_token: TaskIntentIdentityToken;
50
+ /** Paired mutation authority registry that must recognize every supplied capability. */
51
+ registry: MutationAuthorityRegistry;
52
+ capability?: MutationAuthorityCapabilityV2;
53
+ /** Trusted injected diff provider; the action's diff_hash is only an expectation. */
54
+ diffProvider: (root: string, intent: TaskIntentV1) => string;
55
+ now?: number;
56
+ /**
57
+ * Terminal commit mode: record + workspace + active-claim removal + task
58
+ * tombstone converge through one recoverable terminal transaction marker
59
+ * instead of the ordinary v2 transaction.
60
+ */
61
+ terminal?: {
62
+ terminalized_at: string;
63
+ };
64
+ artifact_transition?: {
65
+ relocations: ArtifactRelocationV1[];
66
+ next_intent_path: string;
67
+ next_artifact_state: "active" | "frozen";
68
+ };
69
+ }
70
+
71
+ export function applyTaskAction(
72
+ input: ApplyTaskActionInput,
73
+ ): StoredTaskMutationV3 {
74
+ const { root, task_id, prior_intent_token, registry, capability, diffProvider, now } =
75
+ input;
76
+ return withKernelStoreLock(root, () => {
77
+ const current = readTaskRecordRaw(root, task_id);
78
+ if (!current.record)
79
+ throw new KernelInvariantError([
80
+ `task ${task_id} has no TaskRecord v3`,
81
+ ]);
82
+ const workspace = readWorkspaceStateRaw(root);
83
+ const action = parseTaskAction(input.action);
84
+
85
+ // Expected-hash CAS and workspace ownership are validated after the
86
+ // preflight reduction: exact committed replay returns before these
87
+ // checks (the action carries the pre-commit expected hashes), while
88
+ // new events must carry the exact current identities and must fail
89
+ // closed before any token or capability is consumed.
90
+
91
+ // Trusted diff provider is the only diff authority.
92
+ const diffHash = diffProvider(root, current.record.intent_snapshot);
93
+ if (diffHash !== action.diff_hash)
94
+ throw new KernelInvariantError([
95
+ `action diff_hash ${action.diff_hash} does not match the trusted diff ${diffHash}`,
96
+ ]);
97
+
98
+ // Fresh secure reread of the sidecar inside the same lock.
99
+ const freshRead = readTaskIntent(root, task_id, current.record.intent_ref.path);
100
+ const { prior: priorIdentity, current: currentIdentity } =
101
+ inspectIntentTokenPair(prior_intent_token, freshRead.token);
102
+ if (
103
+ priorIdentity.sidecar_path !== freshRead.intent_ref.path ||
104
+ currentIdentity.sidecar_path !== freshRead.intent_ref.path
105
+ )
106
+ throw new KernelInvariantError([
107
+ "intent token sidecar path mismatch",
108
+ ]);
109
+ // A→B→A swap detection: the prior token must represent the same file
110
+ // identity as the fresh reread, even when the content hash is equal.
111
+ if (
112
+ priorIdentity.path_dev !== currentIdentity.path_dev ||
113
+ priorIdentity.path_ino !== currentIdentity.path_ino ||
114
+ priorIdentity.fd_dev !== currentIdentity.fd_dev ||
115
+ priorIdentity.fd_ino !== currentIdentity.fd_ino
116
+ )
117
+ throw new KernelInvariantError([
118
+ "intent sidecar identity changed between reads",
119
+ ]);
120
+
121
+ const isRevisionAction =
122
+ action.type === "revise_intent" ||
123
+ action.type === "approve_breaking_intent_revision";
124
+
125
+ if (isRevisionAction) {
126
+ // Prior token must bind the old committed record identity.
127
+ if (
128
+ priorIdentity.intent_content_hash !==
129
+ current.record.intent_ref.content_hash
130
+ )
131
+ throw new KernelInvariantError([
132
+ "prior intent token does not match the committed record intent",
133
+ ]);
134
+ // Fresh token must bind the next intent carried by the action.
135
+ if (
136
+ freshRead.content_hash !==
137
+ canonicalIntentHash(action.next_intent) ||
138
+ action.next_intent_ref.content_hash !== freshRead.content_hash ||
139
+ action.next_intent.revision !== freshRead.intent.revision
140
+ )
141
+ throw new KernelInvariantError([
142
+ "fresh intent token does not match the requested next intent",
143
+ ]);
144
+ } else {
145
+ // Both tokens must bind the committed record intent identity.
146
+ if (
147
+ priorIdentity.intent_content_hash !==
148
+ current.record.intent_ref.content_hash ||
149
+ currentIdentity.intent_content_hash !==
150
+ current.record.intent_ref.content_hash
151
+ )
152
+ throw new KernelInvariantError([
153
+ "intent token does not match the committed record intent",
154
+ ]);
155
+ }
156
+
157
+ const privileged =
158
+ action.type === "record_approval" ||
159
+ action.type === "record_user_approval" ||
160
+ action.type === "approve_breaking_intent_revision" ||
161
+ action.type === "request_rework" ||
162
+ action.type === "stop" ||
163
+ action.type === "resolve_user_decision";
164
+
165
+ const expectedAuthority = privileged
166
+ ? {
167
+ task_id,
168
+ action,
169
+ expected_record_hash: current.revision,
170
+ intent_revision: isRevisionAction
171
+ ? action.next_intent.revision
172
+ : current.record.intent_snapshot.revision,
173
+ intent_content_hash: isRevisionAction
174
+ ? freshRead.content_hash
175
+ : current.record.intent_ref.content_hash,
176
+ diff_hash: diffHash,
177
+ ...(action.type === "request_rework"
178
+ ? { findings_digest: findingsDigestV2(action.findings) }
179
+ : {}),
180
+ }
181
+ : null;
182
+
183
+ // Preflight reduction with inspected (not consumed) authority. The pure
184
+ // reducer enforces expected-hash equality for new events, lifecycle rules,
185
+ // replay identity, and conflicting reuse, so any throw is a zero-write
186
+ // failure (including stale record/workspace CAS).
187
+ const inspectedAudit = expectedAuthority
188
+ ? registry.inspect(capability, expectedAuthority, now)
189
+ : null;
190
+ const mutation = reduceTask(
191
+ current.record,
192
+ action,
193
+ inspectedAudit ? (inspectedAudit.audit as AuthorityAuditDescriptor) : null,
194
+ );
195
+ if (!isReducedMutation(mutation))
196
+ throw new KernelInvariantError(["reducer returned an invalid mutation"]);
197
+
198
+ // Exact committed replay: identical record -> return committed snapshot
199
+ // without consuming tokens or authority.
200
+ if (canonicalRecordHash(mutation.record) === current.revision)
201
+ return {
202
+ revision: current.revision,
203
+ record: current.record,
204
+ workspace,
205
+ };
206
+
207
+ // New event: enforce stale CAS and workspace ownership preflight.
208
+ if (action.expected_record_hash !== current.revision)
209
+ throw new KernelInvariantError([
210
+ `expected record hash mismatch: ${action.expected_record_hash} != ${current.revision}`,
211
+ ]);
212
+ if (action.expected_workspace_hash !== workspace.revision)
213
+ throw new KernelInvariantError([
214
+ `expected workspace hash mismatch: ${action.expected_workspace_hash} != ${workspace.revision}`,
215
+ ]);
216
+ if (
217
+ workspace.state.current_working !== null &&
218
+ workspace.state.current_working !== task_id
219
+ )
220
+ throw new KernelInvariantError([
221
+ `workspace is already owned by ${workspace.state.current_working}`,
222
+ ]);
223
+
224
+ // All preflight passed: consume tokens and authority, then commit.
225
+ consumeIntentToken(prior_intent_token);
226
+ consumeIntentToken(freshRead.token);
227
+ let consumedAudit: ValidatedAuthorityV2 | null = null;
228
+ if (expectedAuthority) {
229
+ consumedAudit = registry.consume(
230
+ capability as MutationAuthorityCapabilityV2,
231
+ expectedAuthority,
232
+ now,
233
+ );
234
+ }
235
+
236
+ const nextWorking = mutation.next_workspace_working;
237
+ const nextRecord = input.artifact_transition
238
+ ? {
239
+ ...mutation.record,
240
+ intent_ref: {
241
+ ...mutation.record.intent_ref,
242
+ path: input.artifact_transition.next_intent_path,
243
+ },
244
+ artifact_state: input.artifact_transition.next_artifact_state,
245
+ }
246
+ : mutation.record;
247
+ let nextWorkspaceState: WorkspaceState = workspace.state;
248
+ if (nextWorking !== null) {
249
+ nextWorkspaceState = {
250
+ ...workspace.state,
251
+ current_working: task_id,
252
+ };
253
+ } else if (workspace.state.current_working === task_id) {
254
+ nextWorkspaceState = {
255
+ ...workspace.state,
256
+ current_working: null,
257
+ };
258
+ }
259
+
260
+ if (input.terminal) {
261
+ const tombstone: TaskTombstone = {
262
+ contract: TASK_TOMBSTONE_CONTRACT,
263
+ task_id,
264
+ lifecycle_status: "terminal",
265
+ terminal_lifecycle: nextRecord.lifecycle,
266
+ terminal_event_id: action.event_id,
267
+ final_record_hash: canonicalRecordHash(nextRecord),
268
+ terminalized_at: input.terminal.terminalized_at,
269
+ };
270
+ const transaction: WorkspaceTransactionV2 = {
271
+ contract: "assurance_kernel/workspace_transaction/v2",
272
+ task_id,
273
+ expected_record_hash: current.revision,
274
+ next_record_content: `${JSON.stringify(nextRecord, null, 2)}\n`,
275
+ expected_workspace_hash: workspace.revision,
276
+ next_workspace_content: serializeWorkspace(nextWorkspaceState),
277
+ ...(input.artifact_transition ? { artifact_relocations: input.artifact_transition.relocations } : {}),
278
+ };
279
+ commitTerminalLocked(root, task_id, transaction, tombstone);
280
+ return {
281
+ revision: canonicalRecordHash(nextRecord),
282
+ record: nextRecord,
283
+ workspace: {
284
+ revision: revisionForContent(serializeWorkspace(nextWorkspaceState)),
285
+ state: nextWorkspaceState,
286
+ },
287
+ };
288
+ }
289
+
290
+ return commitTaskRecordLocked(
291
+ root,
292
+ task_id,
293
+ current.revision,
294
+ nextRecord,
295
+ workspace.revision,
296
+ nextWorkspaceState,
297
+ input.artifact_transition?.relocations,
298
+ );
299
+ });
300
+ }
@@ -0,0 +1,284 @@
1
+ // Assurance projection: the single host-neutral, read-only current-facts
2
+ // projection that binds a task's backend claim, TaskRecord v3, workspace,
3
+ // attestations, findings, and completion facts into one closed result.
4
+ //
5
+ // This module is deliberately INTERNAL to the Kernel runtime: it is imported
6
+ // by the Pi extension only through the structural runtime-stub adapter and is
7
+ // never re-exported from ./index.ts. Hosts consume the projection without
8
+ // reconstructing evidence/approval freshness or authorization readiness.
9
+ //
10
+ // It does not know about Pi session state, pending native Review verdicts,
11
+ // Pi lifecycle actions, TUI concepts, or presentation. It never decides
12
+ // "start QA" or "start Review"; it returns Kernel-owned facts only.
13
+
14
+ import { readBackendClaim, readTaskTombstone } from "./backend_claim";
15
+ import { readTaskRecord, readAuditTaskPair, readWorkspaceStateRaw, reconcileKernelAuthority } from "./storage";
16
+ import { projectTask } from "./completion";
17
+ import type { AssuranceObligation, TaskIntentV1, TaskRecordV2, TaskRecordV3 } from "./types";
18
+
19
+ export interface AssuranceAuthorizationReadiness {
20
+ /**
21
+ * Kernel-decidable authorization readiness:
22
+ * - "resolve_user_decision": exactly one open unresolved-user-decision finding;
23
+ * - "record_user_approval": Kernel projects authorize_user after fresh QA
24
+ * and Review attestations for a critical task;
25
+ * - "none": nothing uniquely decidable from Kernel facts.
26
+ * A pending Pi native Review verdict is a session fact and is NOT visible
27
+ * here; the host composes it before this readiness.
28
+ */
29
+ state: "resolve_user_decision" | "record_user_approval" | "none";
30
+ /** Non-null only when Kernel facts prove the authorization is blocked. */
31
+ blocked: string | null;
32
+ }
33
+
34
+ export interface AssuranceProjection {
35
+ record_revision: string;
36
+ workspace_revision: string;
37
+ intent_revision: number;
38
+ intent_content_hash: string;
39
+ diff_hash: string;
40
+ lifecycle: string;
41
+ artifact_state: string;
42
+ risk: TaskIntentV1["risk"] | "";
43
+ next_obligation: AssuranceObligation;
44
+ fresh_acceptance_ids: string[];
45
+ missing_acceptance_ids: string[];
46
+ stale_attestation_ids: string[];
47
+ fresh_approval_kinds: string[];
48
+ missing_approval_kinds: string[];
49
+ blocking_finding_ids: string[];
50
+ unresolved_user_decision_ids: string[];
51
+ replan_required_ids: string[];
52
+ independence_violations: string[];
53
+ open_user_decision_count: number;
54
+ completion_ready: boolean;
55
+ authorization: AssuranceAuthorizationReadiness;
56
+ }
57
+
58
+ export interface AssuranceProjectionResult {
59
+ contract: "assurance_kernel/assurance_projection/v1";
60
+ task_id: string;
61
+ error: string | null;
62
+ claim: { task_id: string; lifecycle_status: string } | null;
63
+ projection: AssuranceProjection;
64
+ }
65
+
66
+ export function deriveAssuranceAuthorization(input: {
67
+ next_obligation: AssuranceObligation;
68
+ open_user_decision_count: number;
69
+ }): AssuranceAuthorizationReadiness {
70
+ if (input.open_user_decision_count === 1)
71
+ return { state: "resolve_user_decision", blocked: null };
72
+ if (input.open_user_decision_count > 1)
73
+ return {
74
+ state: "none",
75
+ blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`,
76
+ };
77
+ if (input.next_obligation === "authorize_user")
78
+ return { state: "record_user_approval", blocked: null };
79
+ return { state: "none", blocked: null };
80
+ }
81
+
82
+ function emptyProjection(): AssuranceProjection {
83
+ return {
84
+ record_revision: "",
85
+ workspace_revision: "",
86
+ intent_revision: 0,
87
+ intent_content_hash: "",
88
+ diff_hash: "",
89
+ lifecycle: "",
90
+ artifact_state: "",
91
+ risk: "",
92
+ next_obligation: "none",
93
+ fresh_acceptance_ids: [],
94
+ missing_acceptance_ids: [],
95
+ stale_attestation_ids: [],
96
+ fresh_approval_kinds: [],
97
+ missing_approval_kinds: [],
98
+ blocking_finding_ids: [],
99
+ unresolved_user_decision_ids: [],
100
+ replan_required_ids: [],
101
+ independence_violations: [],
102
+ open_user_decision_count: 0,
103
+ completion_ready: false,
104
+ authorization: { state: "none", blocked: null },
105
+ };
106
+ }
107
+
108
+ function projectHistoricalTerminal(
109
+ record: TaskRecordV2,
110
+ recordRevision: string,
111
+ workspaceRevision: string,
112
+ ): AssuranceProjection {
113
+ const approvalKinds = [...new Set(record.approvals.map((item) => item.kind))];
114
+ return {
115
+ ...emptyProjection(),
116
+ record_revision: recordRevision,
117
+ workspace_revision: workspaceRevision,
118
+ intent_revision: record.intent_revision,
119
+ intent_content_hash: record.intent_ref.content_hash,
120
+ lifecycle: record.phase,
121
+ artifact_state: "frozen",
122
+ risk: record.intent_snapshot.risk,
123
+ fresh_acceptance_ids: [...new Set(record.evidence.filter((item) => item.status === "passed").map((item) => item.acceptance_id))],
124
+ fresh_approval_kinds: approvalKinds,
125
+ completion_ready: record.phase === "done",
126
+ };
127
+ }
128
+
129
+ function freshApprovalKinds(
130
+ record: TaskRecordV3,
131
+ currentIntentContentHash: string,
132
+ diffHash: string,
133
+ ): string[] {
134
+ const kinds: string[] = [];
135
+ const seen = new Set<string>();
136
+ for (const approval of record.attestations) {
137
+ if (
138
+ approval.task_revision !== record.intent_snapshot.revision ||
139
+ approval.intent_content_hash !== currentIntentContentHash ||
140
+ approval.diff_hash !== diffHash
141
+ )
142
+ continue;
143
+ if (seen.has(approval.kind)) continue;
144
+ seen.add(approval.kind);
145
+ kinds.push(approval.kind);
146
+ }
147
+ return kinds;
148
+ }
149
+
150
+ function projectFromRecord(
151
+ record: TaskRecordV3,
152
+ recordRevision: string,
153
+ workspaceRevision: string,
154
+ diffHash: string,
155
+ ): AssuranceProjection {
156
+ const intent: TaskIntentV1 = record.intent_snapshot;
157
+ const decision = projectTask(intent, record, diffHash, record.intent_ref.content_hash);
158
+ const approvalKinds = freshApprovalKinds(record, record.intent_ref.content_hash, diffHash);
159
+ const openUserDecisionCount = record.findings.filter(
160
+ (finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
161
+ ).length;
162
+ return {
163
+ record_revision: recordRevision,
164
+ workspace_revision: workspaceRevision,
165
+ intent_revision: record.intent_snapshot.revision,
166
+ intent_content_hash: record.intent_ref.content_hash,
167
+ diff_hash: diffHash,
168
+ lifecycle: record.lifecycle,
169
+ artifact_state: record.artifact_state,
170
+ risk: intent.risk,
171
+ next_obligation: decision.next_obligation,
172
+ fresh_acceptance_ids: decision.fresh_acceptance_ids,
173
+ missing_acceptance_ids: decision.missing_acceptance_ids,
174
+ stale_attestation_ids: decision.stale_attestation_ids,
175
+ fresh_approval_kinds: approvalKinds,
176
+ missing_approval_kinds: decision.missing_approval_kinds,
177
+ blocking_finding_ids: decision.blocking_finding_ids,
178
+ unresolved_user_decision_ids: decision.unresolved_user_decision_ids,
179
+ replan_required_ids: decision.replan_required_ids,
180
+ independence_violations: decision.independence_violations,
181
+ open_user_decision_count: openUserDecisionCount,
182
+ completion_ready: decision.complete,
183
+ authorization: deriveAssuranceAuthorization({
184
+ next_obligation: decision.next_obligation,
185
+ open_user_decision_count: openUserDecisionCount,
186
+ }),
187
+ };
188
+ }
189
+
190
+ /**
191
+ * One closed read-only projection for a task. The injected diffProvider keeps
192
+ * this module free of git/worktree concerns; hosts supply it from their own
193
+ * task-scope diff implementation.
194
+ *
195
+ * Error semantics:
196
+ * - no authority facts -> error null, claim null, empty projection
197
+ * - matching terminal proof -> error null, claim null, done/stopped projection
198
+ * - repairable stale claim -> error requiring repair_authority_state
199
+ * - authority conflict -> fail-closed authority diagnostic
200
+ * - missing/mismatched record -> fail-closed identity diagnostic
201
+ * - any read failure -> error message, claim null, empty projection
202
+ */
203
+ export async function projectAssurance(
204
+ root: string,
205
+ taskId: string,
206
+ diffProvider: (root: string, intent: { scope_hint?: unknown }) => string,
207
+ ): Promise<AssuranceProjectionResult> {
208
+ const fail = (
209
+ error: string,
210
+ claim: AssuranceProjectionResult["claim"] = null,
211
+ ): AssuranceProjectionResult => ({
212
+ contract: "assurance_kernel/assurance_projection/v1",
213
+ task_id: taskId,
214
+ error,
215
+ claim,
216
+ projection: emptyProjection(),
217
+ });
218
+ try {
219
+ const claim = readBackendClaim(root);
220
+ let terminalOwner = false;
221
+ if (claim?.task_id !== undefined && claim.task_id !== taskId)
222
+ return fail(`backend claim belongs to ${claim.task_id}, not ${taskId}`, claim);
223
+ if (claim) {
224
+ const tombstone = readTaskTombstone(root, taskId);
225
+ if (tombstone) {
226
+ const authority = reconcileKernelAuthority(root, taskId);
227
+ if (authority.state === "repairable_stale_claim")
228
+ return fail(`task ${taskId} has a repairable stale backend claim`, claim);
229
+ return fail(authority.diagnostic ?? `authority state conflicts for ${taskId}`, claim);
230
+ }
231
+ } else {
232
+ const authority = reconcileKernelAuthority(root, taskId);
233
+ if (authority.state === "unowned")
234
+ return { contract: "assurance_kernel/assurance_projection/v1", task_id: taskId, error: null, claim: null, projection: emptyProjection() };
235
+ if (authority.state !== "terminal_owner")
236
+ return fail(authority.diagnostic ?? `authority state conflicts for ${taskId}`);
237
+ terminalOwner = true;
238
+ if (readBackendClaim(root)) return fail(`authority state changed while projecting ${taskId}`);
239
+ }
240
+ let read;
241
+ try {
242
+ read = await readTaskRecord(root, taskId);
243
+ } catch (error) {
244
+ if (!terminalOwner || !(error instanceof Error) || !error.message.startsWith("TaskRecord v2")) throw error;
245
+ return fail(error instanceof Error ? error.message : String(error));
246
+ }
247
+ if (!read.record) {
248
+ if (!terminalOwner) return fail(`task ${taskId} has no TaskRecord v3`, claim);
249
+ const auditPair = await readAuditTaskPair(root, taskId);
250
+ if (!auditPair) return fail(`task ${taskId} has no terminal audit pair`, claim);
251
+ const workspace = await readWorkspaceStateRaw(root);
252
+ if (auditPair.record.contract === "assurance_kernel/task_record/v2")
253
+ return {
254
+ contract: "assurance_kernel/assurance_projection/v1",
255
+ task_id: taskId,
256
+ error: null,
257
+ claim: null,
258
+ projection: projectHistoricalTerminal(auditPair.record, auditPair.recordRevision, workspace.revision),
259
+ };
260
+ return {
261
+ contract: "assurance_kernel/assurance_projection/v1",
262
+ task_id: taskId,
263
+ error: null,
264
+ claim: null,
265
+ projection: projectFromRecord(auditPair.record, auditPair.recordRevision, workspace.revision, diffProvider(root, auditPair.record.intent_snapshot)),
266
+ };
267
+ }
268
+ if (read.record.task_id !== taskId)
269
+ return fail(`task record identity is inconsistent for ${taskId}`, claim);
270
+ if (claim && read.record.lifecycle !== "active")
271
+ return fail(`terminal task ${taskId} has no matching tombstone proof`, claim);
272
+ const workspace = await readWorkspaceStateRaw(root);
273
+ const diffHash = diffProvider(root, read.record.intent_snapshot);
274
+ return {
275
+ contract: "assurance_kernel/assurance_projection/v1",
276
+ task_id: taskId,
277
+ error: null,
278
+ claim,
279
+ projection: projectFromRecord(read.record, read.revision, workspace.revision, diffHash),
280
+ };
281
+ } catch (error) {
282
+ return fail(error instanceof Error ? error.message : String(error));
283
+ }
284
+ }