openplanr 1.13.2 → 1.14.0

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 (159) hide show
  1. package/README.md +46 -4
  2. package/dist/cli/commands/artifact.d.ts.map +1 -1
  3. package/dist/cli/commands/artifact.js +17 -6
  4. package/dist/cli/commands/artifact.js.map +1 -1
  5. package/dist/cli/commands/operate.d.ts +3 -0
  6. package/dist/cli/commands/operate.d.ts.map +1 -0
  7. package/dist/cli/commands/operate.js +568 -0
  8. package/dist/cli/commands/operate.js.map +1 -0
  9. package/dist/cli/index.js +42 -2
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/services/ai-service.d.ts +9 -2
  12. package/dist/services/ai-service.d.ts.map +1 -1
  13. package/dist/services/ai-service.js +14 -6
  14. package/dist/services/ai-service.js.map +1 -1
  15. package/dist/services/artifact-pipeline-service.d.ts +1 -0
  16. package/dist/services/artifact-pipeline-service.d.ts.map +1 -1
  17. package/dist/services/artifact-pipeline-service.js.map +1 -1
  18. package/dist/services/delta-apply-service.js +1 -1
  19. package/dist/services/delta-apply-service.js.map +1 -1
  20. package/dist/services/operate/advisors.d.ts +219 -0
  21. package/dist/services/operate/advisors.d.ts.map +1 -0
  22. package/dist/services/operate/advisors.js +745 -0
  23. package/dist/services/operate/advisors.js.map +1 -0
  24. package/dist/services/operate/artifact-route-generation.d.ts +111 -0
  25. package/dist/services/operate/artifact-route-generation.d.ts.map +1 -0
  26. package/dist/services/operate/artifact-route-generation.js +459 -0
  27. package/dist/services/operate/artifact-route-generation.js.map +1 -0
  28. package/dist/services/operate/artifacts.d.ts +26 -0
  29. package/dist/services/operate/artifacts.d.ts.map +1 -0
  30. package/dist/services/operate/artifacts.js +92 -0
  31. package/dist/services/operate/artifacts.js.map +1 -0
  32. package/dist/services/operate/canonical.d.ts +18 -0
  33. package/dist/services/operate/canonical.d.ts.map +1 -0
  34. package/dist/services/operate/canonical.js +157 -0
  35. package/dist/services/operate/canonical.js.map +1 -0
  36. package/dist/services/operate/config.d.ts +56 -0
  37. package/dist/services/operate/config.d.ts.map +1 -0
  38. package/dist/services/operate/config.js +644 -0
  39. package/dist/services/operate/config.js.map +1 -0
  40. package/dist/services/operate/consolidation.d.ts +18 -0
  41. package/dist/services/operate/consolidation.d.ts.map +1 -0
  42. package/dist/services/operate/consolidation.js +539 -0
  43. package/dist/services/operate/consolidation.js.map +1 -0
  44. package/dist/services/operate/doctor.d.ts +12 -0
  45. package/dist/services/operate/doctor.d.ts.map +1 -0
  46. package/dist/services/operate/doctor.js +307 -0
  47. package/dist/services/operate/doctor.js.map +1 -0
  48. package/dist/services/operate/engine.d.ts +53 -0
  49. package/dist/services/operate/engine.d.ts.map +1 -0
  50. package/dist/services/operate/engine.js +1097 -0
  51. package/dist/services/operate/engine.js.map +1 -0
  52. package/dist/services/operate/event-store.d.ts +57 -0
  53. package/dist/services/operate/event-store.d.ts.map +1 -0
  54. package/dist/services/operate/event-store.js +232 -0
  55. package/dist/services/operate/event-store.js.map +1 -0
  56. package/dist/services/operate/evidence-cache.d.ts +17 -0
  57. package/dist/services/operate/evidence-cache.d.ts.map +1 -0
  58. package/dist/services/operate/evidence-cache.js +96 -0
  59. package/dist/services/operate/evidence-cache.js.map +1 -0
  60. package/dist/services/operate/evidence-import.d.ts +40 -0
  61. package/dist/services/operate/evidence-import.d.ts.map +1 -0
  62. package/dist/services/operate/evidence-import.js +340 -0
  63. package/dist/services/operate/evidence-import.js.map +1 -0
  64. package/dist/services/operate/evidence-readiness.d.ts +8 -0
  65. package/dist/services/operate/evidence-readiness.d.ts.map +1 -0
  66. package/dist/services/operate/evidence-readiness.js +78 -0
  67. package/dist/services/operate/evidence-readiness.js.map +1 -0
  68. package/dist/services/operate/evidence.d.ts +71 -0
  69. package/dist/services/operate/evidence.d.ts.map +1 -0
  70. package/dist/services/operate/evidence.js +1022 -0
  71. package/dist/services/operate/evidence.js.map +1 -0
  72. package/dist/services/operate/ids.d.ts +7 -0
  73. package/dist/services/operate/ids.d.ts.map +1 -0
  74. package/dist/services/operate/ids.js +23 -0
  75. package/dist/services/operate/ids.js.map +1 -0
  76. package/dist/services/operate/index.d.ts +14 -0
  77. package/dist/services/operate/index.d.ts.map +1 -0
  78. package/dist/services/operate/index.js +881 -0
  79. package/dist/services/operate/index.js.map +1 -0
  80. package/dist/services/operate/journal.d.ts +34 -0
  81. package/dist/services/operate/journal.d.ts.map +1 -0
  82. package/dist/services/operate/journal.js +229 -0
  83. package/dist/services/operate/journal.js.map +1 -0
  84. package/dist/services/operate/legacy-import-service.d.ts +58 -0
  85. package/dist/services/operate/legacy-import-service.d.ts.map +1 -0
  86. package/dist/services/operate/legacy-import-service.js +922 -0
  87. package/dist/services/operate/legacy-import-service.js.map +1 -0
  88. package/dist/services/operate/lifecycle.d.ts +88 -0
  89. package/dist/services/operate/lifecycle.d.ts.map +1 -0
  90. package/dist/services/operate/lifecycle.js +556 -0
  91. package/dist/services/operate/lifecycle.js.map +1 -0
  92. package/dist/services/operate/lock-service.d.ts +37 -0
  93. package/dist/services/operate/lock-service.d.ts.map +1 -0
  94. package/dist/services/operate/lock-service.js +208 -0
  95. package/dist/services/operate/lock-service.js.map +1 -0
  96. package/dist/services/operate/maintenance.d.ts +38 -0
  97. package/dist/services/operate/maintenance.d.ts.map +1 -0
  98. package/dist/services/operate/maintenance.js +699 -0
  99. package/dist/services/operate/maintenance.js.map +1 -0
  100. package/dist/services/operate/outcomes.d.ts +48 -0
  101. package/dist/services/operate/outcomes.d.ts.map +1 -0
  102. package/dist/services/operate/outcomes.js +312 -0
  103. package/dist/services/operate/outcomes.js.map +1 -0
  104. package/dist/services/operate/pipeline-handoff.d.ts +138 -0
  105. package/dist/services/operate/pipeline-handoff.d.ts.map +1 -0
  106. package/dist/services/operate/pipeline-handoff.js +175 -0
  107. package/dist/services/operate/pipeline-handoff.js.map +1 -0
  108. package/dist/services/operate/projection-persistence.d.ts +55 -0
  109. package/dist/services/operate/projection-persistence.d.ts.map +1 -0
  110. package/dist/services/operate/projection-persistence.js +393 -0
  111. package/dist/services/operate/projection-persistence.js.map +1 -0
  112. package/dist/services/operate/projection.d.ts +4 -0
  113. package/dist/services/operate/projection.d.ts.map +1 -0
  114. package/dist/services/operate/projection.js +128 -0
  115. package/dist/services/operate/projection.js.map +1 -0
  116. package/dist/services/operate/protocol.d.ts +60 -0
  117. package/dist/services/operate/protocol.d.ts.map +1 -0
  118. package/dist/services/operate/protocol.js +62 -0
  119. package/dist/services/operate/protocol.js.map +1 -0
  120. package/dist/services/operate/read-only-providers.d.ts +38 -0
  121. package/dist/services/operate/read-only-providers.d.ts.map +1 -0
  122. package/dist/services/operate/read-only-providers.js +286 -0
  123. package/dist/services/operate/read-only-providers.js.map +1 -0
  124. package/dist/services/operate/redaction.d.ts +47 -0
  125. package/dist/services/operate/redaction.d.ts.map +1 -0
  126. package/dist/services/operate/redaction.js +279 -0
  127. package/dist/services/operate/redaction.js.map +1 -0
  128. package/dist/services/operate/reducer.d.ts +10 -0
  129. package/dist/services/operate/reducer.d.ts.map +1 -0
  130. package/dist/services/operate/reducer.js +11 -0
  131. package/dist/services/operate/reducer.js.map +1 -0
  132. package/dist/services/operate/routes.d.ts +49 -0
  133. package/dist/services/operate/routes.d.ts.map +1 -0
  134. package/dist/services/operate/routes.js +996 -0
  135. package/dist/services/operate/routes.js.map +1 -0
  136. package/dist/services/operate/shipment-observer.d.ts +14 -0
  137. package/dist/services/operate/shipment-observer.d.ts.map +1 -0
  138. package/dist/services/operate/shipment-observer.js +321 -0
  139. package/dist/services/operate/shipment-observer.js.map +1 -0
  140. package/dist/services/operate/stalled-item-service.d.ts +18 -0
  141. package/dist/services/operate/stalled-item-service.d.ts.map +1 -0
  142. package/dist/services/operate/stalled-item-service.js +121 -0
  143. package/dist/services/operate/stalled-item-service.js.map +1 -0
  144. package/dist/services/operate/subprocess-env.d.ts +4 -0
  145. package/dist/services/operate/subprocess-env.d.ts.map +1 -0
  146. package/dist/services/operate/subprocess-env.js +45 -0
  147. package/dist/services/operate/subprocess-env.js.map +1 -0
  148. package/dist/services/operate/types.d.ts +729 -0
  149. package/dist/services/operate/types.d.ts.map +1 -0
  150. package/dist/services/operate/types.js +13 -0
  151. package/dist/services/operate/types.js.map +1 -0
  152. package/dist/services/operate/workspace.d.ts +59 -0
  153. package/dist/services/operate/workspace.d.ts.map +1 -0
  154. package/dist/services/operate/workspace.js +316 -0
  155. package/dist/services/operate/workspace.js.map +1 -0
  156. package/dist/services/runtime-manager-service.d.ts.map +1 -1
  157. package/dist/services/runtime-manager-service.js +71 -2
  158. package/dist/services/runtime-manager-service.js.map +1 -1
  159. package/package.json +10 -4
@@ -0,0 +1,644 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { spliceManagedBlock } from '../../utils/splice-managed-block.js';
5
+ import { canonicalDigest, canonicalize, sha256Digest } from './canonical.js';
6
+ import { OperatingEventStore } from './event-store.js';
7
+ import { resolveEvidenceImportPath } from './evidence-import.js';
8
+ import { applyJournalTransaction, prepareJournalTransaction, rollbackJournalTransaction, } from './journal.js';
9
+ import { withOperatingLock } from './lock-service.js';
10
+ import { prepareOperatingProjectionPersistence, renderOperatingProjectionFiles, } from './projection-persistence.js';
11
+ import { assertOperatingArtifact, loadOperatingProtocol } from './protocol.js';
12
+ import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
13
+ import { buildWorkspaceManifest, ensureOperatingDirectories, projectMachineKey, resolveOperatingPaths, resolveOperatingProject, } from './workspace.js';
14
+ const ALL_ROLES = [
15
+ 'strategy-finance',
16
+ 'technology-risk',
17
+ 'product-activation',
18
+ 'growth-market',
19
+ 'operations-customer',
20
+ 'chair',
21
+ ];
22
+ function profile(id, title, description, overrides = {}) {
23
+ return {
24
+ id,
25
+ title,
26
+ description,
27
+ enabledRoles: [...ALL_ROLES],
28
+ enabledProviders: ['repository', 'planr', 'git'],
29
+ caps: {
30
+ surfacedFindings: 12,
31
+ newSpecs: 3,
32
+ openDecisions: 5,
33
+ agentArtifacts: 4,
34
+ },
35
+ budgets: {
36
+ maxFiles: 1_000,
37
+ maxItems: 2_000,
38
+ maxBytes: 10 * 1024 * 1024,
39
+ maxDurationMs: 60_000,
40
+ },
41
+ ...overrides,
42
+ };
43
+ }
44
+ export const OPERATING_PROFILES = [
45
+ profile('saas', 'SaaS', 'Balanced product, growth, risk, and operating review.'),
46
+ profile('product', 'Product', 'Activation and customer-outcome focused review.', {
47
+ enabledProviders: ['repository', 'planr', 'git', 'github', 'linear'],
48
+ }),
49
+ profile('engineering', 'Engineering', 'Delivery, reliability, and risk focused review.', {
50
+ enabledRoles: ['technology-risk', 'product-activation', 'operations-customer', 'chair'],
51
+ caps: {
52
+ surfacedFindings: 10,
53
+ newSpecs: 3,
54
+ openDecisions: 3,
55
+ agentArtifacts: 2,
56
+ },
57
+ }),
58
+ profile('custom', 'Custom', 'Explicit user-supplied operating configuration.'),
59
+ ];
60
+ export function listOperatingProfiles() {
61
+ return OPERATING_PROFILES.map((candidate) => structuredClone(candidate));
62
+ }
63
+ export function getOperatingProfile(id) {
64
+ const candidate = listOperatingProfiles().find((entry) => entry.id === id);
65
+ if (!candidate) {
66
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', `Unknown operating profile: ${id}.`);
67
+ }
68
+ return candidate;
69
+ }
70
+ const CUSTOM_PROFILE_FIELDS = new Set([
71
+ 'id',
72
+ 'title',
73
+ 'description',
74
+ 'enabledRoles',
75
+ 'enabledProviders',
76
+ 'caps',
77
+ 'budgets',
78
+ ]);
79
+ const CUSTOM_CAP_FIELDS = new Set([
80
+ 'surfacedFindings',
81
+ 'newSpecs',
82
+ 'openDecisions',
83
+ 'agentArtifacts',
84
+ ]);
85
+ const CUSTOM_BUDGET_FIELDS = new Set(['maxFiles', 'maxItems', 'maxBytes', 'maxDurationMs']);
86
+ const CUSTOM_PROVIDER_IDS = new Set([
87
+ 'repository',
88
+ 'planr',
89
+ 'git',
90
+ 'github',
91
+ 'linear',
92
+ 'file-import',
93
+ ]);
94
+ function plainRecord(value, label) {
95
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
96
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', `${label} must be a JSON object.`);
97
+ }
98
+ return value;
99
+ }
100
+ function boundedStringArray(value, label) {
101
+ if (!Array.isArray(value) ||
102
+ value.length > 64 ||
103
+ value.some((entry) => typeof entry !== 'string' || entry.length > 256)) {
104
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', `${label} must be a bounded array of strings.`);
105
+ }
106
+ return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
107
+ }
108
+ function numericOverrides(value, label, allowed) {
109
+ const record = plainRecord(value, label);
110
+ const unknown = Object.keys(record).filter((key) => !allowed.has(key));
111
+ if (unknown.length > 0 || Object.values(record).some((entry) => !Number.isInteger(entry))) {
112
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', `${label} contains unsupported fields or non-integer values.`);
113
+ }
114
+ return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, Number(entry)]));
115
+ }
116
+ /**
117
+ * Strictly allowlists custom-profile data before it can be echoed, persisted,
118
+ * or merged. This is shared by init and profiles validate so unknown fields
119
+ * (including accidental secrets) never reach command results.
120
+ */
121
+ export function normalizeCustomOperatingProfile(value) {
122
+ const record = plainRecord(value, 'Custom profile');
123
+ const unknown = Object.keys(record).filter((key) => !CUSTOM_PROFILE_FIELDS.has(key));
124
+ if (unknown.length > 0 || (record.id !== undefined && record.id !== 'custom')) {
125
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Custom profile contains unsupported fields or identity.');
126
+ }
127
+ const normalized = {};
128
+ if (record.id !== undefined)
129
+ normalized.id = 'custom';
130
+ for (const key of ['title', 'description']) {
131
+ const entry = record[key];
132
+ if (entry === undefined)
133
+ continue;
134
+ if (typeof entry !== 'string' || entry.trim().length === 0 || entry.length > 1_024) {
135
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', `Custom profile ${key} must be a non-empty bounded string.`);
136
+ }
137
+ normalized[key] = entry.trim();
138
+ }
139
+ if (record.enabledRoles !== undefined) {
140
+ const roles = boundedStringArray(record.enabledRoles, 'Custom profile enabledRoles');
141
+ if (roles.length === 0 || roles.some((role) => !ALL_ROLES.includes(role))) {
142
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Custom profile enabledRoles contains an unknown role or is empty.');
143
+ }
144
+ normalized.enabledRoles = roles;
145
+ }
146
+ if (record.enabledProviders !== undefined) {
147
+ const providers = boundedStringArray(record.enabledProviders, 'Custom profile enabledProviders');
148
+ if (providers.length === 0 ||
149
+ providers.some((provider) => !CUSTOM_PROVIDER_IDS.has(provider))) {
150
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Custom profile enabledProviders contains an unknown provider or is empty.');
151
+ }
152
+ normalized.enabledProviders = providers;
153
+ }
154
+ if (record.caps !== undefined) {
155
+ const caps = numericOverrides(record.caps, 'Custom profile caps', CUSTOM_CAP_FIELDS);
156
+ const maxima = {
157
+ surfacedFindings: 50,
158
+ newSpecs: 12,
159
+ openDecisions: 20,
160
+ agentArtifacts: 20,
161
+ };
162
+ if (Object.entries(caps).some(([key, entry]) => entry < 1 || entry > maxima[key])) {
163
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Custom profile caps exceed the supported safety bounds.');
164
+ }
165
+ normalized.caps = caps;
166
+ }
167
+ if (record.budgets !== undefined) {
168
+ const budgets = numericOverrides(record.budgets, 'Custom profile budgets', CUSTOM_BUDGET_FIELDS);
169
+ const withinBounds = (budgets.maxFiles === undefined || (budgets.maxFiles >= 1 && budgets.maxFiles <= 10_000)) &&
170
+ (budgets.maxItems === undefined || (budgets.maxItems >= 1 && budgets.maxItems <= 10_000)) &&
171
+ (budgets.maxBytes === undefined ||
172
+ (budgets.maxBytes >= 1_024 && budgets.maxBytes <= 50 * 1024 * 1024)) &&
173
+ (budgets.maxDurationMs === undefined ||
174
+ (budgets.maxDurationMs >= 100 && budgets.maxDurationMs <= 10 * 60_000));
175
+ if (!withinBounds) {
176
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Custom profile budgets exceed the supported safety bounds.');
177
+ }
178
+ normalized.budgets = budgets;
179
+ }
180
+ return normalized;
181
+ }
182
+ export function normalizeCharter(input = {}) {
183
+ const list = (values) => [
184
+ ...new Set((values ?? []).map((value) => value.trim()).filter(Boolean)),
185
+ ];
186
+ return {
187
+ purpose: input.purpose?.trim() ?? '',
188
+ stage: input.stage?.trim() ?? '',
189
+ businessModel: input.businessModel?.trim() ?? '',
190
+ idealCustomer: input.idealCustomer?.trim() ?? '',
191
+ goals: list(input.goals),
192
+ constraints: list(input.constraints),
193
+ successMetrics: list(input.successMetrics),
194
+ guardrails: list(input.guardrails),
195
+ knownUnknowns: list(input.knownUnknowns),
196
+ };
197
+ }
198
+ function markdownList(values, fallback) {
199
+ return values.length > 0 ? values.map((value) => `- ${value}`).join('\n') : `- ${fallback}`;
200
+ }
201
+ export function renderOperatingCharter(config, input = {}) {
202
+ const charter = normalizeCharter(input);
203
+ return [
204
+ '# Operating charter',
205
+ '',
206
+ `Decision owner: ${config.decisionOwner}`,
207
+ `Planning engine: ${config.planningEngine}`,
208
+ `Profile: ${config.profile}`,
209
+ '',
210
+ '## Product context',
211
+ '',
212
+ `- Purpose: ${charter.purpose || '[unknown — answer before dependent routes]'}`,
213
+ `- Stage: ${charter.stage || '[unknown]'}`,
214
+ `- Business model: ${charter.businessModel || '[unknown]'}`,
215
+ `- Ideal customer: ${charter.idealCustomer || '[unknown]'}`,
216
+ '',
217
+ '## Current goals',
218
+ '',
219
+ markdownList(charter.goals, '[unknown]'),
220
+ '',
221
+ '## Constraints',
222
+ '',
223
+ markdownList(charter.constraints, '[none recorded]'),
224
+ '',
225
+ '## Success metrics',
226
+ '',
227
+ markdownList(charter.successMetrics, '[unknown — numeric claims require a source and observation window]'),
228
+ '',
229
+ '## Guardrails',
230
+ '',
231
+ markdownList(charter.guardrails, 'No external or irreversible action without explicit human authority.'),
232
+ '',
233
+ '## Known unknowns',
234
+ '',
235
+ markdownList(charter.knownUnknowns, 'Complete the missing product context above.'),
236
+ ].join('\n');
237
+ }
238
+ const INITIALIZATION_RECOVERY_ID = 'RCV-operating-board-initialized';
239
+ const INITIALIZATION_CORRELATION_ID = 'operate-initialization-v1';
240
+ function operatingRecordPath(digest) {
241
+ const hex = digest.slice('sha256:'.length);
242
+ return `.planr/operate/records/sha256/${hex.slice(0, 2)}/${hex.slice(2)}.json`;
243
+ }
244
+ async function buildInitialOperatingState(input) {
245
+ const protocol = await loadOperatingProtocol();
246
+ const genesis = { sequence: 0, hash: null };
247
+ const recovery = {
248
+ kind: 'operating-recovery-record',
249
+ schemaVersion: OPERATE_SCHEMA_VERSION,
250
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
251
+ id: INITIALIZATION_RECOVERY_ID,
252
+ transactionId: null,
253
+ action: 'rebuild-projection',
254
+ reason: 'Operating Board initialized with a verified canonical event and checkpoint.',
255
+ previewDigest: canonicalDigest({
256
+ kind: 'operating-initialization',
257
+ config: input.config,
258
+ workspaceDigest: input.workspace.workspaceDigest,
259
+ }),
260
+ fromHead: genesis,
261
+ // The recovery record is created before the event that references it. The
262
+ // event and checkpoint carry the authoritative resulting hash.
263
+ toHead: { sequence: 1, hash: null },
264
+ outcome: 'recovered',
265
+ confirmedBy: 'operate-cli',
266
+ createdAt: input.createdAt,
267
+ };
268
+ await assertOperatingArtifact('operating-recovery-record', recovery);
269
+ const contentDigest = canonicalDigest(recovery);
270
+ const digest = canonicalDigest({
271
+ recordType: 'recovery',
272
+ createdAt: input.createdAt,
273
+ correlationId: INITIALIZATION_CORRELATION_ID,
274
+ contentDigest,
275
+ });
276
+ const record = {
277
+ kind: 'operating-record',
278
+ schemaVersion: OPERATE_SCHEMA_VERSION,
279
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
280
+ digest,
281
+ recordType: 'recovery',
282
+ createdAt: input.createdAt,
283
+ correlationId: INITIALIZATION_CORRELATION_ID,
284
+ contentDigest,
285
+ content: recovery,
286
+ };
287
+ await assertOperatingArtifact('operating-record', record);
288
+ const event = protocol.createOperatingEvent({
289
+ eventId: INITIALIZATION_CORRELATION_ID,
290
+ timestamp: input.createdAt,
291
+ cycleId: 'CYCLE-000',
292
+ type: 'projection.rebuilt',
293
+ entityId: INITIALIZATION_RECOVERY_ID,
294
+ actor: { kind: 'engine', id: 'openplanr' },
295
+ causationId: null,
296
+ correlationId: INITIALIZATION_CORRELATION_ID,
297
+ evidenceRefs: [],
298
+ payload: { recordDigest: record.digest },
299
+ }, { previousEvent: null, sequence: 1 });
300
+ const state = protocol.reduceOperatingEvents([event]);
301
+ const checkpoint = protocol.createOperatingCheckpoint(state, {
302
+ createdAt: input.createdAt,
303
+ recordDigests: [record.digest],
304
+ });
305
+ await Promise.all([
306
+ assertOperatingArtifact('operating-event', event),
307
+ assertOperatingArtifact('operating-state', state),
308
+ assertOperatingArtifact('operating-checkpoint', checkpoint),
309
+ ]);
310
+ const projection = await prepareOperatingProjectionPersistence({
311
+ projectRoot: input.projectRoot,
312
+ state,
313
+ });
314
+ const projectionByPath = new Map(projection.files.map((file) => [file.relativePath, file]));
315
+ const writes = [
316
+ {
317
+ relativePath: operatingRecordPath(record.digest),
318
+ content: canonicalize(record),
319
+ operation: 'create',
320
+ },
321
+ {
322
+ relativePath: '.planr/operate/checkpoints/current.json',
323
+ content: `${canonicalize(checkpoint)}\n`,
324
+ },
325
+ ...renderOperatingProjectionFiles(state).map((file) => ({
326
+ relativePath: file.relativePath,
327
+ content: projectionByPath.get(file.relativePath)?.content ?? file.content,
328
+ })),
329
+ // Keep the canonical event last: journal head revalidation remains at
330
+ // genesis for every preceding write, then changes exactly once.
331
+ {
332
+ relativePath: '.planr/operate/events.jsonl',
333
+ content: `${canonicalize(event)}\n`,
334
+ operation: 'create',
335
+ },
336
+ ];
337
+ return { event, record, checkpoint, state, writes };
338
+ }
339
+ export async function prepareOperatingInitialization(input) {
340
+ if (!input.decisionOwner.trim()) {
341
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Operating initialization requires a decision owner.');
342
+ }
343
+ const base = getOperatingProfile(input.profile);
344
+ const customProfile = input.profile === 'custom'
345
+ ? normalizeCustomOperatingProfile(input.customProfile ?? {})
346
+ : undefined;
347
+ const selected = input.profile === 'custom'
348
+ ? {
349
+ ...base,
350
+ ...customProfile,
351
+ caps: { ...base.caps, ...customProfile?.caps },
352
+ budgets: { ...base.budgets, ...customProfile?.budgets },
353
+ }
354
+ : base;
355
+ const requestedEvidenceFiles = [
356
+ ...new Set((input.evidenceFiles ?? []).map((value) => value.trim()).filter(Boolean)),
357
+ ];
358
+ const requestedProviders = [
359
+ ...new Set([
360
+ ...(input.enabledProviders ?? selected.enabledProviders),
361
+ ...(requestedEvidenceFiles.length > 0 ? ['file-import'] : []),
362
+ ]),
363
+ ].sort();
364
+ if (requestedProviders.includes('file-import') && requestedEvidenceFiles.length === 0) {
365
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'The file-import source requires at least one --evidence-file JSON or CSV path.');
366
+ }
367
+ const config = {
368
+ kind: 'operating-config',
369
+ schemaVersion: OPERATE_SCHEMA_VERSION,
370
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
371
+ profile: input.profile,
372
+ decisionOwner: input.decisionOwner.trim(),
373
+ cadence: input.cadence ?? 'manual',
374
+ planningEngine: input.planningEngine,
375
+ enabledRoles: [...selected.enabledRoles],
376
+ enabledProviders: requestedProviders,
377
+ caps: { ...selected.caps },
378
+ budgets: { ...selected.budgets },
379
+ };
380
+ const caps = config.caps;
381
+ const capBounds = [
382
+ ['surfacedFindings', 50],
383
+ ['newSpecs', 12],
384
+ ['openDecisions', 20],
385
+ ['agentArtifacts', 20],
386
+ ];
387
+ for (const [name, maximum] of capBounds) {
388
+ if (!Number.isInteger(caps[name]) || caps[name] < 1 || caps[name] > maximum) {
389
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', `${name} must be an integer from 1 to ${maximum}.`);
390
+ }
391
+ }
392
+ if (!Number.isInteger(config.budgets.maxFiles) ||
393
+ config.budgets.maxFiles < 1 ||
394
+ config.budgets.maxFiles > 10_000 ||
395
+ !Number.isInteger(config.budgets.maxItems) ||
396
+ config.budgets.maxItems < 1 ||
397
+ config.budgets.maxItems > 10_000 ||
398
+ !Number.isInteger(config.budgets.maxBytes) ||
399
+ config.budgets.maxBytes < 1_024 ||
400
+ config.budgets.maxBytes > 50 * 1024 * 1024 ||
401
+ !Number.isInteger(config.budgets.maxDurationMs) ||
402
+ config.budgets.maxDurationMs < 100 ||
403
+ config.budgets.maxDurationMs > 10 * 60_000) {
404
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Custom evidence budgets exceed the supported safety bounds.');
405
+ }
406
+ await assertOperatingArtifact('operating-config', config);
407
+ const timezone = input.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
408
+ try {
409
+ new Intl.DateTimeFormat('en', { timeZone: timezone }).format(new Date(0));
410
+ }
411
+ catch {
412
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', `Invalid IANA timezone: ${timezone}.`);
413
+ }
414
+ const evidenceTtlMs = input.evidenceTtlMs ?? 7 * 24 * 60 * 60 * 1000;
415
+ if (!Number.isInteger(evidenceTtlMs) ||
416
+ evidenceTtlMs < 60 * 60 * 1_000 ||
417
+ evidenceTtlMs > 30 * 24 * 60 * 60 * 1_000) {
418
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Evidence TTL must be between one hour and 30 days.');
419
+ }
420
+ const componentRoots = [...new Set(input.componentRoots ?? [])];
421
+ let workspace = await buildWorkspaceManifest(input.projectRoot, componentRoots, {
422
+ capturedAt: input.now,
423
+ localRoot: input.localRoot,
424
+ persistRoots: false,
425
+ });
426
+ const controlRoot = await resolveOperatingProject(input.projectRoot);
427
+ const resolvedImportPaths = [];
428
+ for (const configuredPath of requestedEvidenceFiles) {
429
+ const resolved = await resolveEvidenceImportPath(input.projectRoot, configuredPath, [
430
+ { componentId: workspace.controlRepository.componentId, root: controlRoot },
431
+ ...componentRoots.map((root, index) => ({
432
+ componentId: workspace.components[index]?.componentId ?? `component-${index + 1}`,
433
+ root,
434
+ })),
435
+ ]);
436
+ resolvedImportPaths.push(resolved.absolutePath);
437
+ }
438
+ const preferences = {
439
+ runtime: input.runtime ?? 'auto',
440
+ timezone,
441
+ sensitivityCeiling: input.sensitivityCeiling ?? 'internal',
442
+ evidenceTtlMs,
443
+ enabledSources: [...config.enabledProviders],
444
+ ...(resolvedImportPaths.length > 0
445
+ ? { importPaths: [...new Set(resolvedImportPaths)].sort() }
446
+ : {}),
447
+ };
448
+ const paths = resolveOperatingPaths(input.projectRoot, { localRoot: input.localRoot });
449
+ const existingWorkspace = await readFile(paths.workspace, 'utf8')
450
+ .then(async (raw) => {
451
+ const parsed = JSON.parse(raw);
452
+ await assertOperatingArtifact('operating-workspace-manifest', parsed);
453
+ return parsed;
454
+ })
455
+ .catch(() => null);
456
+ if (existingWorkspace &&
457
+ existingWorkspace.workspaceDigest === workspace.workspaceDigest &&
458
+ canonicalDigest(existingWorkspace.controlRepository) ===
459
+ canonicalDigest(workspace.controlRepository) &&
460
+ canonicalDigest(existingWorkspace.components) === canonicalDigest(workspace.components)) {
461
+ // Repeated init must be byte-idempotent. Preserve the original capture
462
+ // time when the verified repository descriptors have not changed.
463
+ workspace = existingWorkspace;
464
+ }
465
+ const expectedPreferences = `${canonicalize(preferences)}\n`;
466
+ const currentPreferences = await readFile(path.join(paths.localRoot, 'preferences.json'), 'utf8').catch(() => null);
467
+ const preferencesChanged = currentPreferences !== expectedPreferences;
468
+ const existingCharter = await readFile(paths.charter, 'utf8').catch(() => '');
469
+ const charter = spliceManagedBlock(existingCharter, 'operate-charter', renderOperatingCharter(config, input.charter));
470
+ const writes = [
471
+ { relativePath: '.planr/operate/config.json', content: `${canonicalize(config)}\n` },
472
+ { relativePath: '.planr/operate/charter.md', content: charter },
473
+ { relativePath: '.planr/operate/workspace.json', content: `${canonicalize(workspace)}\n` },
474
+ ];
475
+ const existingEvents = await readFile(paths.events, 'utf8').catch((error) => {
476
+ if (error.code === 'ENOENT')
477
+ return '';
478
+ throw error;
479
+ });
480
+ let resultingEventHead = { sequence: 0, hash: null };
481
+ if (existingEvents.trim()) {
482
+ const replay = await new OperatingEventStore(input.projectRoot, {
483
+ localRoot: input.localRoot,
484
+ }).replay();
485
+ resultingEventHead = replay.eventHead;
486
+ }
487
+ else {
488
+ const initial = await buildInitialOperatingState({
489
+ projectRoot: input.projectRoot,
490
+ createdAt: workspace.capturedAt,
491
+ config,
492
+ workspace,
493
+ });
494
+ resultingEventHead = {
495
+ sequence: initial.event.sequence,
496
+ hash: initial.event.eventHash,
497
+ };
498
+ writes.push(...initial.writes);
499
+ }
500
+ const changedPaths = [];
501
+ for (const write of writes) {
502
+ const current = await readFile(path.join(input.projectRoot, write.relativePath)).catch(() => null);
503
+ const next = Buffer.from(write.content);
504
+ if (!current || sha256Digest(current) !== sha256Digest(next)) {
505
+ changedPaths.push(write.relativePath);
506
+ }
507
+ }
508
+ const previewDigest = canonicalDigest({
509
+ config,
510
+ preferences,
511
+ charterDigest: sha256Digest(charter),
512
+ workspaceDigest: workspace.workspaceDigest,
513
+ changedPaths,
514
+ preferencesChanged,
515
+ });
516
+ return {
517
+ config,
518
+ preferences,
519
+ charter,
520
+ workspace,
521
+ previewDigest,
522
+ changedPaths,
523
+ preferencesChanged,
524
+ writes,
525
+ componentRoots,
526
+ resultingEventHead,
527
+ };
528
+ }
529
+ export async function applyOperatingInitialization(input) {
530
+ if (input.confirmationDigest !== input.preview.previewDigest) {
531
+ throw new OperateError('E_OPERATE_ROUTE_CONFIRMATION_REQUIRED', 'Initialization requires confirmation of the exact preview digest.');
532
+ }
533
+ const currentWorkspace = await buildWorkspaceManifest(input.projectRoot, input.preview.componentRoots, {
534
+ capturedAt: input.preview.workspace.capturedAt,
535
+ localRoot: input.localRoot,
536
+ persistRoots: false,
537
+ });
538
+ if (currentWorkspace.workspaceDigest !== input.preview.workspace.workspaceDigest) {
539
+ throw new OperateError('E_OPERATE_ROUTE_DRIFT', 'Workspace revisions changed after initialization preview.');
540
+ }
541
+ const paths = await ensureOperatingDirectories(input.projectRoot, {
542
+ localRoot: input.localRoot,
543
+ });
544
+ const store = new OperatingEventStore(input.projectRoot, { localRoot: input.localRoot });
545
+ const initial = await store.replay();
546
+ await withOperatingLock(input.projectRoot, {
547
+ projectKey: operatingProjectKey(input.projectRoot),
548
+ expectedEventHead: initial.eventHead,
549
+ currentEventHead: initial.eventHead,
550
+ localRoot: input.localRoot,
551
+ }, async (lock) => {
552
+ const preferencePath = path.join(paths.localRoot, 'preferences.json');
553
+ const beforeRoots = await readFile(paths.roots).catch(() => null);
554
+ const beforePreferences = await readFile(preferencePath).catch(() => null);
555
+ const restorePrivate = async () => {
556
+ for (const [target, before] of [
557
+ [paths.roots, beforeRoots],
558
+ [preferencePath, beforePreferences],
559
+ ]) {
560
+ if (before)
561
+ await writeFile(target, before, { mode: 0o600 });
562
+ else
563
+ await unlink(target).catch(() => undefined);
564
+ }
565
+ };
566
+ const journal = input.preview.changedPaths.length > 0
567
+ ? await prepareJournalTransaction(input.projectRoot, {
568
+ writes: input.preview.writes.filter((write) => input.preview.changedPaths.includes(write.relativePath)),
569
+ eventHead: initial.eventHead,
570
+ previewDigest: input.preview.previewDigest,
571
+ localRoot: input.localRoot,
572
+ })
573
+ : null;
574
+ let advancedHead = false;
575
+ try {
576
+ if (journal)
577
+ await input.faultInjector?.('journal-prepared');
578
+ if (journal) {
579
+ await applyJournalTransaction(input.projectRoot, journal, {
580
+ currentEventHead: initial.eventHead,
581
+ revalidateEventHead: async () => (await store.replay()).eventHead,
582
+ });
583
+ await input.faultInjector?.('project-promoted');
584
+ if (input.preview.resultingEventHead.sequence !== initial.eventHead.sequence ||
585
+ input.preview.resultingEventHead.hash !== initial.eventHead.hash) {
586
+ const actual = (await store.replay()).eventHead;
587
+ if (actual.sequence !== input.preview.resultingEventHead.sequence ||
588
+ actual.hash !== input.preview.resultingEventHead.hash) {
589
+ throw new OperateError('E_OPERATE_HEAD_DIVERGED', 'Initialization did not produce the confirmed canonical event head.');
590
+ }
591
+ await lock.advanceEventHead(initial.eventHead, actual);
592
+ advancedHead = true;
593
+ }
594
+ }
595
+ await input.faultInjector?.('workspace-roots');
596
+ await buildWorkspaceManifest(input.projectRoot, input.preview.componentRoots, {
597
+ capturedAt: input.preview.workspace.capturedAt,
598
+ localRoot: input.localRoot,
599
+ persistRoots: true,
600
+ });
601
+ await input.faultInjector?.('preferences');
602
+ await mkdir(paths.localRoot, { recursive: true, mode: 0o700 });
603
+ const temporary = `${preferencePath}.${process.pid}.${randomUUID()}.tmp`;
604
+ await writeFile(temporary, `${canonicalize(input.preview.preferences)}\n`, { mode: 0o600 });
605
+ await rename(temporary, preferencePath);
606
+ await input.faultInjector?.('committed');
607
+ }
608
+ catch (error) {
609
+ await restorePrivate().catch(() => undefined);
610
+ if (journal) {
611
+ await rollbackJournalTransaction(input.projectRoot, journal).catch(() => undefined);
612
+ }
613
+ if (advancedHead) {
614
+ await lock
615
+ .advanceEventHead(input.preview.resultingEventHead, initial.eventHead)
616
+ .catch(() => undefined);
617
+ }
618
+ throw error;
619
+ }
620
+ });
621
+ return {
622
+ initialized: input.preview.changedPaths.length > 0 || input.preview.preferencesChanged,
623
+ changedPaths: [...input.preview.changedPaths],
624
+ };
625
+ }
626
+ export async function validateOperatingConfiguration(projectRoot) {
627
+ const paths = resolveOperatingPaths(projectRoot);
628
+ const config = JSON.parse(await readFile(paths.config, 'utf8'));
629
+ await assertOperatingArtifact('operating-config', config);
630
+ const protocol = await loadOperatingProtocol();
631
+ const knownRoles = new Set(protocol.listOperatingRoles().map((entry) => entry.id));
632
+ const knownProviders = new Set(protocol.listOperatingProviders().map((entry) => entry.id));
633
+ if (config.enabledRoles.some((role) => !knownRoles.has(role))) {
634
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Configuration has an unknown role.');
635
+ }
636
+ if (config.enabledProviders.some((provider) => !knownProviders.has(provider))) {
637
+ throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Configuration has an unknown provider.');
638
+ }
639
+ return config;
640
+ }
641
+ export function operatingProjectKey(projectRoot) {
642
+ return `operate-${projectMachineKey(projectRoot)}`;
643
+ }
644
+ //# sourceMappingURL=config.js.map