fraim 2.0.346 → 2.0.349

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 (34) hide show
  1. package/dist/registry/scripts/learning/completion-state.js +80 -4
  2. package/dist/registry/scripts/learning/corpus-audit.js +7 -7
  3. package/dist/registry/scripts/learning/entry-authoring.js +181 -0
  4. package/dist/registry/scripts/learning/entry-format.js +10 -1
  5. package/dist/registry/scripts/learning/outcome-assessment.js +162 -0
  6. package/dist/registry/scripts/learning/outcome-types.js +92 -0
  7. package/dist/registry/scripts/learning/owner-identity.js +11 -0
  8. package/dist/registry/scripts/learning/proposal-queue.js +81 -0
  9. package/dist/registry/scripts/learning/run-application.js +1003 -0
  10. package/dist/registry/scripts/learning/run-publication.js +364 -0
  11. package/dist/registry/scripts/learning/run-record.js +895 -0
  12. package/dist/registry/scripts/learning-usage/learning-source-record.js +131 -0
  13. package/dist/src/cli/commands/learning.js +64 -0
  14. package/dist/src/cli/doctor/checks/scripts-checks.js +53 -1
  15. package/dist/src/cli/fraim.js +2 -0
  16. package/dist/src/cli/learning/inventory.js +285 -0
  17. package/dist/src/cli/learning/queue-operations.js +199 -0
  18. package/dist/src/cli/learning/runtime.js +82 -0
  19. package/dist/src/cli/learning/usage.js +277 -0
  20. package/dist/src/core/fraim-config-schema.generated.js +0 -4
  21. package/dist/src/core/utils/local-registry-resolver.js +13 -3
  22. package/dist/src/local-mcp-server/learning-context-builder.js +109 -379
  23. package/dist/src/local-mcp-server/learning-entry-format.js +25 -0
  24. package/dist/src/local-mcp-server/learning-usage-analysis.js +12 -410
  25. package/dist/src/local-mcp-server/learning-usage-eligibility.js +8 -14
  26. package/dist/src/local-mcp-server/learning-usage-projection.js +5 -22
  27. package/dist/src/local-mcp-server/learning-usage-store.js +93 -393
  28. package/dist/src/local-mcp-server/learning-usage-verify.js +9 -40
  29. package/dist/src/local-mcp-server/stdio-server.js +50 -114
  30. package/dist/src/mcp/tool-schemas.js +2 -1
  31. package/package.json +9 -1
  32. package/dist/src/local-mcp-server/learning-firing-parser.js +0 -247
  33. package/dist/src/local-mcp-server/learning-usage-attestation.js +0 -238
  34. package/dist/src/local-mcp-server/learning-usage-command.js +0 -518
@@ -0,0 +1,895 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RunRecord = exports.RECOVERY_DIRECTORIES = exports.decodeCompletion = exports.canonical = void 0;
7
+ exports.bindCleanupItem = bindCleanupItem;
8
+ exports.validateDeliveryReader = validateDeliveryReader;
9
+ exports.completedRunActions = completedRunActions;
10
+ exports.actionDigest = actionDigest;
11
+ exports.readRunRecord = readRunRecord;
12
+ exports.readSupportedRunRecord = readSupportedRunRecord;
13
+ exports.assertNoRunSyncConflicts = assertNoRunSyncConflicts;
14
+ const node_fs_1 = __importDefault(require("node:fs"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const node_crypto_1 = require("node:crypto");
17
+ const proposal_queue_1 = require("./proposal-queue");
18
+ const entry_authoring_1 = require("./entry-authoring");
19
+ const owner_identity_1 = require("./owner-identity");
20
+ const corpus_audit_1 = require("./corpus-audit");
21
+ const outcome_types_1 = require("./outcome-types");
22
+ const completion_state_1 = require("./completion-state");
23
+ var completion_state_2 = require("./completion-state");
24
+ Object.defineProperty(exports, "canonical", { enumerable: true, get: function () { return completion_state_2.canonicalJournal; } });
25
+ var completion_state_3 = require("./completion-state");
26
+ Object.defineProperty(exports, "decodeCompletion", { enumerable: true, get: function () { return completion_state_3.decodeCompletion; } });
27
+ exports.RECOVERY_DIRECTORIES = ['review-queue', 'review-archive', 'review-evidence', 'archive'];
28
+ function bindCleanupItem(item, home) {
29
+ if (item.location)
30
+ (0, owner_identity_1.assertSameOwner)(item.location.owner, home.owner);
31
+ const relativePath = node_path_1.default.relative(home.contentRoot, item.path).replace(/\\/g, '/');
32
+ if (!exports.RECOVERY_DIRECTORIES.some(directory => relativePath.startsWith(`learnings/${directory}/`)))
33
+ throw new Error('cleanup-owner-unresolved');
34
+ const file = (0, owner_identity_1.ownerArtifactPath)(home, relativePath);
35
+ if (node_path_1.default.resolve(item.path) !== file)
36
+ throw new Error('cleanup-owner-unresolved');
37
+ for (let current = file; current !== node_path_1.default.dirname(node_path_1.default.resolve(home.root)); current = node_path_1.default.dirname(current)) {
38
+ try {
39
+ if (node_fs_1.default.lstatSync(current).isSymbolicLink())
40
+ throw new Error('cleanup-link-conflict');
41
+ }
42
+ catch (error) {
43
+ if (error.code !== 'ENOENT')
44
+ throw error;
45
+ }
46
+ if (current === node_path_1.default.dirname(current))
47
+ throw new Error('cleanup-owner-unresolved');
48
+ }
49
+ return { ...item, path: file, location: { owner: home.owner, relativePath } };
50
+ }
51
+ function sameCleanupIdentity(a, b) {
52
+ return node_path_1.default.resolve(a.path) === node_path_1.default.resolve(b.path) || !!(a.location && b.location
53
+ && (0, completion_state_1.canonicalJournal)(a.location) === (0, completion_state_1.canonicalJournal)(b.location) && a.sha256 === b.sha256);
54
+ }
55
+ function validateDeliveryReader(reader) { validateReader(reader, false); }
56
+ function validateReader(reader, historical) {
57
+ if (!reader || typeof reader !== 'object')
58
+ throw new Error('unsupported-capability-reader');
59
+ if (reader.kind === 'team-context' && Object.keys(reader).sort().join(',') === 'key,kind'
60
+ && ['org', 'manager', 'orgRules', 'managerRules', 'projectContext', 'projectBrief', 'projectRules', 'projectQa'].includes(reader.key))
61
+ return;
62
+ if (reader.kind === 'workspace-config' && Object.keys(reader).sort().join(',') === 'expectedValue,key,kind'
63
+ && ['learning.scoreThreshold', 'learning.usage.firingLogLimit', 'learning.usage.offerLogLimit', ...(historical ? ['learning.usage.retirementOfferThreshold'] : [])].includes(reader.key)
64
+ && Number.isFinite(reader.expectedValue) && reader.expectedValue > 0
65
+ && (reader.key === 'learning.scoreThreshold' || Number.isInteger(reader.expectedValue)))
66
+ return;
67
+ throw new Error('unsupported-capability-reader');
68
+ }
69
+ /**
70
+ * Issue #1857 R6 and D2.
71
+ *
72
+ * `basis` records who reached the decision. A suppression inherited from a
73
+ * matching earlier rejection is a decision the software made, and recording it
74
+ * with only a `reviewer` makes it indistinguishable from one the manager made.
75
+ * That is an audit-integrity problem rather than a cosmetic one: the suppression
76
+ * report tells the manager what was decided on their behalf, and it can only be
77
+ * honest if the record knows the difference. `predecessorId` names the rejection
78
+ * an inherited decision came from, so the report can cite it.
79
+ *
80
+ * `kind` separates the two things a rejection can mean. The manager can refuse a
81
+ * claim, or they can discard a queued learning because a rule or skill now owns
82
+ * it, which `learning-review-presentation` step 6 records as a reject. Both
83
+ * suppress future proposals, so without this the report would tell the manager
84
+ * they refused a claim they actually promoted.
85
+ */
86
+ /**
87
+ * Issue #1857: the provenance fields are optional so records written before this
88
+ * change stay valid, but a half-filled pair is not. An inherited decision without
89
+ * its predecessor cannot be cited in the suppression report, and a `kind` on an
90
+ * approval is meaningless.
91
+ */
92
+ function validDecisionProvenance(d) {
93
+ if (d.basis !== undefined && d.basis !== 'manager' && d.basis !== 'inherited')
94
+ return false;
95
+ if (d.basis === 'inherited' && (typeof d.predecessorId !== 'string' || !d.predecessorId.trim()))
96
+ return false;
97
+ if (d.predecessorId !== undefined && d.basis !== 'inherited')
98
+ return false;
99
+ if (d.kind !== undefined && (d.action !== 'reject' || (d.kind !== 'refused' && d.kind !== 'superseded')))
100
+ return false;
101
+ return true;
102
+ }
103
+ /** Only terminal, attributable actions with terminal dependencies belong in a receipt. */
104
+ function completedRunActions(state) {
105
+ const remaining = new Map(Object.values(state.actions).filter(action => action.decision
106
+ && (action.decision.action === 'reject' ? !action.application : action.application?.stage === 'applied')
107
+ && action.sourceRefs.every(ref => action.sourceHashes?.[ref])).map(action => [action.actionId, action]));
108
+ let changed = true;
109
+ while (changed) {
110
+ changed = false;
111
+ for (const [id, action] of remaining)
112
+ if (action.dependencies.some(dependency => !remaining.has(dependency))) {
113
+ remaining.delete(id);
114
+ changed = true;
115
+ }
116
+ }
117
+ return [...remaining.values()];
118
+ }
119
+ const idPattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,159}$/;
120
+ function actionDigest(action) {
121
+ const { decision: _decision, application: _application, digest: _digest, ...revision } = action;
122
+ return (0, proposal_queue_1.hashContent)((0, completion_state_1.canonicalJournal)(revision));
123
+ }
124
+ function checkRef(ref) {
125
+ (0, owner_identity_1.validateOwner)(ref?.owner);
126
+ if (!ref.relativePath || node_path_1.default.isAbsolute(ref.relativePath) || ref.relativePath.includes('\\') || ref.relativePath.split('/').some(p => !p || p === '..' || p === '.') || ref.relativePath.includes(':'))
127
+ throw new Error('invalid-artifact-path');
128
+ }
129
+ function reviewedInput(input) {
130
+ const fields = ['kind', 'operation', 'claimKey', 'evidenceFingerprint', 'sourceRefs', 'sourceHashes', 'target', 'source', 'sourceHash', 'destinationHash', 'copies', 'content', 'specification', 'delivery', 'dependencies', 'verification'];
131
+ const result = {};
132
+ for (const key of fields)
133
+ if (input[key] !== undefined)
134
+ result[key] = structuredClone(input[key]);
135
+ return result;
136
+ }
137
+ function validateAction(action, historical = false) {
138
+ if (!['learning', 'capability'].includes(action.kind) || !['insert', 'update', 'move', 'remove', 'deliver', 'reconcile'].includes(action.operation))
139
+ throw new Error('invalid-action');
140
+ if (action.source !== undefined && action.operation !== 'move')
141
+ throw new Error('invalid-action-source');
142
+ checkRef(action.target);
143
+ if (action.source)
144
+ checkRef(action.source);
145
+ if (!action.claimKey?.trim() || !action.evidenceFingerprint?.trim() || !Array.isArray(action.sourceRefs) || !action.sourceRefs.length || action.sourceRefs.some(s => typeof s !== 'string' || !s))
146
+ throw new Error('invalid-action-evidence');
147
+ if (action.sourceHashes && (Object.entries(action.sourceHashes).some(([ref, hash]) => !action.sourceRefs.includes(ref) || !/^[a-f0-9]{64}$/.test(hash))))
148
+ throw new Error('invalid-source-hashes');
149
+ if (!Array.isArray(action.dependencies) || new Set(action.dependencies).size !== action.dependencies.length || !Array.isArray(action.verification) || !action.verification.length)
150
+ throw new Error('invalid-action-dependencies');
151
+ if (action.kind === 'learning') {
152
+ if (!/^L-[a-f0-9]{10}$/.test(action.target.entryId || ''))
153
+ throw new Error('invalid-entry-id');
154
+ if (action.operation !== 'remove') {
155
+ (0, entry_authoring_1.validateEntry)(action.content);
156
+ if (action.content.domain.status !== 'resolved')
157
+ throw new Error('needs-domain');
158
+ (0, owner_identity_1.assertLearningTargetPath)(action.target, action.content.domain.value);
159
+ }
160
+ if (action.operation === 'deliver')
161
+ throw new Error('invalid-learning-operation');
162
+ }
163
+ else {
164
+ if (action.operation !== 'deliver' || !action.specification?.trim())
165
+ throw new Error('capability-delivery-specification-required');
166
+ if (action.delivery !== undefined) {
167
+ if (!/^[a-f0-9]{64}$/.test(action.delivery.expectedHash) || !Array.isArray(action.delivery.callers))
168
+ throw new Error('invalid-capability-delivery');
169
+ if (action.delivery.reader !== undefined) {
170
+ // Historic revisions retain exact approval bytes; current proposals and execution use the active reader contract.
171
+ validateReader(action.delivery.reader, historical);
172
+ if (action.delivery.callers.length)
173
+ throw new Error('invalid-capability-reader-callers');
174
+ if (action.delivery.reader.kind === 'workspace-config' && (action.target.owner.level !== 'Project' || action.target.relativePath !== 'config.json'))
175
+ throw new Error('invalid-workspace-config-target');
176
+ }
177
+ else if (!action.delivery.callers.length)
178
+ throw new Error('invalid-capability-delivery');
179
+ if (action.target.owner.level === 'Project' && action.target.relativePath === 'config.json' && action.delivery.reader?.kind !== 'workspace-config')
180
+ throw new Error('workspace-config-reader-required');
181
+ for (const caller of action.delivery.callers)
182
+ checkRef(caller);
183
+ }
184
+ }
185
+ if (['update', 'move', 'remove'].includes(action.operation) && !/^[a-f0-9]{64}$/.test(action.sourceHash || action.destinationHash || ''))
186
+ throw new Error('source-hash-required');
187
+ if (action.operation === 'move' && (!action.source || action.source.entryId !== action.target.entryId || (0, completion_state_1.canonicalJournal)(action.source) === (0, completion_state_1.canonicalJournal)(action.target)))
188
+ throw new Error('invalid-move');
189
+ if (action.operation === 'reconcile') {
190
+ if (action.kind !== 'learning' || action.source || !Array.isArray(action.copies) || !action.copies.length
191
+ || (action.destinationHash !== undefined && !/^[a-f0-9]{64}$/.test(action.destinationHash)))
192
+ throw new Error('invalid-reconciliation');
193
+ const keys = new Set([(0, corpus_audit_1.corpusEntryKey)({ ref: action.target, sha256: '' })]);
194
+ for (const copy of action.copies) {
195
+ checkRef(copy.ref);
196
+ const key = (0, corpus_audit_1.corpusEntryKey)({ ref: copy.ref, sha256: copy.sha256 });
197
+ if (copy.ref.entryId !== action.target.entryId || keys.has(key) || !/^[a-f0-9]{64}$/.test(copy.sha256)
198
+ || !/^learnings\/.+-(preferences|mistake-patterns|validated-patterns|manager-coaching)(\.cold)?\.md$/.test(copy.ref.relativePath))
199
+ throw new Error('invalid-reconciliation');
200
+ keys.add(key);
201
+ }
202
+ }
203
+ else if (action.copies !== undefined)
204
+ throw new Error('invalid-reconciliation');
205
+ }
206
+ function validateApplication(action, application) {
207
+ if (!application || !['prepared', 'written', 'published', 'applied'].includes(application.stage) || !Array.isArray(application.steps))
208
+ throw new Error('invalid-application');
209
+ const refs = action.kind === 'learning' ? [action.target, ...(action.operation === 'move' ? [action.source] : action.copies?.map(copy => copy.ref) || [])] : [];
210
+ if (application.steps.length !== refs.length)
211
+ throw new Error('invalid-application-targets');
212
+ for (const [index, step] of application.steps.entries()) {
213
+ const after = index > 0 || action.operation === 'remove' ? null : (0, entry_authoring_1.renderEntry)(action.target.entryId, action.content).trimEnd();
214
+ const expectedHash = index > 0 ? action.operation === 'reconcile' ? action.copies[index - 1].sha256 : action.sourceHash : action.destinationHash || (action.operation !== 'move' ? action.sourceHash : undefined);
215
+ if ((0, completion_state_1.canonicalJournal)(step.ref) !== (0, completion_state_1.canonicalJournal)(refs[index]) || step.after !== after
216
+ || (index === 0 && (['insert', 'move'].includes(action.operation) || (action.operation === 'reconcile' && !action.destinationHash)) ? step.before !== null : typeof step.before !== 'string' || (0, proposal_queue_1.hashContent)(step.before) !== expectedHash)
217
+ || (step.published && !step.written) || (application.stage === 'applied' && !step.published))
218
+ throw new Error('invalid-application-targets');
219
+ const prior = action.application?.steps[index];
220
+ if (prior && (prior.before !== step.before || prior.after !== step.after || (0, completion_state_1.canonicalJournal)(prior.ref) !== (0, completion_state_1.canonicalJournal)(step.ref)
221
+ || (prior.written && !step.written) || (prior.published && !step.published)))
222
+ throw new Error('invalid-application-transition');
223
+ }
224
+ if (action.application?.stage === 'applied' && application.stage !== 'applied')
225
+ throw new Error('invalid-application-transition');
226
+ }
227
+ function validateReconciliation(item, state) {
228
+ (0, owner_identity_1.validateOwner)(item.owner);
229
+ if (!item.sourceRef || !node_path_1.default.isAbsolute(item.sourceRef) || !/^[a-f0-9]{64}$/.test(item.sourceHash)
230
+ || !exports.RECOVERY_DIRECTORIES.some(directory => item.relativePath?.startsWith(`learnings/${directory}/`))
231
+ || /[\\:]/.test(item.relativePath) || item.relativePath.split('/').some(part => !part || part === '.' || part === '..')
232
+ || !['accounted', 'unrelated', 'raw-source'].includes(item.disposition) || !item.rationale?.trim()
233
+ || item.reviewer !== state.reviewer || !Array.isArray(item.preservedDecisions) || item.preservedDecisions.some(value => typeof value !== 'string' || !value.trim())
234
+ || !Array.isArray(item.actionIds) || new Set(item.actionIds).size !== item.actionIds.length || !Array.isArray(item.actionBindings)
235
+ || item.actionBindings.length !== item.actionIds.length)
236
+ throw new Error('invalid-source-reconciliation');
237
+ for (const [index, id] of item.actionIds.entries()) {
238
+ const action = state.actions[id];
239
+ const binding = item.actionBindings[index];
240
+ if (!action?.decision || action.decision.digest !== action.digest || binding.actionId !== id || binding.revision !== action.revision || binding.digest !== action.digest
241
+ || !action.sourceRefs.some(ref => ref.replace(/\\/g, '/') === item.sourceRef.replace(/\\/g, '/') && action.sourceHashes?.[ref] === item.sourceHash))
242
+ throw new Error('reconciliation-action-conflict');
243
+ }
244
+ }
245
+ function replay(file) {
246
+ const journal = (0, completion_state_1.readRunJournal)(file);
247
+ if (journal.format === 'historical')
248
+ throw new completion_state_1.UnsupportedRunHistoryError();
249
+ let state;
250
+ for (const event of journal.events) {
251
+ const { type, data } = event.payload;
252
+ if (!state) {
253
+ if (type !== 'created' || data.schemaVersion !== 1 || !idPattern.test(data.runId) || !data.reviewer)
254
+ throw new Error('invalid-run');
255
+ (0, owner_identity_1.validateOwner)(data.owner);
256
+ if (data.completionDirectory !== undefined)
257
+ validateCompletionDirectory(data.completionDirectory, data.owner);
258
+ state = { schemaVersion: 1, runId: data.runId, owner: data.owner, reviewer: data.reviewer, sequence: 0, actions: {}, appliedOrder: [], sourceDispositions: [], cleanup: [], reconciliations: [], pendingReviewActionIds: [],
259
+ ...(data.completionDirectory !== undefined ? { completionDirectory: data.completionDirectory } : {}),
260
+ ...(data.sourceParticipants !== undefined ? { sourceParticipants: (0, completion_state_1.normalizeSourceParticipants)(data.sourceParticipants, data.owner) } : {}),
261
+ ...(data.corpusAuditRequired === true ? { corpusAuditRequired: true } : {}) };
262
+ }
263
+ else if (type === 'action') {
264
+ validateAction(data, true);
265
+ if (!idPattern.test(data.actionId) || !Number.isInteger(data.revision) || data.revision < 1 || actionDigest(data) !== data.digest)
266
+ throw new Error('invalid-revision');
267
+ const prior = state.actions[data.actionId];
268
+ if (data.revision !== (prior?.revision || 0) + 1 || prior?.application || data.application || data.decision)
269
+ throw new Error('invalid-revision-transition');
270
+ state.actions[data.actionId] = data;
271
+ }
272
+ else if (type === 'decision') {
273
+ const action = state.actions[data.actionId];
274
+ const d = data.decision;
275
+ if (!action || action.application || d.revision !== action.revision || d.digest !== action.digest || d.reviewer !== state.reviewer || !['approve', 'reject'].includes(d.action) || typeof d.reason !== 'string' || !d.reason.trim() || (d.revisitCondition !== undefined && (d.action !== 'reject' || typeof d.revisitCondition !== 'string' || !d.revisitCondition.trim())) || !validDecisionProvenance(d))
276
+ throw new Error('invalid-decision');
277
+ action.decision = d;
278
+ }
279
+ else if (type === 'decision-batch') {
280
+ if (!Array.isArray(data) || !data.length || new Set(data.map(item => item.actionId)).size !== data.length)
281
+ throw new Error('invalid-batch-decision');
282
+ for (const item of data) {
283
+ const action = state.actions[item.actionId];
284
+ const decision = item.decision;
285
+ if (!action || action.application || decision.action !== 'approve' || decision.revision !== action.revision || decision.digest !== action.digest || decision.reviewer !== state.reviewer || !decision.reason)
286
+ throw new Error('invalid-batch-decision');
287
+ }
288
+ for (const item of data)
289
+ state.actions[item.actionId].decision = item.decision;
290
+ }
291
+ else if (type === 'application') {
292
+ const action = state.actions[data.actionId];
293
+ if (!action || action.decision?.action !== 'approve' || data.digest !== action.digest || !['prepared', 'written', 'published', 'applied'].includes(data.application?.stage))
294
+ throw new Error('invalid-application');
295
+ validateApplication(action, data.application);
296
+ action.application = data.application;
297
+ if (action.application?.stage === 'applied')
298
+ state.appliedOrder = [...state.appliedOrder.filter(id => id !== action.actionId), action.actionId];
299
+ }
300
+ else if (type === 'reconciliation') {
301
+ const { reconciliation, cleanup } = data;
302
+ validateReconciliation(reconciliation, state);
303
+ state.reconciliations = state.reconciliations.filter(item => item.sourceRef !== reconciliation.sourceRef).concat(reconciliation);
304
+ state.cleanup = state.cleanup.filter(item => item.path !== cleanup.path).concat(cleanup);
305
+ }
306
+ else if (type === 'assessment-prepared') {
307
+ (0, outcome_types_1.validateOutcomeAssessmentRecord)(data.record);
308
+ if (data.record.runId !== state.runId || data.record.reviewer !== state.reviewer
309
+ || !Array.isArray(data.sourcePaths) || data.sourcePaths.length !== data.record.sourceRefs.length || data.sourcePaths.some((file) => typeof file !== 'string' || !node_path_1.default.isAbsolute(file))
310
+ || [...(state.outcomeAssessments ?? []), ...(state.pendingOutcomeAssessments ?? [])].some(item => item.digest === data.record.digest))
311
+ throw new Error('invalid-assessment-transition');
312
+ state.pendingOutcomeAssessments = [...(state.pendingOutcomeAssessments ?? []), data.record];
313
+ }
314
+ else if (type === 'assessment-committed') {
315
+ const prepared = state.pendingOutcomeAssessments?.find(item => item.digest === data.digest);
316
+ if (!prepared)
317
+ throw new Error('invalid-assessment-transition');
318
+ state.outcomeAssessments = [...(state.outcomeAssessments ?? []), prepared];
319
+ state.pendingOutcomeAssessments = state.pendingOutcomeAssessments.filter(item => item.digest !== data.digest);
320
+ }
321
+ else if (type === 'corpus-audit') {
322
+ (0, corpus_audit_1.validateCorpusAudit)(data, state.reviewer);
323
+ state.corpusAudit = data;
324
+ }
325
+ else if (type === 'disposition')
326
+ state.sourceDispositions.push(data);
327
+ else if (type === 'cleanup')
328
+ state.cleanup.push(data);
329
+ else if (type === 'source-completion') {
330
+ (0, completion_state_1.validateProcessedSources)(data);
331
+ if (state.processedSources?.some(prior => !data.some((item) => (0, completion_state_1.canonicalJournal)(item) === (0, completion_state_1.canonicalJournal)(prior))))
332
+ throw new Error('completion-sources-conflict');
333
+ state.processedSources = data;
334
+ }
335
+ else if (type === 'checkpoint') {
336
+ const receipt = (0, completion_state_1.decodeCompletion)(`<!-- fraim-sleep-decisions:v1 -->\n\x60\x60\x60json\n${JSON.stringify(data.receipt)}\n\x60\x60\x60\n<!-- /fraim-sleep-decisions -->`, { runId: state.runId, owner: state.owner });
337
+ if (!receipt.deferred || !/^[a-f0-9]{64}$/.test(data.evidenceHash))
338
+ throw new Error('invalid-checkpoint');
339
+ for (const item of [...receipt.decisions, ...receipt.deferred.actions])
340
+ if (state.actions[item.actionId]?.digest !== item.digest || state.actions[item.actionId]?.revision !== item.revision)
341
+ throw new Error('checkpoint-action-conflict');
342
+ const completed = completedRunActions(state);
343
+ if (receipt.decisions.length !== completed.length || receipt.decisions.some(item => !completed.some(action => action.actionId === item.actionId
344
+ && (0, completion_state_1.canonicalJournal)(action.decision) === (0, completion_state_1.canonicalJournal)(item.decision) && (0, completion_state_1.canonicalJournal)(action.target) === (0, completion_state_1.canonicalJournal)(item.target))))
345
+ throw new Error('checkpoint-action-conflict');
346
+ if ((0, completion_state_1.canonicalJournal)(receipt.processedSources) !== (0, completion_state_1.canonicalJournal)(state.processedSources))
347
+ throw new Error('completion-sources-conflict');
348
+ if ((0, completion_state_1.canonicalJournal)(receipt.outcomeAssessments) !== (0, completion_state_1.canonicalJournal)(state.outcomeAssessments))
349
+ throw new Error('completion-assessments-conflict');
350
+ state.checkpoint = data;
351
+ if (receipt.finalizationState === 'complete' && !state.deferredHistory?.some(prior => (0, completion_state_1.canonicalJournal)(prior) === (0, completion_state_1.canonicalJournal)(receipt.deferred)))
352
+ state.deferredHistory = [...(state.deferredHistory || []), receipt.deferred];
353
+ }
354
+ else if (type === 'finalization')
355
+ state.finalization = data;
356
+ else if (type === 'deferred-attempt') {
357
+ (0, completion_state_1.validateDeferredAttempt)(data);
358
+ if (!state.checkpoint || state.checkpoint.receipt.finalizationState !== 'complete' || state.checkpoint.evidenceHash !== data.evidenceHash
359
+ || (data.deferred !== undefined && (0, completion_state_1.canonicalJournal)(state.checkpoint.receipt.deferred) !== (0, completion_state_1.canonicalJournal)(data.deferred)))
360
+ throw new Error('deferred-attempt-conflict');
361
+ state.deferredAttempts = [...(state.deferredAttempts ?? []), data];
362
+ }
363
+ else
364
+ throw new Error('unknown-journal-event');
365
+ state.sequence = event.sequence;
366
+ }
367
+ if (!state)
368
+ throw new Error('empty-journal');
369
+ state.pendingReviewActionIds = Object.values(state.actions).filter(action => !action.decision).map(action => action.actionId);
370
+ return { state, events: journal.events, lastHash: journal.lastHash };
371
+ }
372
+ function readRunRecord(file) { return replay(file).state; }
373
+ /** Enumeration skips only recognized historical records; current corruption must remain visible. */
374
+ function readSupportedRunRecord(file) {
375
+ try {
376
+ return readRunRecord(file);
377
+ }
378
+ catch (error) {
379
+ if (error instanceof completion_state_1.UnsupportedRunHistoryError)
380
+ return undefined;
381
+ throw error;
382
+ }
383
+ }
384
+ function assertNoRunSyncConflicts(directory, runId) {
385
+ if (node_fs_1.default.existsSync(directory) && node_fs_1.default.readdirSync(directory).some(name => name.startsWith(runId + ' ') && /jsonl/i.test(name)))
386
+ throw new Error('journal-sync-conflict');
387
+ }
388
+ /** One append-only record holds every reviewed revision, decision and recovery step. */
389
+ function validateCompletionDirectory(directory, owner) {
390
+ if (typeof directory !== 'string' || !node_path_1.default.isAbsolute(directory) || /[\x00-\x1f]/.test(directory))
391
+ throw new Error('invalid-completion-directory');
392
+ const relative = node_path_1.default.relative((0, completion_state_1.sourcePathKey)(owner.path), (0, completion_state_1.sourcePathKey)(directory));
393
+ if (!relative || relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative))
394
+ throw new Error('invalid-completion-directory');
395
+ }
396
+ class RunRecord {
397
+ constructor(directory, runId, owner, reviewer, completionDirectory = directory) {
398
+ this.directory = directory;
399
+ this.runId = runId;
400
+ this.owner = owner;
401
+ this.reviewer = reviewer;
402
+ this.completionDirectory = completionDirectory;
403
+ (0, owner_identity_1.validateOwner)(owner);
404
+ if (!idPattern.test(runId) || !reviewer?.trim())
405
+ throw new Error('invalid-run-context');
406
+ validateCompletionDirectory(completionDirectory, owner);
407
+ this.file = node_path_1.default.join(directory, runId + '.queue.jsonl');
408
+ this.completionFile = node_path_1.default.join(completionDirectory, runId + '-completion.md');
409
+ for (const file of [this.file, this.completionFile])
410
+ if (node_fs_1.default.existsSync(file) && node_fs_1.default.lstatSync(file).isSymbolicLink())
411
+ throw new Error('run-path-link-conflict');
412
+ }
413
+ read() {
414
+ assertNoRunSyncConflicts(this.directory, this.runId);
415
+ const state = readRunRecord(this.file);
416
+ (0, owner_identity_1.assertSameOwner)(this.owner, state.owner);
417
+ if (state.runId !== this.runId || state.reviewer !== this.reviewer)
418
+ throw new Error('run-context-mismatch');
419
+ this.assertCompletionDirectory(state);
420
+ return state;
421
+ }
422
+ assertCompletionDirectory(state) {
423
+ if ((0, completion_state_1.sourcePathKey)(state.completionDirectory ?? this.directory) !== (0, completion_state_1.sourcePathKey)(this.completionDirectory))
424
+ throw new Error('completion-evidence-path-changed');
425
+ }
426
+ append(type, data, operationId = (0, node_crypto_1.randomUUID)()) {
427
+ const prior = node_fs_1.default.existsSync(this.file) ? replay(this.file) : undefined;
428
+ if (prior)
429
+ this.assertCompletionDirectory(prior.state);
430
+ const payload = { type, data };
431
+ const event = { sequence: (prior?.state.sequence || 0) + 1, previousHash: prior?.lastHash || '', operationId, payloadHash: (0, proposal_queue_1.hashContent)((0, completion_state_1.canonicalJournal)(payload)), payload };
432
+ const fd = node_fs_1.default.openSync(this.file, 'a', 0o600);
433
+ try {
434
+ node_fs_1.default.writeSync(fd, (0, completion_state_1.canonicalJournal)(event) + '\n');
435
+ node_fs_1.default.fsyncSync(fd);
436
+ }
437
+ finally {
438
+ node_fs_1.default.closeSync(fd);
439
+ }
440
+ }
441
+ create(sourceParticipants) {
442
+ const grants = sourceParticipants === undefined ? undefined : (0, completion_state_1.normalizeSourceParticipants)(sourceParticipants, this.owner);
443
+ return (0, proposal_queue_1.withFileLock)(this.file, () => {
444
+ if (node_fs_1.default.existsSync(this.file)) {
445
+ const state = this.read();
446
+ if (grants !== undefined && (0, completion_state_1.canonicalJournal)(grants) !== (0, completion_state_1.canonicalJournal)(state.sourceParticipants ?? []))
447
+ throw new Error('source-participants-conflict');
448
+ return state;
449
+ }
450
+ if (node_fs_1.default.existsSync(this.completionFile))
451
+ throw new Error('run-completed');
452
+ this.append('created', { schemaVersion: 1, runId: this.runId, owner: this.owner, reviewer: this.reviewer, completionDirectory: node_path_1.default.resolve(this.completionDirectory), ...(this.owner.level === 'Manager' ? { corpusAuditRequired: true } : {}), ...(grants !== undefined ? { sourceParticipants: grants } : {}) });
453
+ return this.read();
454
+ });
455
+ }
456
+ mutate(expectedSequence, fn) {
457
+ return (0, proposal_queue_1.withFileLock)(this.file, () => {
458
+ const state = this.read();
459
+ if (!Number.isInteger(expectedSequence) || state.sequence !== expectedSequence)
460
+ throw new Error('stale-sequence');
461
+ if (state.finalization || state.checkpoint?.receipt.finalizationState === 'pending')
462
+ throw new Error('finalization-started');
463
+ return fn(state);
464
+ });
465
+ }
466
+ prepareOutcomeAssessment(record, expectedSequence, sourcePaths) {
467
+ (0, outcome_types_1.validateOutcomeAssessmentRecord)(record);
468
+ if (!Array.isArray(sourcePaths) || sourcePaths.length !== record.sourceRefs.length || sourcePaths.some(file => !node_path_1.default.isAbsolute(file)))
469
+ throw new Error('invalid-assessment-sources');
470
+ this.mutate(expectedSequence, state => {
471
+ if (record.runId !== state.runId || record.reviewer !== state.reviewer)
472
+ throw new Error('run-context-mismatch');
473
+ if ([...(state.outcomeAssessments ?? []), ...(state.pendingOutcomeAssessments ?? [])].some(item => item.digest === record.digest))
474
+ return;
475
+ this.append('assessment-prepared', { record, sourcePaths });
476
+ });
477
+ }
478
+ commitOutcomeAssessment(digest, expectedSequence) {
479
+ this.mutate(expectedSequence, state => {
480
+ if (state.outcomeAssessments?.some(item => item.digest === digest))
481
+ return;
482
+ if (!state.pendingOutcomeAssessments?.some(item => item.digest === digest))
483
+ throw new Error('assessment-not-prepared');
484
+ this.append('assessment-committed', { digest });
485
+ });
486
+ }
487
+ propose(input, expectedSequence) {
488
+ input = reviewedInput(input);
489
+ validateAction(input);
490
+ return this.mutate(expectedSequence, state => {
491
+ for (const dependency of input.dependencies)
492
+ if (!state.actions[dependency])
493
+ throw new Error('missing-dependency');
494
+ const prior = Object.values(state.actions).find(a => a.claimKey === input.claimKey && a.evidenceFingerprint === input.evidenceFingerprint && (0, owner_identity_1.ownerKey)(a.target.owner) === (0, owner_identity_1.ownerKey)(input.target.owner));
495
+ if (prior)
496
+ return prior;
497
+ for (const name of node_fs_1.default.readdirSync(this.directory).filter(n => n.endsWith('-completion.md'))) {
498
+ const history = node_fs_1.default.readFileSync(node_path_1.default.join(this.directory, name), 'utf8');
499
+ let completed;
500
+ try {
501
+ completed = (0, completion_state_1.decodeCompletion)(history);
502
+ }
503
+ catch {
504
+ continue;
505
+ } // Historical formats are interpreted by the agent, not imported here.
506
+ if (completed.decisions.some(d => d.claimKey === input.claimKey && d.evidenceFingerprint === input.evidenceFingerprint && (0, owner_identity_1.ownerKey)(d.owner) === (0, owner_identity_1.ownerKey)(input.target.owner)))
507
+ throw new Error('already-reviewed: unchanged evidence');
508
+ }
509
+ const action = { ...structuredClone(input), actionId: (0, node_crypto_1.randomUUID)(), revision: 1 };
510
+ action.digest = actionDigest(action);
511
+ this.append('action', action);
512
+ return action;
513
+ });
514
+ }
515
+ edit(actionId, revision, input, expectedSequence) {
516
+ input = reviewedInput(input);
517
+ validateAction(input);
518
+ return this.mutate(expectedSequence, state => {
519
+ const prior = state.actions[actionId];
520
+ if (!prior || prior.revision !== revision)
521
+ throw new Error('stale-revision');
522
+ if (state.checkpoint?.receipt.decisions.some(item => item.actionId === actionId))
523
+ throw new Error('action-completed');
524
+ if (prior.application)
525
+ throw new Error('application-started');
526
+ if (prior.content && input.content && (0, completion_state_1.canonicalJournal)(prior.content.trace) !== (0, completion_state_1.canonicalJournal)(input.content.trace))
527
+ throw new Error('immutable-history');
528
+ if (prior.target.entryId !== input.target.entryId)
529
+ throw new Error('immutable-entry-id');
530
+ if (input.dependencies.includes(actionId) || input.dependencies.some(id => !state.actions[id]))
531
+ throw new Error('invalid-dependencies');
532
+ const reaches = (id, visited = new Set()) => {
533
+ if (id === actionId)
534
+ return true;
535
+ if (visited.has(id))
536
+ return false;
537
+ visited.add(id);
538
+ return state.actions[id]?.dependencies.some(dependency => reaches(dependency, visited)) || false;
539
+ };
540
+ if (input.dependencies.some(id => reaches(id)))
541
+ throw new Error('dependency-cycle');
542
+ const action = { ...structuredClone(input), actionId, revision: revision + 1 };
543
+ action.digest = actionDigest(action);
544
+ this.append('action', action);
545
+ return action;
546
+ });
547
+ }
548
+ decide(actionId, revision, decision, expectedSequence) {
549
+ return this.mutate(expectedSequence, state => {
550
+ const action = state.actions[actionId];
551
+ if (!action || action.revision !== revision)
552
+ throw new Error('stale-revision');
553
+ if (state.checkpoint?.receipt.decisions.some(item => item.actionId === actionId))
554
+ throw new Error('action-completed');
555
+ if (action.application || decision.reviewer !== state.reviewer || typeof decision.reason !== 'string' || !decision.reason.trim() || !['approve', 'reject'].includes(decision.action) || (decision.revisitCondition !== undefined && (decision.action !== 'reject' || typeof decision.revisitCondition !== 'string' || !decision.revisitCondition.trim())) || !validDecisionProvenance(decision))
556
+ throw new Error('invalid-decision');
557
+ this.append('decision', { actionId, decision: { ...decision, revision, digest: action.digest, at: new Date().toISOString() } });
558
+ return this.read().actions[actionId];
559
+ });
560
+ }
561
+ decideBatch(tuples, reviewer, reason, expectedSequence) {
562
+ return this.mutate(expectedSequence, state => {
563
+ if (reviewer !== state.reviewer || !reason.trim() || !tuples.length || new Set(tuples.map(t => t.actionId)).size !== tuples.length)
564
+ throw new Error('invalid-batch-decision');
565
+ for (const tuple of tuples) {
566
+ const action = state.actions[tuple.actionId];
567
+ if (!action || action.application || action.revision !== tuple.revision || action.digest !== tuple.digest)
568
+ throw new Error('stale-batch-decision');
569
+ if (state.checkpoint?.receipt.decisions.some(item => item.actionId === tuple.actionId))
570
+ throw new Error('action-completed');
571
+ }
572
+ // One event makes batch approval interruption-atomic.
573
+ this.append('decision-batch', tuples.map(tuple => ({ actionId: tuple.actionId, decision: { action: 'approve', revision: tuple.revision, digest: tuple.digest, reviewer, reason, at: new Date().toISOString() } })));
574
+ return this.read();
575
+ });
576
+ }
577
+ recoverTail(expectedPrefixHash) {
578
+ return (0, proposal_queue_1.withFileLock)(this.file, () => {
579
+ const raw = node_fs_1.default.readFileSync(this.file, 'utf8');
580
+ if (raw.endsWith('\n'))
581
+ return this.read();
582
+ const end = raw.lastIndexOf('\n') + 1;
583
+ const prefix = raw.slice(0, end);
584
+ if (!end || (0, proposal_queue_1.hashContent)(prefix) !== expectedPrefixHash)
585
+ throw new Error('recovery-prefix-conflict');
586
+ // Validate the intact prefix through the same replay parser before truncating.
587
+ const temporary = this.file + '.recovery-check';
588
+ try {
589
+ node_fs_1.default.writeFileSync(temporary, prefix, { flag: 'wx' });
590
+ this.assertCompletionDirectory(replay(temporary).state);
591
+ }
592
+ finally {
593
+ if (node_fs_1.default.existsSync(temporary))
594
+ node_fs_1.default.unlinkSync(temporary);
595
+ }
596
+ const fd = node_fs_1.default.openSync(this.file, 'r+');
597
+ try {
598
+ node_fs_1.default.ftruncateSync(fd, Buffer.byteLength(prefix));
599
+ node_fs_1.default.fsyncSync(fd);
600
+ }
601
+ finally {
602
+ node_fs_1.default.closeSync(fd);
603
+ }
604
+ return this.read();
605
+ });
606
+ }
607
+ progress(actionId, application) {
608
+ const state = this.read();
609
+ const action = state.actions[actionId];
610
+ if (!action || action.decision?.action !== 'approve' || action.decision.digest !== action.digest)
611
+ throw new Error('not-approved');
612
+ validateApplication(action, application);
613
+ this.append('application', { actionId, digest: action.digest, application });
614
+ }
615
+ recordCleanup(item, expectedSequence) {
616
+ // The normal run location supplies an unambiguous same-owner home for
617
+ // direct registrations. Cross-owner reconciliations bind their resolved home explicitly.
618
+ if (!item.location) {
619
+ const root = node_fs_1.default.realpathSync.native(this.owner.path);
620
+ for (const contentRoot of [node_path_1.default.join(root, 'personalized-employee'), root]) {
621
+ const relative = node_path_1.default.relative(contentRoot, item.path).replace(/\\/g, '/');
622
+ if (exports.RECOVERY_DIRECTORIES.some(directory => relative.startsWith(`learnings/${directory}/`))) {
623
+ item = bindCleanupItem(item, { owner: this.owner, level: this.owner.level, root, contentRoot, backend: 'local-folder', requiresPublish: false });
624
+ break;
625
+ }
626
+ }
627
+ }
628
+ this.mutate(expectedSequence, () => this.append('cleanup', item));
629
+ }
630
+ /** Record agent interpretation only after exact source identity and current decisions are bound. */
631
+ reconcileSource(input, home, expectedSequence) {
632
+ (0, owner_identity_1.assertSameOwner)(input.owner, home.owner);
633
+ const file = (0, owner_identity_1.ownerArtifactPath)(home, input.relativePath);
634
+ const cleanup = bindCleanupItem({ path: file, sha256: input.sha256,
635
+ outcome: input.disposition === 'accounted' ? 'pending' : 'retained',
636
+ reason: input.disposition === 'accounted' ? 'reviewed-source' : input.disposition }, home);
637
+ if (!node_fs_1.default.lstatSync(file).isFile() || (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(file, 'utf8')) !== input.sha256)
638
+ throw new Error('source-content-conflict');
639
+ return this.mutate(expectedSequence, state => {
640
+ const reconciliation = {
641
+ sourceRef: file, sourceHash: input.sha256, owner: input.owner, relativePath: input.relativePath,
642
+ disposition: input.disposition, rationale: input.rationale, preservedDecisions: input.preservedDecisions, reviewer: input.reviewer,
643
+ actionIds: input.actionIds, actionBindings: (input.actionIds || []).map(actionId => {
644
+ const action = state.actions[actionId];
645
+ if (!action)
646
+ throw new Error('reconciliation-action-missing');
647
+ return { actionId, revision: action.revision, digest: action.digest };
648
+ }),
649
+ };
650
+ validateReconciliation(reconciliation, state);
651
+ this.append('reconciliation', { reconciliation, cleanup });
652
+ return this.read();
653
+ });
654
+ }
655
+ assertReconciliations() {
656
+ const state = this.read();
657
+ for (const item of state.reconciliations)
658
+ validateReconciliation(item, state);
659
+ }
660
+ prepareSourceCompletion(sources, expectedSequence, extend = false) {
661
+ (0, completion_state_1.validateProcessedSources)(sources);
662
+ this.mutate(expectedSequence, state => {
663
+ if (state.processedSources && (0, completion_state_1.canonicalJournal)(state.processedSources) !== (0, completion_state_1.canonicalJournal)(sources) && (!extend || state.processedSources.some(prior => !sources.some(item => (0, completion_state_1.canonicalJournal)(item) === (0, completion_state_1.canonicalJournal)(prior)))))
664
+ throw new Error('completion-sources-conflict');
665
+ if ((0, completion_state_1.canonicalJournal)(state.processedSources) !== (0, completion_state_1.canonicalJournal)(sources))
666
+ this.append('source-completion', sources);
667
+ });
668
+ }
669
+ recordCorpusAudit(input, expectedSequence) {
670
+ return this.mutate(expectedSequence, state => {
671
+ const audit = structuredClone(input);
672
+ for (const disposition of audit.dispositions)
673
+ disposition.actionBindings = disposition.actionIds.map(id => {
674
+ const action = state.actions[id];
675
+ if (!action)
676
+ throw new Error('corpus-audit-action-missing');
677
+ const refs = [action.target, ...(action.source ? [action.source] : []), ...(action.copies?.map(copy => copy.ref) || [])];
678
+ if (!refs.some(ref => (0, corpus_audit_1.corpusEntryKey)({ ref, sha256: '' }) === (0, corpus_audit_1.corpusEntryKey)(disposition)))
679
+ throw new Error('corpus-audit-action-unrelated');
680
+ return { actionId: id, revision: action.revision, digest: action.digest };
681
+ });
682
+ (0, corpus_audit_1.validateCorpusAudit)(audit, state.reviewer);
683
+ this.append('corpus-audit', audit);
684
+ return this.read();
685
+ });
686
+ }
687
+ complete(summary, inventory, afterStage, verifyApplied, retrospective, finishSources, deferred) {
688
+ return (0, proposal_queue_1.withFileLock)(this.file, () => {
689
+ assertNoRunSyncConflicts(this.directory, this.runId);
690
+ const inspect = () => {
691
+ const items = new Map();
692
+ for (const item of inventory()) {
693
+ const key = node_path_1.default.resolve(item.path);
694
+ const prior = items.get(key);
695
+ if (prior && (0, completion_state_1.canonicalJournal)(prior) !== (0, completion_state_1.canonicalJournal)(item))
696
+ throw new Error('cleanup-inventory-conflict');
697
+ if (item.outcome === 'retained' && item.reason !== 'unrelated' && item.reason !== 'raw-source')
698
+ throw new Error('cleanup-unresolved');
699
+ items.set(key, item);
700
+ }
701
+ return [...items.values()];
702
+ };
703
+ const finishedInventory = () => {
704
+ const items = inspect();
705
+ if (items.some(item => item.outcome === 'pending'))
706
+ throw new Error('cleanup-incomplete');
707
+ return items;
708
+ };
709
+ if (!node_fs_1.default.existsSync(this.file)) {
710
+ const result = (0, completion_state_1.decodeCompletion)(node_fs_1.default.readFileSync(this.completionFile, 'utf8'), { runId: this.runId, owner: this.owner });
711
+ if (result.finalizationState !== 'complete')
712
+ throw new Error('incomplete-finalization');
713
+ if ((0, completion_state_1.canonicalJournal)(result.retrospective) !== (0, completion_state_1.canonicalJournal)(retrospective))
714
+ throw new Error('completion-retrospective-conflict');
715
+ verifyApplied?.(true);
716
+ finishedInventory();
717
+ return result;
718
+ }
719
+ const state = this.read();
720
+ if (state.pendingOutcomeAssessments?.length && !deferred)
721
+ throw new Error('assessment-processing-pending');
722
+ if (!summary?.trim() || summary.includes('fraim-sleep-decisions'))
723
+ throw new Error('invalid-completion-summary');
724
+ const actions = deferred ? completedRunActions(state) : Object.values(state.actions);
725
+ if (deferred) {
726
+ (0, completion_state_1.validateDeferredWork)(deferred);
727
+ const ids = new Set(actions.map(action => action.actionId));
728
+ if ((0, completion_state_1.canonicalJournal)(deferred.actions.map(item => [item.actionId, item.revision, item.digest])) !== (0, completion_state_1.canonicalJournal)(Object.values(state.actions).filter(action => !ids.has(action.actionId)).map(action => [action.actionId, action.revision, action.digest])))
729
+ throw new Error('checkpoint-action-conflict');
730
+ }
731
+ else {
732
+ if (state.corpusAuditRequired && !state.corpusAudit)
733
+ throw new Error('corpus-audit-required');
734
+ this.assertReconciliations();
735
+ if (actions.some(a => !a.decision || (a.decision.action === 'approve' && a.application?.stage !== 'applied') || (a.decision.action !== 'approve' && a.application)))
736
+ throw new Error('run-unfinished');
737
+ }
738
+ if (actions.some(action => action.sourceRefs.some(ref => !action.sourceHashes?.[ref])))
739
+ throw new Error('source-hashes-required: record the actual source hash before completion');
740
+ if ((deferred || state.corpusAudit || actions.some(action => action.application)) && !verifyApplied)
741
+ throw new Error('completion-verifier-required');
742
+ verifyApplied?.();
743
+ const snapshot = deferred ? [] : inspect();
744
+ const reconciliationHistory = actions.filter(action => action.operation === 'reconcile' && action.application?.stage === 'applied').map(action => ({
745
+ actionId: action.actionId, revision: action.revision, digest: action.digest, target: action.target,
746
+ resultHash: (0, proposal_queue_1.hashContent)(action.application.steps[0].after),
747
+ occurrences: action.application.steps.filter(step => step.before !== null).map(step => ({ ref: step.ref, sha256: (0, proposal_queue_1.hashContent)(step.before), before: step.before })),
748
+ }));
749
+ const result = {
750
+ schemaVersion: 1, runId: this.runId, owner: this.owner, finalizationState: 'pending',
751
+ decisions: actions.map(a => ({ actionId: a.actionId, revision: a.revision, digest: a.digest, owner: a.target.owner, target: a.target, claimKey: a.claimKey, evidenceFingerprint: a.evidenceFingerprint, sourceRefs: a.sourceRefs, decision: a.decision, dependencies: a.dependencies.map(id => ({ actionId: id, outcome: state.actions[id].decision?.action === 'approve' ? 'applied' : 'rejected' })), ...(a.application?.publicationRef ? { publicationRef: a.application.publicationRef } : {}) })),
752
+ sourceDispositions: actions.flatMap(a => a.sourceRefs.map(sourceRef => ({ sourceRef, sourceHash: a.sourceHashes[sourceRef], outcome: a.decision.action === 'approve' ? 'applied' : 'rejected', actionId: a.actionId }))),
753
+ cleanup: snapshot,
754
+ ...(state.processedSources ? { processedSources: state.processedSources } : {}),
755
+ ...(state.sourceParticipants !== undefined ? { sourceParticipants: state.sourceParticipants } : {}),
756
+ ...(state.outcomeAssessments?.length ? { outcomeAssessments: state.outcomeAssessments } : {}),
757
+ ...(retrospective ? { retrospective } : {}),
758
+ ...(deferred ? { deferred } : {
759
+ ...(state.reconciliations.length ? { reconciliations: state.reconciliations } : {}),
760
+ ...(state.corpusAudit ? { corpusAudit: state.corpusAudit } : {}),
761
+ ...(reconciliationHistory.length ? { reconciliationHistory } : {}),
762
+ ...(state.deferredHistory?.length ? { deferredHistory: state.deferredHistory } : {}),
763
+ ...(state.deferredAttempts?.length ? { deferredAttempts: state.deferredAttempts } : {}),
764
+ }),
765
+ };
766
+ let narrative = `---\nartifactKind: work-evidence\ncontainsFeedback: ${actions.length > 0}\ndate: ${new Date().toISOString().slice(0, 10)}\n---\n# Sleep completion: ${this.runId}\n\n${summary}\n\nApplied: ${actions.filter(a => a.application?.stage === 'applied').length}; rejected: ${actions.filter(a => a.decision?.action === 'reject').length}.\n\n## Decisions and source disposition\n\n`;
767
+ // Submission and feedback already own this path before the completion receipt exists.
768
+ const existingEvidence = node_fs_1.default.existsSync(this.completionFile) ? node_fs_1.default.readFileSync(this.completionFile, 'utf8') : undefined;
769
+ const hasReceipt = existingEvidence?.includes('fraim-sleep-decisions');
770
+ if (existingEvidence && !hasReceipt) {
771
+ const header = existingEvidence.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
772
+ if (!header || (header[1].match(/^artifactKind:/gm) || []).length !== 1 || !/^artifactKind: work-evidence\s*$/m.test(header[1])
773
+ || (header[1].match(/^containsFeedback:/gm) || []).length !== 1 || !/^containsFeedback: (true|false)\s*$/m.test(header[1]))
774
+ throw new Error('completion-evidence-conflict');
775
+ narrative = existingEvidence + `\n\n## Sleep completion: ${this.runId}\n\n${summary}\n\n## Decisions and source disposition\n\n`;
776
+ }
777
+ let trailingHistory = '\n';
778
+ const writeEvidence = () => {
779
+ if (actions.length)
780
+ narrative = narrative.replace(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/, header => header.replace(/^containsFeedback: false[ \t]*(?=\r?$)/m, 'containsFeedback: true'));
781
+ const encoded = JSON.stringify(result, null, 2).split('<').join(String.raw `\u003c`);
782
+ const body = narrative + `<!-- fraim-sleep-decisions:v1 -->\n\x60\x60\x60json\n${encoded}\n\x60\x60\x60\n<!-- /fraim-sleep-decisions -->` + trailingHistory;
783
+ (0, completion_state_1.decodeCompletion)(body, { runId: this.runId, owner: this.owner });
784
+ const previousEvidenceHash = node_fs_1.default.existsSync(this.completionFile) ? (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(this.completionFile, 'utf8')) : undefined;
785
+ // A replacement intent names both recoverable states. A crash before
786
+ // or after the atomic rename can resume without accepting a third state.
787
+ if (deferred)
788
+ this.append('checkpoint', { evidenceHash: (0, proposal_queue_1.hashContent)(body), previousEvidenceHash, receipt: result });
789
+ else if (previousEvidenceHash)
790
+ this.append('finalization', { evidenceHash: (0, proposal_queue_1.hashContent)(body), previousEvidenceHash });
791
+ (0, proposal_queue_1.atomicWrite)(this.completionFile, body);
792
+ const verified = node_fs_1.default.readFileSync(this.completionFile, 'utf8');
793
+ if ((0, proposal_queue_1.hashContent)(verified) !== (0, proposal_queue_1.hashContent)(body))
794
+ throw new Error('completion-write-conflict');
795
+ (0, completion_state_1.decodeCompletion)(verified, { runId: this.runId, owner: this.owner });
796
+ if (!deferred && !previousEvidenceHash)
797
+ this.append('finalization', { evidenceHash: (0, proposal_queue_1.hashContent)(body) });
798
+ };
799
+ if (node_fs_1.default.existsSync(this.completionFile) && hasReceipt) {
800
+ const body = node_fs_1.default.readFileSync(this.completionFile, 'utf8');
801
+ const prior = (0, completion_state_1.decodeCompletion)(body, { runId: this.runId, owner: this.owner });
802
+ if (!deferred && !prior.deferred && prior.finalizationState === 'complete' && (0, completion_state_1.canonicalJournal)(prior.retrospective) !== (0, completion_state_1.canonicalJournal)(retrospective))
803
+ throw new Error('completion-retrospective-conflict');
804
+ if (!deferred && state.finalization && ![state.finalization.evidenceHash, state.finalization.previousEvidenceHash].includes((0, proposal_queue_1.hashContent)(body)))
805
+ throw new Error('completion-evidence-conflict');
806
+ if (deferred || prior.deferred) {
807
+ if (!state.checkpoint || ![state.checkpoint.evidenceHash, state.checkpoint.previousEvidenceHash].includes((0, proposal_queue_1.hashContent)(body))
808
+ || prior.decisions.some(item => !result.decisions.some(current => (0, completion_state_1.canonicalJournal)(current) === (0, completion_state_1.canonicalJournal)(item)))
809
+ || prior.processedSources?.some(item => !result.processedSources?.some(current => (0, completion_state_1.canonicalJournal)(current) === (0, completion_state_1.canonicalJournal)(item))))
810
+ throw new Error('completion-decisions-conflict');
811
+ }
812
+ else if ((0, completion_state_1.canonicalJournal)(prior.decisions) !== (0, completion_state_1.canonicalJournal)(result.decisions) || (0, completion_state_1.canonicalJournal)(prior.sourceDispositions) !== (0, completion_state_1.canonicalJournal)(result.sourceDispositions) || (0, completion_state_1.canonicalJournal)(prior.reconciliations) !== (0, completion_state_1.canonicalJournal)(result.reconciliations) || (0, completion_state_1.canonicalJournal)(prior.processedSources) !== (0, completion_state_1.canonicalJournal)(result.processedSources) || (0, completion_state_1.canonicalJournal)(prior.corpusAudit) !== (0, completion_state_1.canonicalJournal)(result.corpusAudit) || (0, completion_state_1.canonicalJournal)(prior.reconciliationHistory) !== (0, completion_state_1.canonicalJournal)(result.reconciliationHistory) || (0, completion_state_1.canonicalJournal)(prior.outcomeAssessments) !== (0, completion_state_1.canonicalJournal)(result.outcomeAssessments))
813
+ throw new Error('completion-decisions-conflict');
814
+ if (deferred && prior.finalizationState === 'complete' && (0, completion_state_1.canonicalJournal)({ ...prior, finalizationState: 'pending' }) === (0, completion_state_1.canonicalJournal)(result)) {
815
+ finishSources?.();
816
+ verifyApplied?.();
817
+ this.append('deferred-attempt', { at: new Date().toISOString(), evidenceHash: (0, proposal_queue_1.hashContent)(body), reason: 'verified-subset-unchanged' });
818
+ return prior;
819
+ }
820
+ // While pending, the agent may update process evidence and the
821
+ // retrospective to explain a failed completion attempt. Exact
822
+ // approved actions stay fixed; the replacement intent binds new bytes.
823
+ narrative = body.slice(0, body.indexOf('<!-- fraim-sleep-decisions:v1 -->'));
824
+ if (prior.deferred && (!deferred || (0, completion_state_1.canonicalJournal)({ ...prior, finalizationState: 'pending' }) !== (0, completion_state_1.canonicalJournal)(result)))
825
+ narrative += `\n## Recovery completion\n\n${summary}\n\n`;
826
+ trailingHistory = body.slice(body.indexOf('<!-- /fraim-sleep-decisions -->') + '<!-- /fraim-sleep-decisions -->'.length);
827
+ if (!deferred && !prior.deferred && prior.finalizationState === 'complete' && !snapshot.some(item => item.outcome === 'pending')) {
828
+ finishedInventory();
829
+ finishSources?.();
830
+ verifyApplied?.(true);
831
+ node_fs_1.default.unlinkSync(this.file);
832
+ return prior;
833
+ }
834
+ if (!deferred && !prior.deferred && prior.finalizationState === 'complete' && snapshot.some(item => item.outcome === 'pending'))
835
+ throw new Error('cleanup-content-reappeared');
836
+ // The fresh inventory authorizes every remaining deletion. Never
837
+ // execute a path merely because it appeared in prior evidence.
838
+ for (const item of deferred ? [] : prior.cleanup) {
839
+ const found = snapshot.find(current => sameCleanupIdentity(item, current));
840
+ if (!found)
841
+ throw new Error('cleanup-inventory-missing');
842
+ if ((item.outcome === 'deleted' || item.outcome === 'absent') && found.outcome === 'pending')
843
+ throw new Error('cleanup-content-reappeared');
844
+ if (found.outcome === 'absent' && item.outcome === 'deleted')
845
+ found.outcome = 'deleted';
846
+ }
847
+ }
848
+ writeEvidence();
849
+ afterStage?.('evidence');
850
+ finishSources?.();
851
+ afterStage?.('sources');
852
+ if (!deferred) {
853
+ for (const item of result.cleanup) {
854
+ if (item.outcome !== 'pending')
855
+ continue;
856
+ const authorized = inspect().find(current => node_path_1.default.resolve(current.path) === node_path_1.default.resolve(item.path));
857
+ if (!authorized || !['pending', 'absent'].includes(authorized.outcome) || authorized.sha256 !== item.sha256)
858
+ throw new Error('cleanup-authorization-changed');
859
+ if (node_fs_1.default.existsSync(item.path)) {
860
+ if (!item.sha256 || (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(item.path, 'utf8')) !== item.sha256)
861
+ throw new Error('cleanup-content-conflict');
862
+ node_fs_1.default.unlinkSync(item.path);
863
+ item.outcome = 'deleted';
864
+ }
865
+ else
866
+ item.outcome = 'absent';
867
+ writeEvidence();
868
+ afterStage?.('cleanup');
869
+ }
870
+ const finalInventory = finishedInventory();
871
+ const completed = new Map(result.cleanup.map(item => [node_path_1.default.resolve(item.path), item]));
872
+ for (const item of finalInventory)
873
+ if (!completed.has(node_path_1.default.resolve(item.path)))
874
+ result.cleanup.push(item);
875
+ }
876
+ else
877
+ verifyApplied?.();
878
+ result.finalizationState = 'complete';
879
+ writeEvidence();
880
+ afterStage?.('complete-evidence');
881
+ const binding = deferred ? this.read().checkpoint : this.read().finalization;
882
+ if ((0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(this.completionFile, 'utf8')) !== binding?.evidenceHash)
883
+ throw new Error('completion-evidence-conflict');
884
+ // Complete attests this receipt's terminal subset. Only full coverage permits retiring run recovery.
885
+ if (!deferred) {
886
+ finishedInventory();
887
+ finishSources?.();
888
+ verifyApplied?.(true);
889
+ node_fs_1.default.unlinkSync(this.file);
890
+ }
891
+ return result;
892
+ });
893
+ }
894
+ }
895
+ exports.RunRecord = RunRecord;