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,666 @@
|
|
|
1
|
+
// Foreground Enrollment extension.
|
|
2
|
+
// The sole production enrollment authority route is the Parent-invoked
|
|
3
|
+
// `imm_canary_enrollment` Tool, whose execute callback owns preparation through
|
|
4
|
+
// terminal settlement. Host cancellation applies to every pre-commit stage;
|
|
5
|
+
// after the explicit commit linearization point, Kernel settlement is
|
|
6
|
+
// non-cancellable.
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
// The Kernel runtime graph is never type-checked from this extension: static
|
|
13
|
+
// imports resolve to ./runtime-stub.ts (relative so the Pi extension loader
|
|
14
|
+
// can resolve them at runtime), and the stub forwards to the real Kernel
|
|
15
|
+
// modules via dynamic import.
|
|
16
|
+
import {
|
|
17
|
+
createEnrollmentAuthorityRegistry,
|
|
18
|
+
preparePiCanary,
|
|
19
|
+
revalidatePiCanary,
|
|
20
|
+
evaluateCanaryEligibility,
|
|
21
|
+
reconcileKernelAuthority,
|
|
22
|
+
readTaskIntent,
|
|
23
|
+
runEnrollmentRehearsal,
|
|
24
|
+
enrollCanaryTask,
|
|
25
|
+
withKernelStoreLock,
|
|
26
|
+
inspectStorageLayout,
|
|
27
|
+
migrateLegacyLayout,
|
|
28
|
+
} from "./runtime-stub";
|
|
29
|
+
import {
|
|
30
|
+
presentTaskRail,
|
|
31
|
+
presentTaskRailResult,
|
|
32
|
+
renderStructuredCall,
|
|
33
|
+
renderStructuredResult,
|
|
34
|
+
requestAuthorityDialog,
|
|
35
|
+
resetInteractionPresentation,
|
|
36
|
+
type UserAttentionReason,
|
|
37
|
+
} from "./pi-canary-interaction";
|
|
38
|
+
import { isToolFailureState, throwToolFailure } from "./pi-canary-tool-failure";
|
|
39
|
+
|
|
40
|
+
const TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
41
|
+
|
|
42
|
+
export function assertTaskIntentPreparationStable(
|
|
43
|
+
preflight: { content_hash: string },
|
|
44
|
+
preparation: { intent: { content_hash: string } | null },
|
|
45
|
+
): void {
|
|
46
|
+
if (preparation.intent?.content_hash !== preflight.content_hash)
|
|
47
|
+
throw new Error("TaskIntent changed during preparation; retry enrollment");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function requestEnrollmentConfirmation(
|
|
51
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
52
|
+
ctx: ExtensionContext,
|
|
53
|
+
taskId: string,
|
|
54
|
+
reason: UserAttentionReason,
|
|
55
|
+
title: string,
|
|
56
|
+
summary: string,
|
|
57
|
+
details: string,
|
|
58
|
+
signal: AbortSignal,
|
|
59
|
+
): Promise<boolean> {
|
|
60
|
+
presentTaskRail(ctx, {
|
|
61
|
+
task_id: taskId,
|
|
62
|
+
state: "Approval required",
|
|
63
|
+
result: title,
|
|
64
|
+
next: "Review enrollment evidence",
|
|
65
|
+
});
|
|
66
|
+
const selected = await requestAuthorityDialog(pi, ctx, {
|
|
67
|
+
attention_id: randomUUID(),
|
|
68
|
+
task_id: taskId,
|
|
69
|
+
reason,
|
|
70
|
+
label: title,
|
|
71
|
+
}, {
|
|
72
|
+
title,
|
|
73
|
+
summary,
|
|
74
|
+
details,
|
|
75
|
+
signal,
|
|
76
|
+
actions: [
|
|
77
|
+
{ value: "confirm", label: "Confirm enrollment", description: "Create the Kernel-managed task" },
|
|
78
|
+
{ value: "cancel", label: "Cancel", description: "Leave planning artifacts and authority unchanged" },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
return selected === "confirm";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ActiveForegroundEnrollment {
|
|
85
|
+
taskId: string;
|
|
86
|
+
controller: AbortController;
|
|
87
|
+
committing: boolean;
|
|
88
|
+
completion: Promise<EnrollmentTerminal>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type EnrollmentAction = "new";
|
|
92
|
+
export type EnrollmentTerminalState =
|
|
93
|
+
| "completed"
|
|
94
|
+
| "blocked"
|
|
95
|
+
| "rejected"
|
|
96
|
+
| "cancelled"
|
|
97
|
+
| "failed"
|
|
98
|
+
| "settlement_unknown"
|
|
99
|
+
| "route_incumbent"
|
|
100
|
+
| "repair_authorization_required"
|
|
101
|
+
| "authority_conflict";
|
|
102
|
+
|
|
103
|
+
export interface EnrollmentTerminal extends Record<string, unknown> {
|
|
104
|
+
contract: "assurance_kernel/enrollment_tool_result/v1";
|
|
105
|
+
state: EnrollmentTerminalState;
|
|
106
|
+
action: EnrollmentAction;
|
|
107
|
+
task_id: string;
|
|
108
|
+
stage: string;
|
|
109
|
+
summary: string;
|
|
110
|
+
next_action: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function terminal(
|
|
114
|
+
action: EnrollmentAction,
|
|
115
|
+
taskId: string,
|
|
116
|
+
state: EnrollmentTerminalState,
|
|
117
|
+
stage: string,
|
|
118
|
+
summary: string,
|
|
119
|
+
nextAction: string,
|
|
120
|
+
): EnrollmentTerminal {
|
|
121
|
+
return {
|
|
122
|
+
contract: "assurance_kernel/enrollment_tool_result/v1",
|
|
123
|
+
state,
|
|
124
|
+
action,
|
|
125
|
+
task_id: taskId,
|
|
126
|
+
stage,
|
|
127
|
+
summary: summary.slice(0, 4_096),
|
|
128
|
+
next_action: nextAction,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function enrollmentToolResult(details: EnrollmentTerminal | Record<string, unknown>) {
|
|
133
|
+
const summary = typeof details.summary === "string" ? details.summary : "Enrollment update";
|
|
134
|
+
return { content: [{ type: "text" as const, text: summary }], details };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class ForegroundEnrollmentCoordinator {
|
|
138
|
+
private active: ActiveForegroundEnrollment | undefined;
|
|
139
|
+
private shuttingDown = false;
|
|
140
|
+
|
|
141
|
+
async run(
|
|
142
|
+
action: EnrollmentAction,
|
|
143
|
+
taskId: string,
|
|
144
|
+
hostSignal: AbortSignal | undefined,
|
|
145
|
+
work: (signal: AbortSignal, beginCommit: () => boolean) => Promise<EnrollmentTerminal>,
|
|
146
|
+
): Promise<EnrollmentTerminal> {
|
|
147
|
+
if (this.shuttingDown)
|
|
148
|
+
return terminal(action, taskId, "blocked", "preparing", "Enrollment cannot start during session shutdown", "retry in an active TUI session");
|
|
149
|
+
if (this.active)
|
|
150
|
+
return terminal(
|
|
151
|
+
action,
|
|
152
|
+
taskId,
|
|
153
|
+
"blocked",
|
|
154
|
+
"preparing",
|
|
155
|
+
`Enrollment already runs in foreground for ${this.active.taskId}`,
|
|
156
|
+
"wait for the active foreground Tool call to settle",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
const placeholder = Promise.resolve(
|
|
161
|
+
terminal(action, taskId, "failed", "preparing", "Enrollment did not start", "inspect the Tool result"),
|
|
162
|
+
);
|
|
163
|
+
const slot: ActiveForegroundEnrollment = {
|
|
164
|
+
taskId,
|
|
165
|
+
controller,
|
|
166
|
+
committing: false,
|
|
167
|
+
completion: placeholder,
|
|
168
|
+
};
|
|
169
|
+
this.active = slot;
|
|
170
|
+
const relayAbort = () => {
|
|
171
|
+
if (this.active !== slot || slot.committing || controller.signal.aborted) return;
|
|
172
|
+
controller.abort(
|
|
173
|
+
hostSignal?.reason instanceof Error
|
|
174
|
+
? hostSignal.reason
|
|
175
|
+
: new Error("foreground enrollment cancelled by host"),
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
hostSignal?.addEventListener("abort", relayAbort, { once: true });
|
|
179
|
+
if (hostSignal?.aborted) relayAbort();
|
|
180
|
+
|
|
181
|
+
const beginCommit = (): boolean => {
|
|
182
|
+
if (this.active !== slot || controller.signal.aborted) return false;
|
|
183
|
+
slot.committing = true;
|
|
184
|
+
return true;
|
|
185
|
+
};
|
|
186
|
+
const completion = work(controller.signal, beginCommit);
|
|
187
|
+
slot.completion = completion;
|
|
188
|
+
try {
|
|
189
|
+
return await completion;
|
|
190
|
+
} finally {
|
|
191
|
+
hostSignal?.removeEventListener("abort", relayAbort);
|
|
192
|
+
if (this.active === slot) this.active = undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async shutdown(): Promise<void> {
|
|
197
|
+
this.shuttingDown = true;
|
|
198
|
+
const active = this.active;
|
|
199
|
+
if (!active) return;
|
|
200
|
+
if (!active.committing && !active.controller.signal.aborted)
|
|
201
|
+
active.controller.abort(new Error("Pi session shutdown"));
|
|
202
|
+
await active.completion.catch(() => undefined);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function updateResult(
|
|
207
|
+
action: EnrollmentAction,
|
|
208
|
+
taskId: string,
|
|
209
|
+
stage: string,
|
|
210
|
+
summary: string,
|
|
211
|
+
): ReturnType<typeof enrollmentToolResult> {
|
|
212
|
+
return enrollmentToolResult({
|
|
213
|
+
contract: "assurance_kernel/enrollment_tool_progress/v1",
|
|
214
|
+
state: "running",
|
|
215
|
+
action,
|
|
216
|
+
task_id: taskId,
|
|
217
|
+
stage,
|
|
218
|
+
summary,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function errorMessage(error: unknown): string {
|
|
223
|
+
return error instanceof Error ? error.message : String(error);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function classifyCommitFailure(
|
|
227
|
+
root: string,
|
|
228
|
+
action: EnrollmentAction,
|
|
229
|
+
taskId: string,
|
|
230
|
+
now: string,
|
|
231
|
+
error: unknown,
|
|
232
|
+
): Promise<EnrollmentTerminal> {
|
|
233
|
+
const failure = errorMessage(error);
|
|
234
|
+
try {
|
|
235
|
+
const current = await preparePiCanary(root, { task_id: taskId, now });
|
|
236
|
+
if (
|
|
237
|
+
current.backend_claim.present
|
|
238
|
+
&& current.backend_claim.task_id === taskId
|
|
239
|
+
&& current.task_record_v3?.present
|
|
240
|
+
&& current.workspace.current_working === taskId
|
|
241
|
+
) {
|
|
242
|
+
return terminal(
|
|
243
|
+
action,
|
|
244
|
+
taskId,
|
|
245
|
+
"completed",
|
|
246
|
+
"committing",
|
|
247
|
+
`Kernel enrollment committed for ${taskId}; terminal receipt was recovered from authoritative owners`,
|
|
248
|
+
"continue with imm-loop",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
!current.backend_claim.present
|
|
253
|
+
&& !current.task_record_v3?.present
|
|
254
|
+
&& current.workspace.current_working === null
|
|
255
|
+
) {
|
|
256
|
+
return terminal(
|
|
257
|
+
action,
|
|
258
|
+
taskId,
|
|
259
|
+
"failed",
|
|
260
|
+
"committing",
|
|
261
|
+
`Kernel enrollment failed with no committed owner state: ${failure}`,
|
|
262
|
+
"correct the reported final-lock failure and retry",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// A contradictory or unreadable owner projection cannot prove settlement.
|
|
267
|
+
}
|
|
268
|
+
return terminal(
|
|
269
|
+
action,
|
|
270
|
+
taskId,
|
|
271
|
+
"settlement_unknown",
|
|
272
|
+
"committing",
|
|
273
|
+
`Kernel enrollment settlement is unknown after commit started: ${failure}`,
|
|
274
|
+
"inspect authoritative Kernel status before any retry",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function executeForegroundEnrollment(
|
|
279
|
+
root: string,
|
|
280
|
+
action: EnrollmentAction,
|
|
281
|
+
taskId: string,
|
|
282
|
+
signal: AbortSignal,
|
|
283
|
+
beginCommit: () => boolean,
|
|
284
|
+
onUpdate: ((update: ReturnType<typeof enrollmentToolResult>) => void) | undefined,
|
|
285
|
+
ctx: ExtensionContext,
|
|
286
|
+
registry: Awaited<ReturnType<typeof createEnrollmentAuthorityRegistry>>,
|
|
287
|
+
pi: Pick<ExtensionAPI, "events">,
|
|
288
|
+
): Promise<EnrollmentTerminal> {
|
|
289
|
+
let stage = "preparing";
|
|
290
|
+
const progress = (nextStage: string, summary: string) => {
|
|
291
|
+
stage = nextStage;
|
|
292
|
+
const update = updateResult(action, taskId, nextStage, summary);
|
|
293
|
+
onUpdate?.(update);
|
|
294
|
+
presentTaskRailResult(ctx, taskId, update.details);
|
|
295
|
+
};
|
|
296
|
+
const cancelled = () => terminal(
|
|
297
|
+
action,
|
|
298
|
+
taskId,
|
|
299
|
+
"cancelled",
|
|
300
|
+
stage,
|
|
301
|
+
`Foreground enrollment cancelled during ${stage}; zero authority writes were requested`,
|
|
302
|
+
"retry by invoking the launcher again",
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
signal.throwIfAborted();
|
|
307
|
+
progress("preparing", `Preparing immutable Kernel owners for ${taskId}`);
|
|
308
|
+
const now = new Date().toISOString();
|
|
309
|
+
let taskIntent: Awaited<ReturnType<typeof readTaskIntent>>;
|
|
310
|
+
|
|
311
|
+
// Storage-layout gate (BR-REQ-005/006): enrollment is the stateful
|
|
312
|
+
// mutation boundary. Kernel transaction markers recover under the
|
|
313
|
+
// store lock; the one-release migrator relocates an owner-free legacy
|
|
314
|
+
// layout and stops, and the literal user retries after committing the
|
|
315
|
+
// affected migration diff. Repreparation never proceeds on a dirty
|
|
316
|
+
// or blocked layout.
|
|
317
|
+
{
|
|
318
|
+
const recoverLayout = async (): Promise<
|
|
319
|
+
EnrollmentTerminal | undefined
|
|
320
|
+
> => {
|
|
321
|
+
try {
|
|
322
|
+
await withKernelStoreLock(root, () => undefined);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return terminal(
|
|
325
|
+
action,
|
|
326
|
+
taskId,
|
|
327
|
+
"blocked",
|
|
328
|
+
stage,
|
|
329
|
+
`Kernel transaction recovery failed before enrollment: ${errorMessage(error)}`,
|
|
330
|
+
"resolve the pending Kernel transaction marker and retry",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const inspection = await inspectStorageLayout(root);
|
|
334
|
+
if (inspection.layout === "ready") return undefined;
|
|
335
|
+
if (
|
|
336
|
+
inspection.layout === "migration_required" ||
|
|
337
|
+
inspection.layout === "recovery_required"
|
|
338
|
+
) {
|
|
339
|
+
const migration = await migrateLegacyLayout(root);
|
|
340
|
+
if (migration.outcome === "migrated")
|
|
341
|
+
return terminal(
|
|
342
|
+
action,
|
|
343
|
+
taskId,
|
|
344
|
+
"blocked",
|
|
345
|
+
stage,
|
|
346
|
+
`Legacy storage migrated (${migration.affected_paths.length} paths); commit the affected migration diff and retry enrollment`,
|
|
347
|
+
"commit .imm/audit/** and .imm/legacy paths, then retry",
|
|
348
|
+
);
|
|
349
|
+
if (migration.outcome === "migration_uncommitted")
|
|
350
|
+
return terminal(
|
|
351
|
+
action,
|
|
352
|
+
taskId,
|
|
353
|
+
"blocked",
|
|
354
|
+
stage,
|
|
355
|
+
`Migration completed but affected paths are uncommitted: ${migration.affected_paths.join(", ")}`,
|
|
356
|
+
"commit the affected migration diff, then retry",
|
|
357
|
+
);
|
|
358
|
+
return terminal(
|
|
359
|
+
action,
|
|
360
|
+
taskId,
|
|
361
|
+
"blocked",
|
|
362
|
+
stage,
|
|
363
|
+
`Enrollment blocked by storage layout (${migration.outcome}): ${migration.reason ?? inspection.reason ?? ""}`,
|
|
364
|
+
"resolve the reported layout condition and retry",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return terminal(
|
|
368
|
+
action,
|
|
369
|
+
taskId,
|
|
370
|
+
"blocked",
|
|
371
|
+
stage,
|
|
372
|
+
`Enrollment blocked by storage layout (${inspection.layout}): ${inspection.reason ?? ""}`,
|
|
373
|
+
"resolve the reported layout condition and retry",
|
|
374
|
+
);
|
|
375
|
+
};
|
|
376
|
+
const blocked = await recoverLayout();
|
|
377
|
+
if (blocked) return blocked;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
taskIntent = await readTaskIntent(root, taskId);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
const message = errorMessage(error);
|
|
384
|
+
if (/not Git-tracked|ENOENT|no such file/i.test(message))
|
|
385
|
+
return terminal(action, taskId, "blocked", stage, "A Git-tracked TaskIntent is required for Kernel enrollment", "author and stage the canonical TaskIntent");
|
|
386
|
+
return terminal(action, taskId, "blocked", stage, `TaskIntent validation failed before rehearsal: ${message}`, "repair the reported TaskIntent schema errors");
|
|
387
|
+
}
|
|
388
|
+
const authority = await reconcileKernelAuthority(root, taskId);
|
|
389
|
+
if (authority.state === "active_owner")
|
|
390
|
+
return terminal(
|
|
391
|
+
action,
|
|
392
|
+
taskId,
|
|
393
|
+
"route_incumbent",
|
|
394
|
+
stage,
|
|
395
|
+
`Kernel task ${authority.owner_task_id} already owns this workspace`,
|
|
396
|
+
`continue ${authority.owner_task_id} through imm-loop without re-enrollment`,
|
|
397
|
+
);
|
|
398
|
+
if (authority.state === "repairable_stale_claim")
|
|
399
|
+
return terminal(
|
|
400
|
+
action,
|
|
401
|
+
taskId,
|
|
402
|
+
"repair_authorization_required",
|
|
403
|
+
stage,
|
|
404
|
+
`Terminal task ${authority.owner_task_id} retains an exactly proven stale backend claim`,
|
|
405
|
+
`invoke imm_kernel_canary repair_authority_state for ${authority.owner_task_id}`,
|
|
406
|
+
);
|
|
407
|
+
if (authority.state === "authority_conflict" || authority.state === "terminal_owner")
|
|
408
|
+
return terminal(
|
|
409
|
+
action,
|
|
410
|
+
taskId,
|
|
411
|
+
"authority_conflict",
|
|
412
|
+
stage,
|
|
413
|
+
authority.diagnostic ?? `Kernel authority state is ${authority.state}`,
|
|
414
|
+
"inspect authority state; do not retry enrollment",
|
|
415
|
+
);
|
|
416
|
+
const preparation = await preparePiCanary(root, { task_id: taskId, now });
|
|
417
|
+
signal.throwIfAborted();
|
|
418
|
+
if (!preparation.intent)
|
|
419
|
+
return terminal(action, taskId, "blocked", stage, "A Git-tracked TaskIntent is required for Kernel enrollment", "author and stage the canonical TaskIntent");
|
|
420
|
+
try {
|
|
421
|
+
assertTaskIntentPreparationStable(taskIntent, preparation);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return terminal(action, taskId, "blocked", stage, errorMessage(error), "retry enrollment from the launcher");
|
|
424
|
+
}
|
|
425
|
+
if (
|
|
426
|
+
action === "new"
|
|
427
|
+
&& (preparation.backend_claim.present || preparation.task_record_v3?.present)
|
|
428
|
+
)
|
|
429
|
+
return terminal(action, taskId, "blocked", stage, `Task ${taskId} is already owned by the Kernel backend`, "continue the existing Kernel task");
|
|
430
|
+
if (action === "new" && preparation.workspace.current_working !== null)
|
|
431
|
+
return terminal(action, taskId, "blocked", stage, `Workspace is owned by ${preparation.workspace.current_working}`, "finish or stop the current owner first");
|
|
432
|
+
|
|
433
|
+
const eligibility = await evaluateCanaryEligibility({
|
|
434
|
+
task: {
|
|
435
|
+
id: taskId,
|
|
436
|
+
intent_path: preparation.intent.path,
|
|
437
|
+
intent_revision: preparation.intent.revision,
|
|
438
|
+
intent_content_hash: preparation.intent.content_hash,
|
|
439
|
+
},
|
|
440
|
+
now,
|
|
441
|
+
});
|
|
442
|
+
if (!eligibility.eligible) {
|
|
443
|
+
const reasons = [...eligibility.rejections, ...eligibility.unmet_non_waivable].join("; ");
|
|
444
|
+
return terminal(action, taskId, "blocked", stage, `Enrollment is ineligible: ${reasons}`, "resolve the eligibility blockers");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const acceptanceDetails = taskIntent.intent.acceptance.length === 0
|
|
448
|
+
? "(none)"
|
|
449
|
+
: taskIntent.intent.acceptance
|
|
450
|
+
.map((item) => `${item.id}: ${item.assertion} [verification: ${item.verification}]`)
|
|
451
|
+
.join("\n");
|
|
452
|
+
// Enrollment binds authority to the validated intent. Acceptance descriptors
|
|
453
|
+
// execute only during post-implementation deterministic QA.
|
|
454
|
+
const confirmedContentHash = preparation.intent.content_hash;
|
|
455
|
+
{
|
|
456
|
+
const confirmationSummary = [
|
|
457
|
+
`Task: ${taskId}`,
|
|
458
|
+
`Goal: ${taskIntent.intent.goal}`,
|
|
459
|
+
`Risk: ${taskIntent.intent.risk}`,
|
|
460
|
+
`Scope: ${taskIntent.intent.scope_hint.length > 0 ? taskIntent.intent.scope_hint.join(", ") : "(none)"}`,
|
|
461
|
+
`Acceptance: ${taskIntent.intent.acceptance.length} descriptor(s); press d to expand`,
|
|
462
|
+
`Intent digest: ${preparation.intent.content_hash}`,
|
|
463
|
+
`Preparation digest: ${preparation.digest}`,
|
|
464
|
+
].join("\n");
|
|
465
|
+
const confirmationDetails = [
|
|
466
|
+
`Acceptance descriptors:`,
|
|
467
|
+
acceptanceDetails,
|
|
468
|
+
`Preparation digest: ${preparation.digest}`,
|
|
469
|
+
`Intent digest: ${preparation.intent.content_hash}`,
|
|
470
|
+
`Intent: ${preparation.intent.path} @ rev ${preparation.intent.revision}`,
|
|
471
|
+
`Owners: intent+workspace+claim+record checked`,
|
|
472
|
+
`Route: Kernel enrollment`,
|
|
473
|
+
].join("\n");
|
|
474
|
+
progress("awaiting_confirmation", "Waiting for exact literal-user confirmation");
|
|
475
|
+
const confirmed = await requestEnrollmentConfirmation(
|
|
476
|
+
pi,
|
|
477
|
+
ctx,
|
|
478
|
+
taskId,
|
|
479
|
+
"enrollment",
|
|
480
|
+
"Create Kernel-managed task?",
|
|
481
|
+
confirmationSummary,
|
|
482
|
+
confirmationDetails,
|
|
483
|
+
signal,
|
|
484
|
+
);
|
|
485
|
+
if (signal.aborted) return cancelled();
|
|
486
|
+
if (!confirmed)
|
|
487
|
+
return terminal(action, taskId, "rejected", stage, "Enrollment confirmation was rejected; zero authority writes were requested", "invoke the launcher again only if enrollment is still intended");
|
|
488
|
+
}
|
|
489
|
+
{
|
|
490
|
+
let liveEarly: string | null = null;
|
|
491
|
+
try {
|
|
492
|
+
liveEarly = (await readTaskIntent(resolve(root), taskId)).content_hash;
|
|
493
|
+
} catch {
|
|
494
|
+
liveEarly = null;
|
|
495
|
+
}
|
|
496
|
+
if (liveEarly !== confirmedContentHash)
|
|
497
|
+
return terminal(action, taskId, "blocked", stage, "Intent changed after confirmation; enrollment aborted before authority", "restore the intended snapshot and rerun enrollment");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
{
|
|
501
|
+
let liveContentHash: string | null = null;
|
|
502
|
+
try {
|
|
503
|
+
liveContentHash = (await readTaskIntent(resolve(root), taskId)).content_hash;
|
|
504
|
+
} catch {
|
|
505
|
+
liveContentHash = null;
|
|
506
|
+
}
|
|
507
|
+
if (liveContentHash !== confirmedContentHash)
|
|
508
|
+
return terminal(action, taskId, "blocked", stage, "Intent changed after confirmation; enrollment aborted before authority", "restore the intended snapshot and rerun enrollment");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
progress("revalidating", "Revalidating immutable owners and the confirmed TaskIntent");
|
|
512
|
+
const { unchanged } = await revalidatePiCanary(root, { task_id: taskId, now }, preparation);
|
|
513
|
+
if (!unchanged)
|
|
514
|
+
return terminal(action, taskId, "blocked", stage, "Workspace changed after confirmation; enrollment aborted before authority", "restore the intended snapshot and rerun enrollment");
|
|
515
|
+
signal.throwIfAborted();
|
|
516
|
+
|
|
517
|
+
const nonce = randomUUID();
|
|
518
|
+
const binding = {
|
|
519
|
+
task_id: taskId,
|
|
520
|
+
intent_path: preparation.intent.path,
|
|
521
|
+
intent_revision: preparation.intent.revision,
|
|
522
|
+
intent_content_hash: preparation.intent.content_hash,
|
|
523
|
+
preparation_digest: preparation.digest,
|
|
524
|
+
actor_id: "user",
|
|
525
|
+
confirmation_ref: `pi-confirm-${createHash("sha256").update(`${taskId}\0${now}\0${nonce}`).digest("hex").slice(0, 16)}`,
|
|
526
|
+
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
527
|
+
nonce,
|
|
528
|
+
};
|
|
529
|
+
const capability = registry.issue(binding);
|
|
530
|
+
const input = {
|
|
531
|
+
task_id: taskId,
|
|
532
|
+
intent_path: binding.intent_path,
|
|
533
|
+
intent_revision: binding.intent_revision,
|
|
534
|
+
preparation_digest: binding.preparation_digest,
|
|
535
|
+
capability,
|
|
536
|
+
capability_binding: binding,
|
|
537
|
+
now,
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
progress("rehearsing", "Running the zero-write Kernel owner rehearsal");
|
|
541
|
+
const rehearsal = await runEnrollmentRehearsal(root, input, capability, registry);
|
|
542
|
+
if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready")
|
|
543
|
+
return terminal(action, taskId, "failed", stage, `Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`, "resolve the final-lock preconditions and retry");
|
|
544
|
+
if (signal.aborted) return cancelled();
|
|
545
|
+
if (!beginCommit()) return cancelled();
|
|
546
|
+
|
|
547
|
+
stage = "committing";
|
|
548
|
+
onUpdate?.(updateResult(action, taskId, stage, "Kernel enrollment commit owns settlement and is no longer cancellable"));
|
|
549
|
+
try {
|
|
550
|
+
const result = await enrollCanaryTask(root, input, registry);
|
|
551
|
+
return terminal(
|
|
552
|
+
action,
|
|
553
|
+
taskId,
|
|
554
|
+
"completed",
|
|
555
|
+
stage,
|
|
556
|
+
`Kernel enrollment completed: task ${result.record.task_id} state=${result.record.lifecycle}:${result.record.artifact_state} backend=${result.backend_claim.backend}`,
|
|
557
|
+
"continue with imm-loop",
|
|
558
|
+
);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
return classifyCommitFailure(root, action, taskId, now, error);
|
|
561
|
+
}
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (signal.aborted) return cancelled();
|
|
564
|
+
return terminal(action, taskId, "failed", stage, `Foreground enrollment failed during ${stage}: ${errorMessage(error)}`, "correct the reported failure and retry");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export default function (pi: ExtensionAPI) {
|
|
569
|
+
const coordinator = new ForegroundEnrollmentCoordinator();
|
|
570
|
+
let registryPromise: ReturnType<typeof createEnrollmentAuthorityRegistry> | undefined;
|
|
571
|
+
const registry = () => registryPromise ??= createEnrollmentAuthorityRegistry();
|
|
572
|
+
|
|
573
|
+
pi.registerTool({
|
|
574
|
+
name: "imm_canary_enrollment",
|
|
575
|
+
label: "Foreground Kernel enrollment",
|
|
576
|
+
description: "Run one exact Kernel new-task enrollment synchronously in the foreground with host cancellation before authority commit.",
|
|
577
|
+
promptSnippet: "Kernel enrollment: invoke once in foreground and consume the direct terminal result.",
|
|
578
|
+
promptGuidelines: [
|
|
579
|
+
"Call from the Parent after natural-language routing selects Enrollment; execute once in the foreground and consume the terminal Tool result.",
|
|
580
|
+
"Do not run this Tool in background, poll for completion, or issue a cancel subcommand; host cancellation is the only pre-commit cancellation path.",
|
|
581
|
+
],
|
|
582
|
+
parameters: Type.Object(
|
|
583
|
+
{
|
|
584
|
+
action: Type.Literal("new"),
|
|
585
|
+
task_id: Type.String({ pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" }),
|
|
586
|
+
},
|
|
587
|
+
{ additionalProperties: false },
|
|
588
|
+
),
|
|
589
|
+
execute: async (
|
|
590
|
+
_toolCallId: string,
|
|
591
|
+
params: { action: EnrollmentAction; task_id: string },
|
|
592
|
+
signal: AbortSignal | undefined,
|
|
593
|
+
onUpdate: ((update: ReturnType<typeof enrollmentToolResult>) => void) | undefined,
|
|
594
|
+
ctx: ExtensionContext,
|
|
595
|
+
) => {
|
|
596
|
+
const { action, task_id: taskId } = params;
|
|
597
|
+
if (action !== "new" || !TASK_ID_PATTERN.test(taskId))
|
|
598
|
+
throwToolFailure({
|
|
599
|
+
tool: "imm_canary_enrollment",
|
|
600
|
+
task_id: taskId,
|
|
601
|
+
operation: action,
|
|
602
|
+
state: "blocked",
|
|
603
|
+
code: "invalid_request",
|
|
604
|
+
message: `invalid task id or enrollment action: ${taskId}`,
|
|
605
|
+
next_action: "invoke the launcher with one canonical task id",
|
|
606
|
+
});
|
|
607
|
+
if (ctx.mode !== "tui")
|
|
608
|
+
throwToolFailure({
|
|
609
|
+
tool: "imm_canary_enrollment",
|
|
610
|
+
task_id: taskId,
|
|
611
|
+
operation: action,
|
|
612
|
+
state: "blocked",
|
|
613
|
+
code: "tui_required",
|
|
614
|
+
message: "imm_canary_enrollment is TUI-only",
|
|
615
|
+
next_action: "invoke the TUI launcher",
|
|
616
|
+
});
|
|
617
|
+
const authorityRegistry = await registry();
|
|
618
|
+
const result = await coordinator.run(action, taskId, signal, (foregroundSignal, beginCommit) =>
|
|
619
|
+
executeForegroundEnrollment(
|
|
620
|
+
ctx.cwd,
|
|
621
|
+
action,
|
|
622
|
+
taskId,
|
|
623
|
+
foregroundSignal,
|
|
624
|
+
beginCommit,
|
|
625
|
+
onUpdate,
|
|
626
|
+
ctx,
|
|
627
|
+
authorityRegistry,
|
|
628
|
+
pi,
|
|
629
|
+
));
|
|
630
|
+
const rendered = enrollmentToolResult(result);
|
|
631
|
+
presentTaskRailResult(ctx, taskId, rendered.details);
|
|
632
|
+
if (isToolFailureState(result.state))
|
|
633
|
+
throwToolFailure({
|
|
634
|
+
tool: "imm_canary_enrollment",
|
|
635
|
+
task_id: taskId,
|
|
636
|
+
operation: action,
|
|
637
|
+
state: result.state,
|
|
638
|
+
code: `enrollment_${result.state}`,
|
|
639
|
+
message: result.summary,
|
|
640
|
+
next_action: result.next_action,
|
|
641
|
+
});
|
|
642
|
+
return rendered;
|
|
643
|
+
},
|
|
644
|
+
renderCall(args, theme) {
|
|
645
|
+
const params = args as { action?: string; task_id?: string };
|
|
646
|
+
return renderStructuredCall(
|
|
647
|
+
"imm_canary_enrollment",
|
|
648
|
+
params.action ?? "unknown",
|
|
649
|
+
params.task_id,
|
|
650
|
+
theme,
|
|
651
|
+
);
|
|
652
|
+
},
|
|
653
|
+
renderResult(result, _options, theme) {
|
|
654
|
+
return renderStructuredResult(
|
|
655
|
+
result as Parameters<typeof renderStructuredResult>[0],
|
|
656
|
+
theme,
|
|
657
|
+
);
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
if (typeof pi.on === "function")
|
|
662
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
663
|
+
resetInteractionPresentation(ctx);
|
|
664
|
+
await coordinator.shutdown();
|
|
665
|
+
});
|
|
666
|
+
}
|