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,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permanent storage path vocabulary and read-only layout inspection
|
|
3
|
+
* (storage-layout cutover).
|
|
4
|
+
*
|
|
5
|
+
* This module is a leaf: it imports no other Kernel module so storage.ts and
|
|
6
|
+
* backend_claim.ts can depend on it without a cycle. `.imm/state/` is the sole
|
|
7
|
+
* ignored mutable authority store; `.imm/audit/` is the sole tracked terminal
|
|
8
|
+
* evidence store. Old-layout paths are enumerated here ONLY so the layout
|
|
9
|
+
* inspection and the one-release migrator can recognize them; no normal
|
|
10
|
+
* runtime code reads old mutable authority through these constants.
|
|
11
|
+
*
|
|
12
|
+
* `inspectStorageLayout` is read-only and returns exactly one of:
|
|
13
|
+
* ready | migration_required | migration_blocked_active |
|
|
14
|
+
* migration_uncommitted | recovery_required | invalid.
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import {
|
|
18
|
+
constants as FS_CONSTANTS,
|
|
19
|
+
lstatSync,
|
|
20
|
+
openSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
closeSync,
|
|
24
|
+
fstatSync,
|
|
25
|
+
} from "node:fs";
|
|
26
|
+
import { resolve } from "node:path";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// New permanent layout paths
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export const STATE_RELATIVE = ".imm/state";
|
|
33
|
+
export const AUDIT_RELATIVE = ".imm/audit";
|
|
34
|
+
export const LEGACY_V3_RELATIVE = ".imm/audit/legacy-v3";
|
|
35
|
+
export const JOURNAL_RELATIVE = ".imm/state/journal.jsonl";
|
|
36
|
+
export const MIGRATION_MARKER_RELATIVE =
|
|
37
|
+
".imm/state/transactions/storage-layout-migration.json";
|
|
38
|
+
|
|
39
|
+
/* Transaction marker file names under `.imm/state/transactions/`. */
|
|
40
|
+
export const KERNEL_TRANSACTION_MARKERS = [
|
|
41
|
+
"workspace-transaction-v2.json",
|
|
42
|
+
"enrollment-marker.json",
|
|
43
|
+
"drain-transaction.json",
|
|
44
|
+
"terminal-transaction.json",
|
|
45
|
+
"authority-repair-transaction.json",
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
function validateTaskId(taskId: string): void {
|
|
49
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(taskId))
|
|
50
|
+
throw new Error(
|
|
51
|
+
`task id must match [A-Za-z0-9][A-Za-z0-9._-]{0,127}: ${taskId}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function stateTaskRecordPath(taskId: string): string {
|
|
56
|
+
validateTaskId(taskId);
|
|
57
|
+
return `${STATE_RELATIVE}/tasks/${taskId}.json`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function stateWorkspacePath(): string {
|
|
61
|
+
return `${STATE_RELATIVE}/workspace.json`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function stateClaimPath(): string {
|
|
65
|
+
return `${STATE_RELATIVE}/active-claim.json`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function stateStoreLockPath(): string {
|
|
69
|
+
return `${STATE_RELATIVE}/locks/kernel-store.lock`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function stateTransactionPath(name: string): string {
|
|
73
|
+
if (!/^[A-Za-z0-9._-]+\.json$/.test(name))
|
|
74
|
+
throw new Error(`invalid transaction marker name: ${name}`);
|
|
75
|
+
return `${STATE_RELATIVE}/transactions/${name}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function auditTaskDirPath(taskId: string): string {
|
|
79
|
+
validateTaskId(taskId);
|
|
80
|
+
return `${AUDIT_RELATIVE}/${taskId}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function auditTaskRecordPath(taskId: string): string {
|
|
84
|
+
return `${auditTaskDirPath(taskId)}/task-record.json`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function auditTerminalProofPath(taskId: string): string {
|
|
88
|
+
return `${auditTaskDirPath(taskId)}/terminal-proof.json`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Historical v3 machine evidence root under the tracked audit store. */
|
|
92
|
+
export function legacyV3Path(...segments: string[]): string {
|
|
93
|
+
for (const segment of segments)
|
|
94
|
+
if (!segment || segment.includes("/") || segment.includes("\\"))
|
|
95
|
+
throw new Error(`invalid legacy-v3 segment: ${segment}`);
|
|
96
|
+
return [LEGACY_V3_RELATIVE, ...segments].join("/");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Old layout recognition (inspection and one-release migrator only)
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
export const LEGACY_CLAIM_RELATIVE = ".imm/tasks/.backend-claim.json";
|
|
104
|
+
export const LEGACY_WORKSPACE_RELATIVE = ".imm/workspace.json";
|
|
105
|
+
export const LEGACY_TASKS_RELATIVE = ".imm/tasks";
|
|
106
|
+
export const LEGACY_MEMORY_RELATIVE = ".imm/memory";
|
|
107
|
+
export const LEGACY_TEMPLATES_RELATIVE = ".imm/templates";
|
|
108
|
+
export const LEGACY_JOURNAL_RELATIVE = ".imm/journal.jsonl";
|
|
109
|
+
export const LEGACY_AUTHORITY_RELATIVE = ".imm/authority";
|
|
110
|
+
|
|
111
|
+
export const LEGACY_KNOWN_FILES = {
|
|
112
|
+
".imm/tasks/.workspace.lock": "lock",
|
|
113
|
+
".imm/tasks/.journal.lock": "lock",
|
|
114
|
+
".imm/tasks/.workspace-transaction.json": "old-marker",
|
|
115
|
+
".imm/tasks/.workspace-transaction-v2.json": "old-marker",
|
|
116
|
+
".imm/tasks/.enrollment-marker.json": "old-marker",
|
|
117
|
+
".imm/tasks/.drain-transaction.json": "old-marker",
|
|
118
|
+
".imm/tasks/.terminal-transaction.json": "old-marker",
|
|
119
|
+
".imm/tasks/.authority-repair-transaction.json": "old-marker",
|
|
120
|
+
".imm/tasks/.backend-claim.json": "claim",
|
|
121
|
+
".imm/memory/current_iteration.json": "memory",
|
|
122
|
+
".imm/memory/current_iteration_history.jsonl": "memory",
|
|
123
|
+
".imm/memory/dispatch_telemetry.jsonl": "memory",
|
|
124
|
+
".imm/memory/.current_iteration.authority_commit_receipts.jsonl": "memory",
|
|
125
|
+
".imm/memory/.current_iteration.automatic_observations.jsonl": "memory",
|
|
126
|
+
".imm/memory/.current_iteration.automatic_observations.lock": "memory",
|
|
127
|
+
".imm/memory/MEMORY.md": "retired",
|
|
128
|
+
".imm/templates/iteration-plan-template.md": "retired",
|
|
129
|
+
".imm/templates/review-report-template.md": "retired",
|
|
130
|
+
} as const;
|
|
131
|
+
|
|
132
|
+
/** Old task-scoped owner files: `<task-id>.json` and `<task-id>.backend-claim.json`. */
|
|
133
|
+
const TASK_OWNER_FILE = /^([A-Za-z0-9][A-Za-z0-9._-]{0,127})\.(json|backend-claim\.json)$/;
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Layout inspection
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
export type StorageLayoutStatus =
|
|
140
|
+
| "ready"
|
|
141
|
+
| "migration_required"
|
|
142
|
+
| "migration_blocked_active"
|
|
143
|
+
| "migration_uncommitted"
|
|
144
|
+
| "recovery_required"
|
|
145
|
+
| "invalid";
|
|
146
|
+
|
|
147
|
+
export interface StorageLayoutInspection {
|
|
148
|
+
contract: "assurance_kernel/storage_layout_inspection/v1";
|
|
149
|
+
layout: StorageLayoutStatus;
|
|
150
|
+
old_authority_present: boolean;
|
|
151
|
+
pending_marker: string | null;
|
|
152
|
+
dirty_affected_paths: string[];
|
|
153
|
+
reason: string | null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
type EntryStatus = "absent" | "file" | "directory" | "symlink" | "other";
|
|
157
|
+
|
|
158
|
+
function entryStatus(root: string, relativePath: string): EntryStatus {
|
|
159
|
+
const candidate = resolve(root, relativePath);
|
|
160
|
+
let stat;
|
|
161
|
+
try {
|
|
162
|
+
stat = lstatSync(candidate);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
165
|
+
if (code === "ENOENT" || code === "ENOTDIR") return "absent";
|
|
166
|
+
return "other";
|
|
167
|
+
}
|
|
168
|
+
if (stat.isSymbolicLink()) return "symlink";
|
|
169
|
+
if (stat.isFile()) return "file";
|
|
170
|
+
if (stat.isDirectory()) return "directory";
|
|
171
|
+
return "other";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function listEntries(root: string, relativePath: string): string[] | null {
|
|
175
|
+
const candidate = resolve(root, relativePath);
|
|
176
|
+
try {
|
|
177
|
+
return readdirSync(candidate).sort();
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readSmallFile(root: string, relativePath: string): string | null {
|
|
184
|
+
const candidate = resolve(root, relativePath);
|
|
185
|
+
try {
|
|
186
|
+
const fd = openSync(candidate, FS_CONSTANTS.O_RDONLY | (FS_CONSTANTS.O_NOFOLLOW ?? 0));
|
|
187
|
+
try {
|
|
188
|
+
const stat = fstatSync(fd);
|
|
189
|
+
if (stat.size > 4 * 1024 * 1024) throw new Error("file exceeds the inspection read bound");
|
|
190
|
+
return readFileSync(fd, "utf8");
|
|
191
|
+
} finally {
|
|
192
|
+
closeSync(fd);
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface OldLayoutFacts {
|
|
200
|
+
old_authority_present: boolean;
|
|
201
|
+
blocked_active: boolean;
|
|
202
|
+
pending_marker: string | null;
|
|
203
|
+
fail_reason: string | null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readJsonField(root: string, path: string, field: string): unknown {
|
|
207
|
+
const content = readSmallFile(root, path);
|
|
208
|
+
if (content === null) throw new Error(`${path} is unreadable`);
|
|
209
|
+
try {
|
|
210
|
+
return (JSON.parse(content) as Record<string, unknown>)[field];
|
|
211
|
+
} catch {
|
|
212
|
+
throw new Error(`${path} is not valid JSON`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Inspect every known old path and the .imm/tasks owner file set. */
|
|
217
|
+
function inspectOldLayout(root: string): OldLayoutFacts {
|
|
218
|
+
const facts: OldLayoutFacts = {
|
|
219
|
+
old_authority_present: false,
|
|
220
|
+
blocked_active: false,
|
|
221
|
+
pending_marker: null,
|
|
222
|
+
fail_reason: null,
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
225
|
+
// 1. Known old files, one by one. Locks are inert; markers, the claim,
|
|
226
|
+
// non-idle memory, and active records carry authority signals.
|
|
227
|
+
for (const [path, kind] of Object.entries(LEGACY_KNOWN_FILES)) {
|
|
228
|
+
const status = entryStatus(root, path);
|
|
229
|
+
if (status === "absent") continue;
|
|
230
|
+
if (status === "symlink" || status === "other")
|
|
231
|
+
throw new Error(`${path} is ${status}`);
|
|
232
|
+
if (status !== "file") throw new Error(`${path} is not a regular file`);
|
|
233
|
+
facts.old_authority_present = true;
|
|
234
|
+
if (kind === "lock") continue;
|
|
235
|
+
if (kind === "old-marker") {
|
|
236
|
+
// review-1: legacy Kernel transaction markers require the
|
|
237
|
+
// prior runtime to settle/recover; they never auto-recover
|
|
238
|
+
// through the new runtime or the migrator. They block
|
|
239
|
+
// migration (migration_blocked_active) and never set
|
|
240
|
+
// pending_marker, which is reserved for new-layout markers
|
|
241
|
+
// the new runtime CAN recover.
|
|
242
|
+
facts.blocked_active = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (kind === "marker") {
|
|
246
|
+
facts.pending_marker ??= path;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (kind === "claim") {
|
|
250
|
+
facts.blocked_active = true;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (kind === "memory" && path === ".imm/memory/current_iteration.json") {
|
|
254
|
+
const runtimeStatus = readJsonField(root, path, "runtime_status");
|
|
255
|
+
if (typeof runtimeStatus === "string" && runtimeStatus !== "idle")
|
|
256
|
+
facts.blocked_active = true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 2. .imm/tasks directory: reject unknown files; active records block.
|
|
261
|
+
const tasksStatus = entryStatus(root, LEGACY_TASKS_RELATIVE);
|
|
262
|
+
if (tasksStatus === "symlink" || tasksStatus === "other")
|
|
263
|
+
throw new Error(`${LEGACY_TASKS_RELATIVE} is ${tasksStatus}`);
|
|
264
|
+
if (tasksStatus === "directory") {
|
|
265
|
+
facts.old_authority_present = true;
|
|
266
|
+
const entries = listEntries(root, LEGACY_TASKS_RELATIVE) ?? [];
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
const full = `.imm/tasks/${entry}`;
|
|
269
|
+
if (full in LEGACY_KNOWN_FILES) continue;
|
|
270
|
+
const matched = TASK_OWNER_FILE.exec(entry);
|
|
271
|
+
if (!matched)
|
|
272
|
+
throw new Error(`unknown file under .imm/tasks: ${entry}`);
|
|
273
|
+
if (entry.endsWith(".backend-claim.json")) continue; // tombstone
|
|
274
|
+
const lifecycle = readJsonField(root, `.imm/tasks/${entry}`, "lifecycle")
|
|
275
|
+
?? readJsonField(root, `.imm/tasks/${entry}`, "phase");
|
|
276
|
+
if (lifecycle !== "done" && lifecycle !== "stopped")
|
|
277
|
+
facts.blocked_active = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 3. Workspace owner is the third active-owner signal.
|
|
282
|
+
if (entryStatus(root, LEGACY_WORKSPACE_RELATIVE) !== "absent") {
|
|
283
|
+
const workspaceStatus = entryStatus(root, LEGACY_WORKSPACE_RELATIVE);
|
|
284
|
+
if (workspaceStatus === "symlink" || workspaceStatus === "other")
|
|
285
|
+
throw new Error(`${LEGACY_WORKSPACE_RELATIVE} is ${workspaceStatus}`);
|
|
286
|
+
if (workspaceStatus !== "file")
|
|
287
|
+
throw new Error(`${LEGACY_WORKSPACE_RELATIVE} is not a regular file`);
|
|
288
|
+
facts.old_authority_present = true;
|
|
289
|
+
const owner = readJsonField(root, LEGACY_WORKSPACE_RELATIVE, "current_working");
|
|
290
|
+
if (typeof owner === "string" && owner.length > 0)
|
|
291
|
+
facts.blocked_active = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 4. Old mutable directories and journal are old layout (owner-free).
|
|
295
|
+
for (const dir of [
|
|
296
|
+
LEGACY_MEMORY_RELATIVE,
|
|
297
|
+
LEGACY_TEMPLATES_RELATIVE,
|
|
298
|
+
LEGACY_AUTHORITY_RELATIVE,
|
|
299
|
+
]) {
|
|
300
|
+
const status = entryStatus(root, dir);
|
|
301
|
+
if (status === "absent") continue;
|
|
302
|
+
if (status === "symlink" || status === "other")
|
|
303
|
+
throw new Error(`${dir} is ${status}`);
|
|
304
|
+
if (status !== "directory") throw new Error(`${dir} is not a directory`);
|
|
305
|
+
facts.old_authority_present = true;
|
|
306
|
+
// Unknown entries in these directories fail closed like the migrator.
|
|
307
|
+
for (const entry of listEntries(root, dir) ?? []) {
|
|
308
|
+
const full = `${dir}/${entry}`;
|
|
309
|
+
const known = LEGACY_KNOWN_FILES[full as keyof typeof LEGACY_KNOWN_FILES];
|
|
310
|
+
if (known === undefined)
|
|
311
|
+
throw new Error(`unknown file under ${dir}: ${entry}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (entryStatus(root, LEGACY_JOURNAL_RELATIVE) !== "absent") {
|
|
315
|
+
const journalStatus = entryStatus(root, LEGACY_JOURNAL_RELATIVE);
|
|
316
|
+
if (journalStatus === "symlink" || journalStatus === "other")
|
|
317
|
+
throw new Error(`${LEGACY_JOURNAL_RELATIVE} is ${journalStatus}`);
|
|
318
|
+
if (journalStatus !== "file")
|
|
319
|
+
throw new Error(`${LEGACY_JOURNAL_RELATIVE} is not a regular file`);
|
|
320
|
+
facts.old_authority_present = true;
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
facts.fail_reason = error instanceof Error ? error.message : String(error);
|
|
324
|
+
}
|
|
325
|
+
return facts;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const AFFECTED_GIT_PREFIXES = [
|
|
329
|
+
".imm/audit/",
|
|
330
|
+
".imm/tasks/",
|
|
331
|
+
".imm/workspace.json",
|
|
332
|
+
".imm/memory/",
|
|
333
|
+
".imm/templates/",
|
|
334
|
+
".imm/authority/",
|
|
335
|
+
".imm/journal.jsonl",
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
function gitDirtyAffected(root: string): string[] | null {
|
|
339
|
+
const tracked = spawnSync(
|
|
340
|
+
"git", ["-C", root, "diff", "--name-only", "-z", "HEAD", "--"],
|
|
341
|
+
{ encoding: "utf8" },
|
|
342
|
+
);
|
|
343
|
+
const untracked = spawnSync(
|
|
344
|
+
"git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"],
|
|
345
|
+
{ encoding: "utf8" },
|
|
346
|
+
);
|
|
347
|
+
if (tracked.status !== 0 || untracked.status !== 0) return null;
|
|
348
|
+
const dirty = new Set<string>();
|
|
349
|
+
for (const output of [tracked.stdout, untracked.stdout]) {
|
|
350
|
+
for (const path of output.split("\0")) {
|
|
351
|
+
if (!path) continue;
|
|
352
|
+
if (AFFECTED_GIT_PREFIXES.some((prefix) => path === prefix || path.startsWith(prefix)))
|
|
353
|
+
dirty.add(path);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return [...dirty].sort();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function pendingNewMarker(root: string): string | null {
|
|
360
|
+
const entries = listEntries(root, ".imm/state/transactions");
|
|
361
|
+
if (!entries) return null;
|
|
362
|
+
for (const entry of entries) {
|
|
363
|
+
if (entry === "storage-layout-migration.json") return `.imm/state/transactions/${entry}`;
|
|
364
|
+
if ((KERNEL_TRANSACTION_MARKERS as readonly string[]).includes(entry))
|
|
365
|
+
return `.imm/state/transactions/${entry}`;
|
|
366
|
+
}
|
|
367
|
+
return entries.find((entry) => entry.endsWith(".json"))
|
|
368
|
+
? `.imm/state/transactions/${entries.find((entry) => entry.endsWith(".json")) ?? ""}`
|
|
369
|
+
: null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Validate that the new-layout roots are not symlinked outside the
|
|
373
|
+
* repository (review-10). Called before any lock acquisition or write. */
|
|
374
|
+
function assertNewLayoutRootsSafe(root: string): string | null {
|
|
375
|
+
for (const relative of [STATE_RELATIVE, AUDIT_RELATIVE]) {
|
|
376
|
+
const path = resolve(root, relative);
|
|
377
|
+
let stat;
|
|
378
|
+
try {
|
|
379
|
+
stat = lstatSync(path);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
382
|
+
if (code === "ENOENT" || code === "ENOTDIR") continue;
|
|
383
|
+
return `${relative} is unreadable`;
|
|
384
|
+
}
|
|
385
|
+
if (stat.isSymbolicLink())
|
|
386
|
+
return `${relative} is a symlink`;
|
|
387
|
+
// Walk parent segments to detect symlinked ancestors.
|
|
388
|
+
let cursor = resolve(root);
|
|
389
|
+
for (const segment of relative.split("/")) {
|
|
390
|
+
cursor = resolve(cursor, segment);
|
|
391
|
+
try {
|
|
392
|
+
const parentStat = lstatSync(cursor);
|
|
393
|
+
if (parentStat.isSymbolicLink())
|
|
394
|
+
return `${relative} traverses a symlink parent`;
|
|
395
|
+
} catch {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function inspectStorageLayout(root: string): StorageLayoutInspection {
|
|
404
|
+
const rootFailure = assertNewLayoutRootsSafe(root);
|
|
405
|
+
if (rootFailure) {
|
|
406
|
+
return {
|
|
407
|
+
contract: "assurance_kernel/storage_layout_inspection/v1",
|
|
408
|
+
layout: "invalid",
|
|
409
|
+
old_authority_present: false,
|
|
410
|
+
pending_marker: null,
|
|
411
|
+
dirty_affected_paths: [],
|
|
412
|
+
reason: rootFailure,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const oldFacts = inspectOldLayout(root);
|
|
416
|
+
if (oldFacts.fail_reason) {
|
|
417
|
+
return {
|
|
418
|
+
contract: "assurance_kernel/storage_layout_inspection/v1",
|
|
419
|
+
layout: "invalid",
|
|
420
|
+
old_authority_present: oldFacts.old_authority_present,
|
|
421
|
+
pending_marker: null,
|
|
422
|
+
dirty_affected_paths: [],
|
|
423
|
+
reason: oldFacts.fail_reason,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const newMarker = pendingNewMarker(root);
|
|
428
|
+
const auditPresent = entryStatus(root, AUDIT_RELATIVE) !== "absent";
|
|
429
|
+
const dirty = gitDirtyAffected(root);
|
|
430
|
+
|
|
431
|
+
if (oldFacts.pending_marker || newMarker) {
|
|
432
|
+
return {
|
|
433
|
+
contract: "assurance_kernel/storage_layout_inspection/v1",
|
|
434
|
+
layout: "recovery_required",
|
|
435
|
+
old_authority_present: oldFacts.old_authority_present,
|
|
436
|
+
pending_marker: oldFacts.pending_marker ?? newMarker,
|
|
437
|
+
dirty_affected_paths: dirty ?? [],
|
|
438
|
+
reason: "a recoverable migration or Kernel transaction marker exists; mutation must recover it under lock first",
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (oldFacts.blocked_active) {
|
|
442
|
+
return {
|
|
443
|
+
contract: "assurance_kernel/storage_layout_inspection/v1",
|
|
444
|
+
layout: "migration_blocked_active",
|
|
445
|
+
old_authority_present: true,
|
|
446
|
+
pending_marker: null,
|
|
447
|
+
dirty_affected_paths: dirty ?? [],
|
|
448
|
+
reason: "an active claim, nonterminal TaskRecord, non-null workspace owner, or non-idle v3 Ledger exists in the old layout; settle or stop it with the prior runtime first",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (oldFacts.old_authority_present) {
|
|
452
|
+
return {
|
|
453
|
+
contract: "assurance_kernel/storage_layout_inspection/v1",
|
|
454
|
+
layout: "migration_required",
|
|
455
|
+
old_authority_present: true,
|
|
456
|
+
pending_marker: null,
|
|
457
|
+
dirty_affected_paths: dirty ?? [],
|
|
458
|
+
reason: "an owner-free legacy layout exists; the next eligible stateful mutation runs the one-release migration and stops",
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (dirty !== null && dirty.length > 0) {
|
|
462
|
+
// review-2: cleanup-only migrations (deleted templates, MEMORY.md,
|
|
463
|
+
// owner-free workspace) still leave an affected diff that must be
|
|
464
|
+
// committed before the layout can be ready.
|
|
465
|
+
return {
|
|
466
|
+
contract: "assurance_kernel/storage_layout_inspection/v1",
|
|
467
|
+
layout: "migration_uncommitted",
|
|
468
|
+
old_authority_present: false,
|
|
469
|
+
pending_marker: null,
|
|
470
|
+
dirty_affected_paths: dirty,
|
|
471
|
+
reason: "affected audit or retired legacy paths differ from HEAD; commit the migration diff before any managed mutation",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (auditPresent && dirty === null) {
|
|
475
|
+
return {
|
|
476
|
+
contract: "assurance_kernel/storage_layout_inspection/v1",
|
|
477
|
+
layout: "invalid",
|
|
478
|
+
old_authority_present: false,
|
|
479
|
+
pending_marker: null,
|
|
480
|
+
dirty_affected_paths: [],
|
|
481
|
+
reason: "audit evidence exists but the Git workspace is unavailable; committed state cannot be verified",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
contract: "assurance_kernel/storage_layout_inspection/v1",
|
|
486
|
+
layout: "ready",
|
|
487
|
+
old_authority_present: false,
|
|
488
|
+
pending_marker: null,
|
|
489
|
+
dirty_affected_paths: [],
|
|
490
|
+
reason: null,
|
|
491
|
+
};
|
|
492
|
+
}
|