immune-brain 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +239 -0
  2. package/README.zh-CN.md +239 -0
  3. package/package.json +84 -0
  4. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
  6. package/plugins/immune-brain/.pi-extension/package.json +11 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
  9. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
  10. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
  11. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
  12. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
  13. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
  14. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
  15. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
  16. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
  17. package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
  18. package/plugins/immune-brain/bin/imm-kernel +4 -0
  19. package/plugins/immune-brain/bin/imm-plan +4 -0
  20. package/plugins/immune-brain/bin/imm-pr-diag +230 -0
  21. package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
  22. package/plugins/immune-brain/bin/imm-retired +4 -0
  23. package/plugins/immune-brain/bin/imm-tracker +4 -0
  24. package/plugins/immune-brain/dist/BASELINE.md +138 -0
  25. package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
  26. package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
  27. package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
  28. package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
  29. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
  30. package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
  31. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
  32. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
  33. package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
  34. package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
  35. package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
  36. package/plugins/immune-brain/dist/imm-loop.md +158 -0
  37. package/plugins/immune-brain/dist/imm-planner.md +387 -0
  38. package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
  39. package/plugins/immune-brain/dist/registry.yaml +49 -0
  40. package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
  41. package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
  42. package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
  43. package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
  44. package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
  45. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
  46. package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
  47. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
  48. package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
  49. package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
  50. package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
  51. package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
  52. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
  53. package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
  54. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
  55. package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
  56. package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
  57. package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
  58. package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
  59. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
  60. package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
  61. package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
  62. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
  63. package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
  64. package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
  65. package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
  66. package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
  67. package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
  68. package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
  69. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
  70. package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
  71. package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
  72. package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
  73. package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
  74. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
  75. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
  76. package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
  77. package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
  78. package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
  79. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
  80. package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
  81. package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
  82. package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
  83. package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
  84. package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
  85. package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
  86. package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
  87. package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
  88. package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
  89. package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
  90. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
  91. package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
  92. package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
  93. package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
  94. package/plugins/immune-brain/skills/.ignore +1 -0
  95. package/plugins/immune-brain/skills/BASELINE.md +138 -0
  96. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
  97. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
  98. package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
  99. package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
  100. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
  101. package/plugins/immune-brain/skills/registry.yaml +49 -0
@@ -0,0 +1,208 @@
1
+ // P2B2 mutation authority consumption port. NOT exported from kernel/index.ts.
2
+ // Registry/application pairing: each registry owns its capability state in a
3
+ // closure-private WeakMap, and capabilities are only recognized by the
4
+ // registry that issued them. No module-level singleton and no test issuer
5
+ // exist here; the production registry/application pair is created inside the
6
+ // Pi lifecycle extension activation closure, and tests issue capabilities
7
+ // through tests/fixtures/mutation-authority-test-seam.ts.
8
+
9
+ import { createHash } from "node:crypto";
10
+
11
+ import {
12
+ MUTATION_AUTHORITY_CAPABILITY_BRAND,
13
+ type AuthorityAuditDescriptor,
14
+ type MutationAuthorityCapabilityV2,
15
+ type MutationAuthorityKind,
16
+ type TaskAction,
17
+ } from "./types";
18
+
19
+ export interface CapabilityBindingV2 {
20
+ authority_kind: MutationAuthorityKind;
21
+ task_id: string;
22
+ action_digest: string;
23
+ expected_record_hash: string;
24
+ intent_revision: number;
25
+ intent_content_hash: string;
26
+ diff_hash: string;
27
+ actor_id: string;
28
+ confirmation_ref: string;
29
+ expires_at: string;
30
+ /** Normalized findings digest required for request_rework; null for other operations. */
31
+ findings_digest: string | null;
32
+ }
33
+
34
+ interface CapabilityState extends CapabilityBindingV2 {
35
+ issued_at: string;
36
+ consumed: boolean;
37
+ }
38
+
39
+ export interface ValidatedAuthorityV2 {
40
+ audit: AuthorityAuditDescriptor;
41
+ action_digest: string;
42
+ expected_record_hash: string;
43
+ intent_revision: number;
44
+ intent_content_hash: string;
45
+ diff_hash: string;
46
+ task_id: string;
47
+ }
48
+
49
+ export interface MutationAuthorityInspection {
50
+ task_id: string;
51
+ action: TaskAction;
52
+ expected_record_hash: string;
53
+ intent_revision: number;
54
+ intent_content_hash: string;
55
+ diff_hash: string;
56
+ /** Present only for request_rework bindings. */
57
+ findings_digest?: string;
58
+ }
59
+
60
+ export interface MutationAuthorityRegistry {
61
+ readonly brand: symbol;
62
+ /** Issue one capability bound to an exact binding. Library primitive; no production caller in P2B2. */
63
+ issue(binding: CapabilityBindingV2, issuedAt?: string): MutationAuthorityCapabilityV2;
64
+ /** Inspect without consuming. Fails on missing, expired, mismatched, or reused capability. */
65
+ inspect(
66
+ capability: MutationAuthorityCapabilityV2 | undefined,
67
+ expected: MutationAuthorityInspection,
68
+ now?: number,
69
+ ): ValidatedAuthorityV2;
70
+ /** Consume irreversibly. Returns the validated authority. */
71
+ consume(
72
+ capability: MutationAuthorityCapabilityV2,
73
+ expected: MutationAuthorityInspection,
74
+ now?: number,
75
+ ): ValidatedAuthorityV2;
76
+ isConsumed(capability: MutationAuthorityCapabilityV2): boolean;
77
+ }
78
+
79
+ export function digestOfAction(action: TaskAction): string {
80
+ const { expected_record_hash: _r, expected_workspace_hash: _w, diff_hash: _d, ...rest } = action;
81
+ return createHash("sha256")
82
+ .update(JSON.stringify(rest))
83
+ .digest("hex");
84
+ }
85
+
86
+ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
87
+ const states = new WeakMap<object, CapabilityState>();
88
+ const brand = Symbol("assurance-kernel-mutation-authority-registry");
89
+
90
+ function isCapability(value: unknown): value is MutationAuthorityCapabilityV2 {
91
+ return (
92
+ !!value &&
93
+ typeof value === "object" &&
94
+ (value as Record<symbol, unknown>)[MUTATION_AUTHORITY_CAPABILITY_BRAND] === true &&
95
+ (value as Record<symbol, unknown>)[brand] === true
96
+ );
97
+ }
98
+
99
+ function stateOf(capability: MutationAuthorityCapabilityV2): CapabilityState {
100
+ const state = states.get(capability);
101
+ if (!state) throw new Error("authority capability is not recognized by this registry");
102
+ return state;
103
+ }
104
+
105
+ function validateBinding(binding: CapabilityBindingV2, issuedAt: string): void {
106
+ const missing: string[] = [];
107
+ for (const [key, value] of Object.entries(binding)) {
108
+ if (key === "findings_digest") continue;
109
+ if (value === undefined || value === null || value === "") missing.push(key);
110
+ }
111
+ if (missing.length > 0)
112
+ throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
113
+ if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
114
+ throw new Error("authority capability findings_digest must be a canonical sha256 hash");
115
+ if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
116
+ throw new Error("authority capability must have a future expiry");
117
+ }
118
+
119
+ return {
120
+ brand,
121
+ issue(binding: CapabilityBindingV2, issuedAt = new Date().toISOString()): MutationAuthorityCapabilityV2 {
122
+ validateBinding(binding, issuedAt);
123
+ const capability = Object.freeze(
124
+ Object.defineProperties(
125
+ {},
126
+ {
127
+ [MUTATION_AUTHORITY_CAPABILITY_BRAND]: {
128
+ value: true,
129
+ enumerable: false,
130
+ writable: false,
131
+ configurable: false,
132
+ },
133
+ [brand]: {
134
+ value: true,
135
+ enumerable: false,
136
+ writable: false,
137
+ configurable: false,
138
+ },
139
+ },
140
+ ),
141
+ ) as MutationAuthorityCapabilityV2;
142
+ states.set(capability, {
143
+ ...binding,
144
+ issued_at: issuedAt,
145
+ consumed: false,
146
+ });
147
+ return capability;
148
+ },
149
+ inspect(
150
+ capability: MutationAuthorityCapabilityV2 | undefined,
151
+ expected: MutationAuthorityInspection,
152
+ now = Date.now(),
153
+ ): ValidatedAuthorityV2 {
154
+ if (!capability || !isCapability(capability))
155
+ throw new Error("privileged action requires an opaque authority capability");
156
+ const state = stateOf(capability);
157
+ if (state.consumed) throw new Error("authority capability is already consumed");
158
+ if (Date.parse(state.expires_at) <= now)
159
+ throw new Error("authority capability has expired");
160
+ const actionDigest = digestOfAction(expected.action);
161
+ if (state.action_digest !== actionDigest)
162
+ throw new Error("authority capability action digest mismatch");
163
+ if (state.task_id !== expected.task_id)
164
+ throw new Error("authority capability task mismatch");
165
+ if (state.expected_record_hash !== expected.expected_record_hash)
166
+ throw new Error("authority capability record hash mismatch");
167
+ if (state.intent_revision !== expected.intent_revision)
168
+ throw new Error("authority capability intent revision mismatch");
169
+ if (state.intent_content_hash !== expected.intent_content_hash)
170
+ throw new Error("authority capability intent hash mismatch");
171
+ if (state.diff_hash !== expected.diff_hash)
172
+ throw new Error("authority capability diff hash mismatch");
173
+ if (expected.findings_digest !== undefined) {
174
+ if (state.findings_digest === null)
175
+ throw new Error("authority capability is not bound to findings");
176
+ if (state.findings_digest !== expected.findings_digest)
177
+ throw new Error("authority capability findings digest mismatch");
178
+ }
179
+ return {
180
+ audit: {
181
+ authority_kind: state.authority_kind,
182
+ actor_id: state.actor_id,
183
+ confirmation_ref: state.confirmation_ref,
184
+ issued_at: state.issued_at,
185
+ expires_at: state.expires_at,
186
+ },
187
+ action_digest: actionDigest,
188
+ expected_record_hash: state.expected_record_hash,
189
+ intent_revision: state.intent_revision,
190
+ intent_content_hash: state.intent_content_hash,
191
+ diff_hash: state.diff_hash,
192
+ task_id: state.task_id,
193
+ };
194
+ },
195
+ consume(
196
+ capability: MutationAuthorityCapabilityV2,
197
+ expected: MutationAuthorityInspection,
198
+ now = Date.now(),
199
+ ): ValidatedAuthorityV2 {
200
+ const validated = this.inspect(capability, expected, now);
201
+ stateOf(capability).consumed = true;
202
+ return validated;
203
+ },
204
+ isConsumed(capability: MutationAuthorityCapabilityV2): boolean {
205
+ return stateOf(capability).consumed;
206
+ },
207
+ };
208
+ }
@@ -0,0 +1,451 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ existsSync,
6
+ fstatSync,
7
+ fsyncSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ openSync,
11
+ readFileSync,
12
+ realpathSync,
13
+ rmSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { dirname, resolve, sep } from "node:path";
17
+ import { stableStringify } from "../canonical_json";
18
+ import {
19
+ AUTHORITY_OBSERVATION_GENERATION_V2,
20
+ AUTHORITY_OBSERVER_VERSION_V2,
21
+ } from "../authority_commit_receipts";
22
+ import type { LegacyMapping, V3CommitSourceEvent } from "./types";
23
+
24
+ const JOURNAL_RELATIVE_PATH =
25
+ ".imm/state/observations/automatic_observations.jsonl";
26
+ const LOCK_RELATIVE_PATH =
27
+ ".imm/state/observations/automatic_observations.lock";
28
+ const MAX_JOURNAL_BYTES = 64 * 1024 * 1024;
29
+
30
+ export interface V3AuthorityObservationV2 {
31
+ contract: "assurance_kernel/v3_authority_observation/v2";
32
+ observation_id: string;
33
+ receipt_record_id: string;
34
+ receipt_attempt_id: string;
35
+ receipt_protocol: "assurance_kernel/authority_commit_receipt/v2";
36
+ receipt_status: "committed" | "recovered_committed";
37
+ source_kind: "state_mutation" | "project_migration";
38
+ source_ref: string;
39
+ state_path_identity: string;
40
+ committed_bytes_sha256: string;
41
+ ledger_revision: string;
42
+ plan_path: string | null;
43
+ plan_signature: string | null;
44
+ source_events: V3CommitSourceEvent[];
45
+ shadow: LegacyMapping;
46
+ divergence: { detected: boolean; fields: string[] };
47
+ observer_generation: typeof AUTHORITY_OBSERVATION_GENERATION_V2;
48
+ observer_version: typeof AUTHORITY_OBSERVER_VERSION_V2;
49
+ committed_at: string;
50
+ observed_at: string;
51
+ }
52
+
53
+ export class AutomaticObservationSecurityError extends Error {}
54
+ export class AutomaticObservationConflictError extends Error {}
55
+
56
+ function sha256(value: string): string {
57
+ return createHash("sha256").update(value).digest("hex");
58
+ }
59
+
60
+ function digest(domain: string, payload: string): string {
61
+ return `sha256:${sha256(`${domain}\0${payload}`)}`;
62
+ }
63
+
64
+ function asRecord(value: unknown): Record<string, unknown> {
65
+ if (!value || typeof value !== "object" || Array.isArray(value))
66
+ throw new AutomaticObservationSecurityError(
67
+ "automatic observation must be an object",
68
+ );
69
+ return value as Record<string, unknown>;
70
+ }
71
+
72
+ function exactKeys(record: Record<string, unknown>, expected: string[]): void {
73
+ if (
74
+ JSON.stringify(Object.keys(record).sort()) !==
75
+ JSON.stringify([...expected].sort())
76
+ )
77
+ throw new AutomaticObservationSecurityError(
78
+ "automatic observation v2 has unknown or missing fields",
79
+ );
80
+ }
81
+
82
+ function nonEmptyString(record: Record<string, unknown>, key: string): string {
83
+ const value = record[key];
84
+ if (typeof value !== "string" || !value)
85
+ throw new AutomaticObservationSecurityError(
86
+ `automatic observation v2 ${key} is invalid`,
87
+ );
88
+ return value;
89
+ }
90
+
91
+ function parseSourceEvents(value: unknown): V3CommitSourceEvent[] {
92
+ if (!Array.isArray(value))
93
+ throw new AutomaticObservationSecurityError(
94
+ "automatic observation v2 source_events is invalid",
95
+ );
96
+ return value.map((entry) => {
97
+ const record = asRecord(entry);
98
+ exactKeys(record, ["action", "at", "id"]);
99
+ const id = nonEmptyString(record, "id");
100
+ const action = nonEmptyString(record, "action");
101
+ if (record.at !== null && typeof record.at !== "string")
102
+ throw new AutomaticObservationSecurityError(
103
+ "automatic observation v2 source event time is invalid",
104
+ );
105
+ return { id, action, at: record.at as string | null };
106
+ });
107
+ }
108
+
109
+ function parseLegacyMapping(value: unknown): LegacyMapping {
110
+ const record = asRecord(value);
111
+ exactKeys(record, ["ambiguous", "phase", "reason", "source_states"]);
112
+ if (
113
+ (record.phase !== null &&
114
+ !(["working", "review", "done", "stopped"] as unknown[]).includes(
115
+ record.phase,
116
+ )) ||
117
+ typeof record.reason !== "string" ||
118
+ typeof record.ambiguous !== "boolean" ||
119
+ !Array.isArray(record.source_states) ||
120
+ record.source_states.some((entry) => typeof entry !== "string")
121
+ )
122
+ throw new AutomaticObservationSecurityError(
123
+ "automatic observation v2 shadow is invalid",
124
+ );
125
+ return record as unknown as LegacyMapping;
126
+ }
127
+
128
+ function parseDivergence(
129
+ value: unknown,
130
+ ): { detected: boolean; fields: string[] } {
131
+ const record = asRecord(value);
132
+ exactKeys(record, ["detected", "fields"]);
133
+ if (
134
+ typeof record.detected !== "boolean" ||
135
+ !Array.isArray(record.fields) ||
136
+ record.fields.some((entry) => typeof entry !== "string") ||
137
+ record.detected !== (record.fields.length > 0)
138
+ )
139
+ throw new AutomaticObservationSecurityError(
140
+ "automatic observation v2 divergence is invalid",
141
+ );
142
+ return { detected: record.detected, fields: [...record.fields] as string[] };
143
+ }
144
+
145
+ export function parseAutomaticObservationV2(
146
+ raw: unknown,
147
+ ): V3AuthorityObservationV2 {
148
+ const record = asRecord(raw);
149
+ exactKeys(record, [
150
+ "committed_at",
151
+ "committed_bytes_sha256",
152
+ "contract",
153
+ "divergence",
154
+ "ledger_revision",
155
+ "observation_id",
156
+ "observed_at",
157
+ "observer_generation",
158
+ "observer_version",
159
+ "plan_path",
160
+ "plan_signature",
161
+ "receipt_attempt_id",
162
+ "receipt_protocol",
163
+ "receipt_record_id",
164
+ "receipt_status",
165
+ "shadow",
166
+ "source_events",
167
+ "source_kind",
168
+ "source_ref",
169
+ "state_path_identity",
170
+ ]);
171
+ if (
172
+ record.contract !== "assurance_kernel/v3_authority_observation/v2" ||
173
+ record.receipt_protocol !==
174
+ "assurance_kernel/authority_commit_receipt/v2" ||
175
+ (record.receipt_status !== "committed" &&
176
+ record.receipt_status !== "recovered_committed") ||
177
+ record.observer_generation !== AUTHORITY_OBSERVATION_GENERATION_V2 ||
178
+ record.observer_version !== AUTHORITY_OBSERVER_VERSION_V2 ||
179
+ (record.source_kind !== "state_mutation" &&
180
+ record.source_kind !== "project_migration")
181
+ )
182
+ throw new AutomaticObservationSecurityError(
183
+ "automatic observation v2 identity is invalid",
184
+ );
185
+ for (const key of [
186
+ "observation_id",
187
+ "receipt_record_id",
188
+ "receipt_attempt_id",
189
+ "source_ref",
190
+ "state_path_identity",
191
+ "committed_bytes_sha256",
192
+ "ledger_revision",
193
+ "committed_at",
194
+ "observed_at",
195
+ ])
196
+ nonEmptyString(record, key);
197
+ if (
198
+ (record.plan_path !== null && typeof record.plan_path !== "string") ||
199
+ (record.plan_signature !== null &&
200
+ typeof record.plan_signature !== "string")
201
+ )
202
+ throw new AutomaticObservationSecurityError(
203
+ "automatic observation v2 Plan identity is invalid",
204
+ );
205
+ const observation: V3AuthorityObservationV2 = {
206
+ ...(record as unknown as V3AuthorityObservationV2),
207
+ source_events: parseSourceEvents(record.source_events),
208
+ shadow: parseLegacyMapping(record.shadow),
209
+ divergence: parseDivergence(record.divergence),
210
+ };
211
+ const { observation_id: _observationId, ...core } = observation;
212
+ if (
213
+ observation.observation_id !==
214
+ digest(
215
+ "assurance-kernel-v3-observation/v2",
216
+ stableStringify(core),
217
+ )
218
+ )
219
+ throw new AutomaticObservationSecurityError(
220
+ "automatic observation v2 identity mismatch",
221
+ );
222
+ return observation;
223
+ }
224
+
225
+ function canonicalRoot(root: string): string {
226
+ const resolved = resolve(root);
227
+ const stat = lstatSync(resolved);
228
+ if (stat.isSymbolicLink() || !stat.isDirectory())
229
+ throw new AutomaticObservationSecurityError(
230
+ "automatic observation root must be a real directory",
231
+ );
232
+ return realpathSync(resolved);
233
+ }
234
+
235
+ function insideRoot(root: string, candidate: string): boolean {
236
+ return candidate === root || candidate.startsWith(`${root}${sep}`);
237
+ }
238
+
239
+ function ensureMemoryDirectory(root: string): { root: string; memory: string } {
240
+ // Storage-layout cutover: automatic observations are active runtime state
241
+ // and live under the ignored .imm/state/observations/ directory.
242
+ const canonical = canonicalRoot(root);
243
+ let cursor = canonical;
244
+ for (const segment of [".imm", "state", "observations"]) {
245
+ cursor = resolve(cursor, segment);
246
+ if (!existsSync(cursor)) mkdirSync(cursor, { mode: 0o700 });
247
+ const stat = lstatSync(cursor);
248
+ if (stat.isSymbolicLink() || !stat.isDirectory())
249
+ throw new AutomaticObservationSecurityError(
250
+ "automatic observation directory is not a real directory",
251
+ );
252
+ const real = realpathSync(cursor);
253
+ if (!insideRoot(canonical, real))
254
+ throw new AutomaticObservationSecurityError(
255
+ "automatic observation directory escapes the project",
256
+ );
257
+ cursor = real;
258
+ }
259
+ return { root: canonical, memory: cursor };
260
+ }
261
+
262
+ function journalPath(root: string): string {
263
+ return resolve(canonicalRoot(root), JOURNAL_RELATIVE_PATH);
264
+ }
265
+
266
+ export function automaticObservationJournalPath(root: string): string {
267
+ return journalPath(root);
268
+ }
269
+
270
+ function assertRegularPathOrMissing(path: string): void {
271
+ if (!existsSync(path)) return;
272
+ const stat = lstatSync(path);
273
+ if (stat.isSymbolicLink() || !stat.isFile())
274
+ throw new AutomaticObservationSecurityError(
275
+ "automatic observation path is not a regular file",
276
+ );
277
+ }
278
+
279
+ function processIsAlive(pid: number): boolean {
280
+ try {
281
+ process.kill(pid, 0);
282
+ return true;
283
+ } catch (error) {
284
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
285
+ }
286
+ }
287
+
288
+ function clearStaleLock(path: string): boolean {
289
+ if (!existsSync(path)) return true;
290
+ const before = lstatSync(path);
291
+ if (before.isSymbolicLink() || !before.isFile())
292
+ throw new AutomaticObservationSecurityError(
293
+ "automatic observation lock is not a regular file",
294
+ );
295
+ let stale = false;
296
+ try {
297
+ const record = JSON.parse(readFileSync(path, "utf8")) as {
298
+ pid?: unknown;
299
+ };
300
+ stale =
301
+ Number.isInteger(record.pid) &&
302
+ Number(record.pid) > 0 &&
303
+ !processIsAlive(Number(record.pid));
304
+ } catch {
305
+ stale = Date.now() - before.mtimeMs > 30_000;
306
+ }
307
+ if (!stale) return false;
308
+ const after = lstatSync(path);
309
+ if (
310
+ after.isSymbolicLink() ||
311
+ after.dev !== before.dev ||
312
+ after.ino !== before.ino
313
+ )
314
+ throw new AutomaticObservationSecurityError(
315
+ "automatic observation lock identity changed",
316
+ );
317
+ rmSync(path);
318
+ return true;
319
+ }
320
+
321
+ function withLock<T>(root: string, operation: () => T): T {
322
+ const { memory } = ensureMemoryDirectory(root);
323
+ const path = resolve(memory, LOCK_RELATIVE_PATH.split("/").at(-1)!);
324
+ let fd: number | null = null;
325
+ for (let attempt = 0; attempt < 2; attempt += 1) {
326
+ try {
327
+ fd = openSync(
328
+ path,
329
+ constants.O_WRONLY |
330
+ constants.O_CREAT |
331
+ constants.O_EXCL |
332
+ (constants.O_NOFOLLOW ?? 0),
333
+ 0o600,
334
+ );
335
+ break;
336
+ } catch (error) {
337
+ if (
338
+ attempt === 0 &&
339
+ (error as NodeJS.ErrnoException).code === "EEXIST" &&
340
+ clearStaleLock(path)
341
+ )
342
+ continue;
343
+ throw new AutomaticObservationConflictError(
344
+ `automatic observation lock is busy: ${error instanceof Error ? error.message : error}`,
345
+ );
346
+ }
347
+ }
348
+ if (fd === null)
349
+ throw new AutomaticObservationConflictError(
350
+ "automatic observation lock could not be acquired",
351
+ );
352
+ const identity = fstatSync(fd);
353
+ try {
354
+ writeFileSync(
355
+ fd,
356
+ `${JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() })}\n`,
357
+ "utf8",
358
+ );
359
+ fsyncSync(fd);
360
+ return operation();
361
+ } finally {
362
+ closeSync(fd);
363
+ if (existsSync(path)) {
364
+ const current = lstatSync(path);
365
+ if (
366
+ !current.isSymbolicLink() &&
367
+ current.dev === identity.dev &&
368
+ current.ino === identity.ino
369
+ )
370
+ rmSync(path);
371
+ }
372
+ }
373
+ }
374
+
375
+ export function readAutomaticObservationsV2(
376
+ root: string,
377
+ ): V3AuthorityObservationV2[] {
378
+ const path = journalPath(root);
379
+ assertRegularPathOrMissing(path);
380
+ if (!existsSync(path)) return [];
381
+ const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
382
+ try {
383
+ const before = fstatSync(fd);
384
+ if (before.size > MAX_JOURNAL_BYTES)
385
+ throw new AutomaticObservationSecurityError(
386
+ "automatic observation journal exceeds the read limit",
387
+ );
388
+ const content = readFileSync(fd, "utf8");
389
+ const after = fstatSync(fd);
390
+ if (before.dev !== after.dev || before.ino !== after.ino)
391
+ throw new AutomaticObservationSecurityError(
392
+ "automatic observation journal identity changed",
393
+ );
394
+ return content
395
+ .split("\n")
396
+ .filter((line) => line.trim())
397
+ .map((line, index) => {
398
+ try {
399
+ return parseAutomaticObservationV2(JSON.parse(line));
400
+ } catch (error) {
401
+ throw new AutomaticObservationSecurityError(
402
+ `automatic observation line ${index + 1} is invalid: ${error instanceof Error ? error.message : error}`,
403
+ );
404
+ }
405
+ });
406
+ } finally {
407
+ closeSync(fd);
408
+ }
409
+ }
410
+
411
+ export function appendAutomaticObservationV2(
412
+ root: string,
413
+ input: V3AuthorityObservationV2,
414
+ ): "appended" | "duplicate" {
415
+ const observation = parseAutomaticObservationV2(input);
416
+ return withLock(root, () => {
417
+ for (const existing of readAutomaticObservationsV2(root)) {
418
+ if (existing.receipt_record_id !== observation.receipt_record_id)
419
+ continue;
420
+ if (existing.observation_id === observation.observation_id)
421
+ return "duplicate";
422
+ throw new AutomaticObservationConflictError(
423
+ `automatic observation receipt identity conflict: ${observation.receipt_record_id}`,
424
+ );
425
+ }
426
+ ensureMemoryDirectory(root);
427
+ const path = resolve(root, JOURNAL_RELATIVE_PATH);
428
+ assertRegularPathOrMissing(path);
429
+ const fd = openSync(
430
+ path,
431
+ constants.O_WRONLY |
432
+ constants.O_CREAT |
433
+ constants.O_APPEND |
434
+ (constants.O_NOFOLLOW ?? 0),
435
+ 0o600,
436
+ );
437
+ try {
438
+ writeFileSync(fd, `${JSON.stringify(observation)}\n`, "utf8");
439
+ fsyncSync(fd);
440
+ } finally {
441
+ closeSync(fd);
442
+ }
443
+ const directoryFd = openSync(dirname(path), constants.O_RDONLY);
444
+ try {
445
+ fsyncSync(directoryFd);
446
+ } finally {
447
+ closeSync(directoryFd);
448
+ }
449
+ return "appended";
450
+ });
451
+ }