@cassiomc1/forgeloop 1.3.0 → 1.6.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 (163) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/AGENTS.md +1 -0
  3. package/CLAUDE.md +1 -0
  4. package/DOCS_INDEX.md +20 -8
  5. package/EXECUTION_STATE.md +60 -0
  6. package/LOOP_ENGINEERING.md +135 -5
  7. package/LOOP_SYSTEM_DESIGN.md +54 -1
  8. package/PROTOCOL_INTEGRATION.md +87 -0
  9. package/QUALITY_SCORECARD.md +2 -0
  10. package/README.md +69 -9
  11. package/TERMINOLOGY.md +15 -0
  12. package/THIRD_PARTY_NOTICES.md +30 -0
  13. package/THREAT_MODEL.md +59 -1
  14. package/docs/ARTIFACT_REFERENCE.md +183 -0
  15. package/docs/CLI_REFERENCE.md +391 -6
  16. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  17. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  18. package/docs/DOCUMENTATION_GUIDE.md +36 -13
  19. package/docs/EXECUTION_TRACE.md +76 -0
  20. package/docs/GETTING_STARTED.md +1 -0
  21. package/docs/MCP.md +159 -0
  22. package/docs/RECIPES.md +149 -0
  23. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  24. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  25. package/docs/TROUBLESHOOTING.md +217 -3
  26. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  27. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  28. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  29. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  30. package/docs/diagrams/README.md +55 -0
  31. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  32. package/docs/diagrams/manifest.json +42 -0
  33. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  34. package/package.json +21 -8
  35. package/schemas/action.schema.json +100 -0
  36. package/schemas/approval.schema.json +51 -0
  37. package/schemas/capability-policy.schema.json +41 -0
  38. package/schemas/diagnostic-case.schema.json +85 -0
  39. package/schemas/execution-receipt.schema.json +16 -0
  40. package/schemas/hypothesis-disposition.schema.json +16 -0
  41. package/schemas/intervention.schema.json +27 -0
  42. package/schemas/policy-lock.schema.json +1 -0
  43. package/schemas/policy-snapshot.schema.json +2 -0
  44. package/schemas/task-recovery.schema.json +61 -0
  45. package/schemas/trajectory-evaluation.schema.json +64 -0
  46. package/schemas/trajectory-scenario.schema.json +42 -0
  47. package/src/cli.js +267 -347
  48. package/src/commands/action-authorize.js +41 -0
  49. package/src/commands/action-propose.js +10 -0
  50. package/src/commands/action-reconcile.js +10 -0
  51. package/src/commands/action-record.js +47 -0
  52. package/src/commands/action-show.js +10 -0
  53. package/src/commands/action-verify.js +10 -0
  54. package/src/commands/advance.js +7 -2
  55. package/src/commands/approval-request.js +64 -0
  56. package/src/commands/approval-resolve.js +10 -0
  57. package/src/commands/audit.js +5 -0
  58. package/src/commands/baseline.js +3 -3
  59. package/src/commands/eval.js +6 -0
  60. package/src/commands/history.js +18 -0
  61. package/src/commands/init.js +2 -2
  62. package/src/commands/inspect.js +55 -0
  63. package/src/commands/metrics.js +7 -0
  64. package/src/commands/next.js +8 -2
  65. package/src/commands/policy-discover.js +2 -2
  66. package/src/commands/progress.js +6 -2
  67. package/src/commands/record-diagnosis.js +37 -1
  68. package/src/commands/record-hypothesis-disposition.js +45 -0
  69. package/src/commands/record-intervention.js +35 -0
  70. package/src/commands/reflect.js +38 -0
  71. package/src/commands/report.js +9 -1
  72. package/src/commands/run-action.js +18 -0
  73. package/src/commands/status.js +17 -0
  74. package/src/commands/task-create.js +39 -1
  75. package/src/commands/task-list.js +14 -1
  76. package/src/commands/task-lock-status.js +2 -2
  77. package/src/commands/task-recover.js +202 -0
  78. package/src/commands/task-repair-legacy-recovery.js +417 -0
  79. package/src/commands/task-resume.js +172 -0
  80. package/src/commands/task-scope.js +23 -4
  81. package/src/commands/task-show.js +18 -4
  82. package/src/commands/trace.js +34 -0
  83. package/src/commands/validate-protocol.js +40 -15
  84. package/src/core/action-authorization.js +106 -0
  85. package/src/core/action-constants.js +86 -0
  86. package/src/core/action-execution.js +105 -0
  87. package/src/core/action-ledger-projection.js +302 -0
  88. package/src/core/action-model.js +581 -0
  89. package/src/core/action-readiness.js +141 -0
  90. package/src/core/action-reconciliation-policy.js +49 -0
  91. package/src/core/action-reconciliation.js +66 -0
  92. package/src/core/action-verification.js +111 -0
  93. package/src/core/actions.js +462 -0
  94. package/src/core/approvals.js +405 -0
  95. package/src/core/artifact-registry.js +60 -0
  96. package/src/core/audit.js +45 -4
  97. package/src/core/bundles.js +30 -0
  98. package/src/core/capability-policy.js +226 -0
  99. package/src/core/cli-command-definitions.js +260 -5
  100. package/src/core/command-executors.js +543 -0
  101. package/src/core/command-input.js +107 -0
  102. package/src/core/command-runtime.js +117 -0
  103. package/src/core/completion-artifacts.js +39 -15
  104. package/src/core/completion-ownership.js +88 -0
  105. package/src/core/completion-recovery-rebind.js +194 -0
  106. package/src/core/completion.js +70 -0
  107. package/src/core/continuity-reconciliation.js +24 -5
  108. package/src/core/diagnostic-model.js +396 -0
  109. package/src/core/diagnostic-projection.js +51 -0
  110. package/src/core/diagnostic-record.js +360 -0
  111. package/src/core/error-codes.js +461 -1
  112. package/src/core/events.js +171 -2
  113. package/src/core/execution-prerequisites.js +4 -1
  114. package/src/core/execution.js +26 -188
  115. package/src/core/failure-signature.js +70 -0
  116. package/src/core/failure-surface.js +57 -0
  117. package/src/core/filesystem.js +55 -6
  118. package/src/core/history.js +110 -0
  119. package/src/core/hypothesis-projection.js +85 -0
  120. package/src/core/information-gain-projection.js +283 -0
  121. package/src/core/information-gain.js +138 -0
  122. package/src/core/inspect.js +132 -7
  123. package/src/core/integration-invocation-policy.js +217 -0
  124. package/src/core/integration-limits.js +20 -0
  125. package/src/core/integration-resources.js +178 -0
  126. package/src/core/next-action-model.js +94 -0
  127. package/src/core/next-action.js +490 -3
  128. package/src/core/phase.js +42 -22
  129. package/src/core/policy-engine.js +113 -6
  130. package/src/core/preflight-consistency.js +31 -5
  131. package/src/core/preflight.js +19 -2
  132. package/src/core/prepared-execution.js +227 -0
  133. package/src/core/progress.js +41 -4
  134. package/src/core/project-root.js +21 -0
  135. package/src/core/protocol-info.js +61 -0
  136. package/src/core/protocol.js +14 -0
  137. package/src/core/receipt.js +1 -0
  138. package/src/core/reconcile-closure.js +35 -10
  139. package/src/core/recovery-history.js +116 -0
  140. package/src/core/reflection.js +305 -0
  141. package/src/core/resumability.js +57 -3
  142. package/src/core/schema-validation.js +9 -0
  143. package/src/core/strategy-analysis.js +97 -0
  144. package/src/core/task-claim-state.js +272 -0
  145. package/src/core/task-command.js +5 -1
  146. package/src/core/task-conflict-inspection.js +321 -0
  147. package/src/core/task-context.js +32 -29
  148. package/src/core/task-discovery.js +14 -1
  149. package/src/core/task-lock.js +216 -22
  150. package/src/core/task-paths.js +31 -2
  151. package/src/core/task-recovery-migration.js +192 -0
  152. package/src/core/task-recovery.js +205 -0
  153. package/src/core/task-scope.js +33 -1
  154. package/src/core/task-snapshot.js +53 -0
  155. package/src/core/templates.js +9 -0
  156. package/src/core/trace.js +548 -0
  157. package/src/core/trajectory-evaluation.js +71 -0
  158. package/src/core/trajectory-metrics.js +80 -0
  159. package/src/core/transaction.js +36 -2
  160. package/src/core/work-state.js +10 -5
  161. package/src/integration.js +47 -0
  162. package/docs/assets/forgeloop-flow.svg +0 -1
  163. package/docs/forgeloop-flow.mmd +0 -51
@@ -0,0 +1,462 @@
1
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
5
+ import { appendProtocolEvent, readEvents } from "./events.js";
6
+ import {
7
+ canonicalActionFingerprint,
8
+ actionRequiresIdempotency,
9
+ assertActionTransition,
10
+ assertActionAuthorizationDetails,
11
+ assertActionVerificationDetails,
12
+ validateActionArtifact,
13
+ } from "./action-model.js";
14
+ import { ACTION_STATES } from "./action-constants.js";
15
+ import {
16
+ E_ACTION_AUTHORIZATION_INVALID,
17
+ E_ACTION_EVIDENCE_INVALID,
18
+ E_ACTION_IDEMPOTENCY_CONFLICT,
19
+ E_ACTION_IDEMPOTENCY_REQUIRED,
20
+ E_ACTION_INVALID,
21
+ E_ACTION_NOT_FOUND,
22
+ E_ACTION_STATE_MISMATCH,
23
+ E_ACTION_VERIFICATION_REQUIRED,
24
+ } from "./error-codes.js";
25
+ import { assertSafePath, ensureWithin } from "./filesystem.js";
26
+ import { taskActionPath, taskDirectory, TASK_ARTIFACT_FILES } from "./task-paths.js";
27
+
28
+ const STATE_EVENT_NAMES = Object.freeze({
29
+ AUTHORIZED: "ACTION_AUTHORIZED",
30
+ STARTED: "ACTION_STARTED",
31
+ COMMITTED: "ACTION_COMMIT_RECORDED",
32
+ VERIFIED: "ACTION_VERIFIED",
33
+ FAILED: "ACTION_FAILED",
34
+ COMMIT_UNKNOWN: "ACTION_COMMIT_UNKNOWN",
35
+ CANCELLED: "ACTION_CANCELLED",
36
+ });
37
+
38
+ function actionError(code, message) {
39
+ const error = new Error(message);
40
+ error.code = code;
41
+ return error;
42
+ }
43
+
44
+ async function readActionFile(target, packageRoot, taskId, actionId) {
45
+ const relPath = taskActionPath(taskId, actionId);
46
+ await assertSafePath(target, relPath);
47
+ const absolute = ensureWithin(target, relPath);
48
+ let text;
49
+ try {
50
+ text = await readFile(absolute, "utf8");
51
+ } catch (error) {
52
+ if (error?.code === "ENOENT") return null;
53
+ throw error;
54
+ }
55
+ try {
56
+ return JSON.parse(text);
57
+ } catch (error) {
58
+ throw actionError(E_ACTION_INVALID, `durable action artifact is not valid JSON: ${relPath}`);
59
+ }
60
+ }
61
+
62
+ async function writeActionFile(target, packageRoot, taskId, action) {
63
+ const relPath = taskActionPath(taskId, action.actionId);
64
+ await assertSafePath(target, relPath);
65
+ const serialized = `${JSON.stringify(action, null, 2)}\n`;
66
+ const activeTransaction = getActiveTaskTransaction();
67
+ if (activeTransaction) {
68
+ await activeTransaction.stageText(relPath, serialized);
69
+ } else {
70
+ const absolute = ensureWithin(target, relPath);
71
+ await mkdir(path.dirname(absolute), { recursive: true });
72
+ await writeFile(absolute, serialized, "utf8");
73
+ }
74
+ }
75
+
76
+ function taskActionsDirectory(taskId) {
77
+ return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.actions}`;
78
+ }
79
+
80
+ async function listActionFiles(target, packageRoot, taskId) {
81
+ const relDir = taskActionsDirectory(taskId);
82
+ await assertSafePath(target, relDir);
83
+ const absoluteDir = ensureWithin(target, relDir);
84
+ let entries;
85
+ try {
86
+ entries = await readdir(absoluteDir);
87
+ } catch (error) {
88
+ if (error?.code === "ENOENT") return [];
89
+ throw error;
90
+ }
91
+ const actions = [];
92
+ for (const entry of entries) {
93
+ if (!entry.endsWith(".json")) continue;
94
+ const parsed = await readActionFile(target, packageRoot, taskId, entry.replace(/\.json$/, ""));
95
+ if (parsed) actions.push(parsed);
96
+ }
97
+ actions.sort((left, right) => String(left.createdAt).localeCompare(String(right.createdAt)));
98
+ return actions;
99
+ }
100
+
101
+ function assertProposeInput(input) {
102
+ if (!input || typeof input !== "object") {
103
+ throw actionError(E_ACTION_INVALID, "action input must be an object");
104
+ }
105
+ if (typeof input.actionId !== "string" || !/^action-[A-Za-z0-9_-]+$/.test(input.actionId)) {
106
+ throw actionError(E_ACTION_INVALID, "actionId must match action-[A-Za-z0-9_-]+");
107
+ }
108
+ for (const key of ["effectClass", "capability", "operation", "target", "provenance"]) {
109
+ if (typeof input[key] !== "string" || !input[key]) {
110
+ throw actionError(E_ACTION_INVALID, `action input.${key} must be a non-empty string`);
111
+ }
112
+ }
113
+ if (actionRequiresIdempotency(input.effectClass)) {
114
+ if (typeof input.idempotencyKey !== "string" || !input.idempotencyKey) {
115
+ throw actionError(
116
+ E_ACTION_IDEMPOTENCY_REQUIRED,
117
+ `effectClass ${input.effectClass} requires an idempotency key`,
118
+ );
119
+ }
120
+ }
121
+ // Completion-critical actions must declare what postcondition they satisfy;
122
+ // without a requirement, strong verification binding is impossible
123
+ // (INV-FINAL-VERIFY-02). Legacy artifacts remain readable but untrusted.
124
+ if (
125
+ input.requiredForCompletion === true
126
+ && (typeof input.requirement !== "string" || input.requirement.trim() === "")
127
+ ) {
128
+ throw actionError(
129
+ E_ACTION_INVALID,
130
+ "required-for-completion actions require a non-empty requirement",
131
+ );
132
+ }
133
+ }
134
+
135
+ export async function proposeAction(target, { packageRoot, taskId, input }) {
136
+ assertProposeInput(input);
137
+ return withTaskTransaction(
138
+ { target, taskId, operation: "propose-action" },
139
+ async () => {
140
+ const existingByKey = input.idempotencyKey
141
+ ? await findActionByIdempotencyKey(target, { packageRoot, taskId, idempotencyKey: input.idempotencyKey })
142
+ : null;
143
+
144
+ const identityFingerprint = canonicalActionFingerprint({ taskId, ...input });
145
+ if (existingByKey) {
146
+ if (existingByKey.actionFingerprint === identityFingerprint) {
147
+ return { created: false, idempotent: true, action: existingByKey };
148
+ }
149
+ throw actionError(
150
+ E_ACTION_IDEMPOTENCY_CONFLICT,
151
+ `idempotency key ${JSON.stringify(input.idempotencyKey)} already binds to action ${existingByKey.actionId}`,
152
+ );
153
+ }
154
+
155
+ const existingById = await readActionFile(target, packageRoot, taskId, input.actionId);
156
+ if (existingById) {
157
+ if (existingById.actionFingerprint === identityFingerprint) {
158
+ validateActionArtifact(existingById);
159
+ return { created: false, idempotent: true, action: existingById };
160
+ }
161
+ throw actionError(
162
+ E_ACTION_INVALID,
163
+ `action ${input.actionId} already exists with a different immutable identity`,
164
+ );
165
+ }
166
+
167
+ const now = new Date().toISOString();
168
+ const action = {
169
+ schemaVersion: 1,
170
+ taskId,
171
+ actionId: input.actionId,
172
+ actionFingerprint: identityFingerprint,
173
+ effectClass: input.effectClass,
174
+ capability: input.capability,
175
+ operation: input.operation,
176
+ target: input.target,
177
+ idempotencyKey: input.idempotencyKey ?? null,
178
+ requiredForCompletion: Boolean(input.requiredForCompletion),
179
+ requirement: input.requirement ?? null,
180
+ provenance: input.provenance,
181
+ state: "PROPOSED",
182
+ revision: 0,
183
+ createdAt: now,
184
+ updatedAt: now,
185
+ };
186
+ validateActionArtifact(action);
187
+
188
+ await writeActionFile(target, packageRoot, taskId, action);
189
+ await appendProtocolEvent(target, {
190
+ taskId,
191
+ event: "ACTION_PROPOSED",
192
+ fingerprint: action.actionFingerprint,
193
+ details: {
194
+ actionId: action.actionId,
195
+ actionFingerprint: action.actionFingerprint,
196
+ effectClass: action.effectClass,
197
+ capability: action.capability,
198
+ idempotencyKey: action.idempotencyKey,
199
+ requiredForCompletion: action.requiredForCompletion,
200
+ requirement: action.requirement,
201
+ provenance: action.provenance,
202
+ },
203
+ }, packageRoot, { taskId });
204
+
205
+ return { created: true, idempotent: false, action };
206
+ },
207
+ );
208
+ }
209
+
210
+ export async function readAction(target, { packageRoot, taskId, actionId }) {
211
+ const action = await readActionFile(target, packageRoot, taskId, actionId);
212
+ if (!action) {
213
+ throw actionError(E_ACTION_NOT_FOUND, `durable action ${actionId} does not exist for task ${taskId}`);
214
+ }
215
+ return validateActionArtifact(action);
216
+ }
217
+
218
+ export async function listActions(target, { packageRoot, taskId }) {
219
+ const actions = await listActionFiles(target, packageRoot, taskId);
220
+ return actions.map((action) => validateActionArtifact(action));
221
+ }
222
+
223
+ export async function findActionByIdempotencyKey(target, { packageRoot, taskId, idempotencyKey }) {
224
+ if (typeof idempotencyKey !== "string" || !idempotencyKey) {
225
+ throw actionError(E_ACTION_INVALID, "idempotencyKey must be a non-empty string");
226
+ }
227
+ const actions = await listActionFiles(target, packageRoot, taskId);
228
+ const found = actions.find((action) => action.idempotencyKey === idempotencyKey);
229
+ return found ? validateActionArtifact(found) : null;
230
+ }
231
+
232
+ // Security-sensitive states can never be minted through the generic
233
+ // caller-reachable transition primitive; dedicated services below own them.
234
+ const GENERIC_TRANSITION_FORBIDDEN_STATES = Object.freeze(["AUTHORIZED", "VERIFIED"]);
235
+
236
+ async function applyTransition(target, {
237
+ packageRoot,
238
+ taskId,
239
+ actionId,
240
+ to,
241
+ details = {},
242
+ expectedRevision,
243
+ expectedFingerprint,
244
+ }) {
245
+ if (!ACTION_STATES.includes(to)) {
246
+ throw actionError(E_ACTION_INVALID, `unknown action state ${to}`);
247
+ }
248
+ if (typeof details !== "object" || details === null || Array.isArray(details)) {
249
+ throw actionError(E_ACTION_EVIDENCE_INVALID, "transition details must be an object");
250
+ }
251
+ if (details.evidenceRefs !== undefined) {
252
+ if (!Array.isArray(details.evidenceRefs) || details.evidenceRefs.some((ref) => typeof ref !== "string" || !ref || ref.length > 256)) {
253
+ throw actionError(E_ACTION_EVIDENCE_INVALID, "evidenceRefs must be bounded non-empty strings");
254
+ }
255
+ }
256
+
257
+ return withTaskTransaction(
258
+ { target, taskId, operation: "transition-action" },
259
+ async () => {
260
+ const current = await readAction(target, { packageRoot, taskId, actionId });
261
+ assertActionTransition(current.state, to);
262
+
263
+ if (Number.isInteger(expectedRevision) && current.revision !== expectedRevision) {
264
+ throw actionError(
265
+ E_ACTION_STATE_MISMATCH,
266
+ `action revision ${current.revision} does not match expected ${expectedRevision}`,
267
+ );
268
+ }
269
+ if (typeof expectedFingerprint === "string" && current.actionFingerprint !== expectedFingerprint) {
270
+ throw actionError(E_ACTION_INVALID, "action fingerprint does not match the expected fingerprint");
271
+ }
272
+
273
+ const next = {
274
+ ...current,
275
+ state: to,
276
+ revision: current.revision + 1,
277
+ updatedAt: new Date().toISOString(),
278
+ };
279
+ if (details.evidenceRef !== undefined) {
280
+ if (typeof details.evidenceRef !== "string" || !details.evidenceRef || details.evidenceRef.length > 256) {
281
+ throw actionError(E_ACTION_EVIDENCE_INVALID, "evidenceRef must be a bounded non-empty string");
282
+ }
283
+ next.lastEvidenceRef = details.evidenceRef;
284
+ }
285
+ if (details.reconciliationAt !== undefined) {
286
+ next.lastReconciliationAt = details.reconciliationAt;
287
+ }
288
+ if (details.commitResultCode !== undefined) {
289
+ next.commitResultCode = details.commitResultCode;
290
+ }
291
+ validateActionArtifact(next);
292
+ await writeActionFile(target, packageRoot, taskId, next);
293
+
294
+ const baseDetails = {
295
+ actionId: next.actionId,
296
+ actionFingerprint: next.actionFingerprint,
297
+ fromState: current.state,
298
+ toState: to,
299
+ revision: next.revision,
300
+ };
301
+ const boundedDetails = { ...baseDetails };
302
+ for (const key of [
303
+ "evidenceRef",
304
+ "evidenceKind",
305
+ "evidenceRefs",
306
+ "reason",
307
+ "reconciliationOutcome",
308
+ "observedAt",
309
+ "commitResultCode",
310
+ "capabilityDecision",
311
+ "capabilityPolicyFingerprint",
312
+ "policyLockDigest",
313
+ "taskPolicyDigest",
314
+ "approvalId",
315
+ "approvalFingerprint",
316
+ "authorityKind",
317
+ "authorityRef",
318
+ "reportedProvenance",
319
+ ]) {
320
+ if (details[key] !== undefined && details[key] !== null) boundedDetails[key] = details[key];
321
+ }
322
+ if (to === "VERIFIED") boundedDetails.verifiedAt = next.updatedAt;
323
+
324
+ const reconciliationDriven = current.state === "COMMIT_UNKNOWN";
325
+ if (reconciliationDriven) {
326
+ await appendProtocolEvent(target, {
327
+ taskId,
328
+ event: "ACTION_RECONCILED",
329
+ fingerprint: next.actionFingerprint,
330
+ details: { ...boundedDetails, outcome: details.reconciliationOutcome ?? "UNKNOWN" },
331
+ }, packageRoot, { taskId });
332
+ if (to === "COMMITTED") {
333
+ await appendProtocolEvent(target, {
334
+ taskId,
335
+ event: "ACTION_COMMIT_RECORDED",
336
+ fingerprint: next.actionFingerprint,
337
+ details: { ...boundedDetails, reconciled: true },
338
+ }, packageRoot, { taskId });
339
+ }
340
+ } else {
341
+ await appendProtocolEvent(target, {
342
+ taskId,
343
+ event: STATE_EVENT_NAMES[to],
344
+ fingerprint: next.actionFingerprint,
345
+ details: boundedDetails,
346
+ }, packageRoot, { taskId });
347
+ }
348
+
349
+ return next;
350
+ },
351
+ );
352
+ }
353
+
354
+ /**
355
+ * Generic observation transition. Refuses security-sensitive states: callers
356
+ * can observe execution outcomes but can never authorize or verify.
357
+ */
358
+ export async function transitionAction(target, {
359
+ packageRoot,
360
+ taskId,
361
+ actionId,
362
+ to,
363
+ details = {},
364
+ expectedRevision,
365
+ expectedFingerprint,
366
+ }) {
367
+ if (GENERIC_TRANSITION_FORBIDDEN_STATES.includes(to)) {
368
+ throw actionError(
369
+ to === "AUTHORIZED" ? E_ACTION_AUTHORIZATION_INVALID : E_ACTION_VERIFICATION_REQUIRED,
370
+ `state ${to} is owned by a canonical ForgeLoop service and cannot be recorded by a generic caller surface`,
371
+ );
372
+ }
373
+ return applyTransition(target, {
374
+ packageRoot, taskId, actionId, to, details, expectedRevision, expectedFingerprint,
375
+ });
376
+ }
377
+
378
+ /**
379
+ * Canonical authorization transition. Only the authorization service may call
380
+ * it and only with complete policy-bound evidence (INV-AUTH-02).
381
+ */
382
+ export async function transitionAuthorizedAction(target, {
383
+ packageRoot,
384
+ taskId,
385
+ actionId,
386
+ details = {},
387
+ expectedRevision,
388
+ expectedFingerprint,
389
+ }) {
390
+ assertActionAuthorizationDetails(details, { legacyAllowed: false });
391
+ const next = await applyTransition(target, {
392
+ packageRoot, taskId, actionId, to: "AUTHORIZED", details, expectedRevision, expectedFingerprint,
393
+ });
394
+ return next;
395
+ }
396
+
397
+ /**
398
+ * Canonical verification transition. Only the verification service may call
399
+ * it and only with canonical independent postcondition evidence.
400
+ */
401
+ export async function transitionVerifiedAction(target, {
402
+ packageRoot,
403
+ taskId,
404
+ actionId,
405
+ details = {},
406
+ expectedRevision,
407
+ expectedFingerprint,
408
+ }) {
409
+ assertActionVerificationDetails(details);
410
+ return applyTransition(target, {
411
+ packageRoot, taskId, actionId, to: "VERIFIED", details, expectedRevision, expectedFingerprint,
412
+ });
413
+ }
414
+
415
+ export async function detectOrphanActions(target, { packageRoot, taskId }) {
416
+ const actions = await listActionFiles(target, packageRoot, taskId);
417
+ const events = await readEvents(target, packageRoot, { taskId });
418
+ const proposedFingerprints = new Set(
419
+ events
420
+ .filter((event) => event.event === "ACTION_PROPOSED")
421
+ .map((event) => event.details?.actionFingerprint),
422
+ );
423
+ return actions
424
+ .filter((action) => !proposedFingerprints.has(action.actionFingerprint))
425
+ .map((action) => action.actionId);
426
+ }
427
+
428
+ /**
429
+ * Compatibility wrapper over the canonical deterministic ledger replay. Every
430
+ * action's full chronology is validated (transitions, revisions, fingerprints,
431
+ * authorization, reconciliation, verification) and compared to its artifact.
432
+ */
433
+ export async function validateActionLedgerConsistency(target, { packageRoot, taskId }) {
434
+ const actions = await listActionFiles(target, packageRoot, taskId);
435
+ const { projectActionLedger } = await import("./action-ledger-projection.js");
436
+ const issues = [];
437
+ for (const action of actions) {
438
+ const projection = await projectActionLedger({
439
+ target,
440
+ packageRoot,
441
+ taskId,
442
+ actionId: action.actionId,
443
+ artifact: action,
444
+ });
445
+ if (!projection.valid) {
446
+ for (const error of projection.errors) {
447
+ issues.push({
448
+ actionId: action.actionId,
449
+ code: error.code ?? "E_ACTION_EVIDENCE_INVALID",
450
+ message: error.message,
451
+ });
452
+ }
453
+ continue;
454
+ }
455
+ // Artifact must correspond to a proposed identity in the ledger even when
456
+ // the chronology itself is internally consistent.
457
+ if (projection.state === null) {
458
+ issues.push({ actionId: action.actionId, code: "E_ACTION_EVIDENCE_INVALID", message: `action ${action.actionId} has no matching ACTION_PROPOSED ledger event` });
459
+ }
460
+ }
461
+ return issues;
462
+ }