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.
- package/README.md +239 -0
- package/README.zh-CN.md +239 -0
- package/package.json +84 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
- package/plugins/immune-brain/.pi-extension/package.json +11 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
- package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
- package/plugins/immune-brain/bin/imm-kernel +4 -0
- package/plugins/immune-brain/bin/imm-plan +4 -0
- package/plugins/immune-brain/bin/imm-pr-diag +230 -0
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
- package/plugins/immune-brain/bin/imm-retired +4 -0
- package/plugins/immune-brain/bin/imm-tracker +4 -0
- package/plugins/immune-brain/dist/BASELINE.md +138 -0
- package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
- package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
- package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
- package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
- package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
- package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
- package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
- package/plugins/immune-brain/dist/imm-loop.md +158 -0
- package/plugins/immune-brain/dist/imm-planner.md +387 -0
- package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
- package/plugins/immune-brain/dist/registry.yaml +49 -0
- package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
- package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
- package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
- package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
- package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
- package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
- package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
- package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
- package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
- package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
- package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
- package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
- package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
- package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
- package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
- package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
- package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
- package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
- package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
- package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
- package/plugins/immune-brain/skills/.ignore +1 -0
- package/plugins/immune-brain/skills/BASELINE.md +138 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
- package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
- package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
- 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
|
+
}
|