@cassiomc1/forgeloop 1.5.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 (126) hide show
  1. package/DOCS_INDEX.md +13 -8
  2. package/EXECUTION_STATE.md +20 -0
  3. package/LOOP_ENGINEERING.md +81 -0
  4. package/LOOP_SYSTEM_DESIGN.md +32 -0
  5. package/PROTOCOL_INTEGRATION.md +46 -0
  6. package/QUALITY_SCORECARD.md +2 -0
  7. package/README.md +31 -9
  8. package/THIRD_PARTY_NOTICES.md +15 -0
  9. package/THREAT_MODEL.md +39 -0
  10. package/docs/ARTIFACT_REFERENCE.md +140 -0
  11. package/docs/CLI_REFERENCE.md +294 -3
  12. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  13. package/docs/DOCUMENTATION_GUIDE.md +22 -13
  14. package/docs/EXECUTION_TRACE.md +76 -0
  15. package/docs/MCP.md +33 -0
  16. package/docs/RECIPES.md +67 -0
  17. package/docs/TROUBLESHOOTING.md +106 -2
  18. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  19. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  20. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  21. package/docs/diagrams/README.md +55 -0
  22. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  23. package/docs/diagrams/manifest.json +42 -0
  24. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  25. package/package.json +8 -6
  26. package/schemas/action.schema.json +100 -0
  27. package/schemas/approval.schema.json +51 -0
  28. package/schemas/capability-policy.schema.json +41 -0
  29. package/schemas/diagnostic-case.schema.json +85 -0
  30. package/schemas/execution-receipt.schema.json +16 -0
  31. package/schemas/hypothesis-disposition.schema.json +16 -0
  32. package/schemas/intervention.schema.json +27 -0
  33. package/schemas/policy-lock.schema.json +1 -0
  34. package/schemas/policy-snapshot.schema.json +2 -0
  35. package/schemas/trajectory-evaluation.schema.json +64 -0
  36. package/schemas/trajectory-scenario.schema.json +42 -0
  37. package/src/cli.js +94 -0
  38. package/src/commands/action-authorize.js +41 -0
  39. package/src/commands/action-propose.js +10 -0
  40. package/src/commands/action-reconcile.js +10 -0
  41. package/src/commands/action-record.js +47 -0
  42. package/src/commands/action-show.js +10 -0
  43. package/src/commands/action-verify.js +10 -0
  44. package/src/commands/advance.js +7 -2
  45. package/src/commands/approval-request.js +64 -0
  46. package/src/commands/approval-resolve.js +10 -0
  47. package/src/commands/baseline.js +3 -3
  48. package/src/commands/eval.js +6 -0
  49. package/src/commands/history.js +18 -0
  50. package/src/commands/init.js +2 -2
  51. package/src/commands/inspect.js +49 -0
  52. package/src/commands/metrics.js +7 -0
  53. package/src/commands/next.js +8 -2
  54. package/src/commands/policy-discover.js +2 -2
  55. package/src/commands/record-diagnosis.js +37 -1
  56. package/src/commands/record-hypothesis-disposition.js +45 -0
  57. package/src/commands/record-intervention.js +35 -0
  58. package/src/commands/reflect.js +38 -0
  59. package/src/commands/report.js +9 -1
  60. package/src/commands/run-action.js +18 -0
  61. package/src/commands/trace.js +34 -0
  62. package/src/commands/validate-protocol.js +21 -13
  63. package/src/core/action-authorization.js +106 -0
  64. package/src/core/action-constants.js +86 -0
  65. package/src/core/action-execution.js +105 -0
  66. package/src/core/action-ledger-projection.js +302 -0
  67. package/src/core/action-model.js +581 -0
  68. package/src/core/action-readiness.js +141 -0
  69. package/src/core/action-reconciliation-policy.js +49 -0
  70. package/src/core/action-reconciliation.js +66 -0
  71. package/src/core/action-verification.js +111 -0
  72. package/src/core/actions.js +462 -0
  73. package/src/core/approvals.js +405 -0
  74. package/src/core/artifact-registry.js +48 -0
  75. package/src/core/audit.js +25 -0
  76. package/src/core/bundles.js +15 -0
  77. package/src/core/capability-policy.js +226 -0
  78. package/src/core/cli-command-definitions.js +210 -1
  79. package/src/core/command-executors.js +171 -15
  80. package/src/core/command-runtime.js +12 -1
  81. package/src/core/completion-artifacts.js +37 -12
  82. package/src/core/completion-recovery-rebind.js +194 -0
  83. package/src/core/completion.js +70 -0
  84. package/src/core/continuity-reconciliation.js +24 -5
  85. package/src/core/diagnostic-model.js +396 -0
  86. package/src/core/diagnostic-projection.js +51 -0
  87. package/src/core/diagnostic-record.js +360 -0
  88. package/src/core/error-codes.js +343 -0
  89. package/src/core/events.js +41 -1
  90. package/src/core/execution-prerequisites.js +4 -1
  91. package/src/core/execution.js +26 -188
  92. package/src/core/failure-signature.js +70 -0
  93. package/src/core/failure-surface.js +57 -0
  94. package/src/core/history.js +110 -0
  95. package/src/core/hypothesis-projection.js +85 -0
  96. package/src/core/information-gain-projection.js +283 -0
  97. package/src/core/information-gain.js +138 -0
  98. package/src/core/inspect.js +105 -7
  99. package/src/core/integration-invocation-policy.js +47 -0
  100. package/src/core/integration-resources.js +51 -0
  101. package/src/core/next-action-model.js +35 -1
  102. package/src/core/next-action.js +459 -3
  103. package/src/core/phase.js +40 -21
  104. package/src/core/policy-engine.js +113 -6
  105. package/src/core/preflight-consistency.js +31 -5
  106. package/src/core/preflight.js +19 -2
  107. package/src/core/prepared-execution.js +227 -0
  108. package/src/core/progress.js +41 -4
  109. package/src/core/protocol-info.js +48 -0
  110. package/src/core/protocol.js +14 -0
  111. package/src/core/receipt.js +1 -0
  112. package/src/core/reconcile-closure.js +15 -12
  113. package/src/core/reflection.js +305 -0
  114. package/src/core/resumability.js +57 -3
  115. package/src/core/schema-validation.js +8 -0
  116. package/src/core/strategy-analysis.js +97 -0
  117. package/src/core/task-paths.js +28 -0
  118. package/src/core/task-snapshot.js +53 -0
  119. package/src/core/templates.js +8 -0
  120. package/src/core/trace.js +548 -0
  121. package/src/core/trajectory-evaluation.js +71 -0
  122. package/src/core/trajectory-metrics.js +80 -0
  123. package/src/core/transaction.js +8 -0
  124. package/src/core/work-state.js +10 -5
  125. package/docs/assets/forgeloop-flow.svg +0 -1
  126. package/docs/forgeloop-flow.mmd +0 -51
@@ -0,0 +1,405 @@
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 } from "./events.js";
6
+ import { canonicalFingerprint } from "./artifacts.js";
7
+ import {
8
+ assertApprovalIdFormat,
9
+ assertApprovalEventDetails,
10
+ validateApprovalArtifact,
11
+ } from "./action-model.js";
12
+ import {
13
+ E_ACTION_AUTHORITY_REQUIRED,
14
+ E_ACTION_NOT_FOUND,
15
+ E_APPROVAL_ALREADY_RESOLVED,
16
+ E_APPROVAL_INVALID,
17
+ E_APPROVAL_STALE,
18
+ } from "./error-codes.js";
19
+ import { assertSafePath, ensureWithin } from "./filesystem.js";
20
+ import { isTrustedHostAuthorityContext } from "./capability-policy.js";
21
+ import { taskApprovalPath, taskDirectory, TASK_ARTIFACT_FILES } from "./task-paths.js";
22
+ import { readAction } from "./actions.js";
23
+ import { readWorkState } from "./work-state.js";
24
+
25
+ function approvalError(code, message) {
26
+ const error = new Error(message);
27
+ error.code = code;
28
+ return error;
29
+ }
30
+
31
+ async function readApprovalFile(target, taskId, approvalId) {
32
+ const relPath = taskApprovalPath(taskId, approvalId);
33
+ await assertSafePath(target, relPath);
34
+ let text;
35
+ try {
36
+ text = await readFile(ensureWithin(target, relPath), "utf8");
37
+ } catch (error) {
38
+ if (error?.code === "ENOENT") return null;
39
+ throw error;
40
+ }
41
+ try {
42
+ return JSON.parse(text);
43
+ } catch {
44
+ throw approvalError(E_APPROVAL_INVALID, `approval artifact is not valid JSON: ${relPath}`);
45
+ }
46
+ }
47
+
48
+ async function writeApprovalFile(target, taskId, approval) {
49
+ const relPath = taskApprovalPath(taskId, approval.approvalId);
50
+ await assertSafePath(target, relPath);
51
+ const serialized = `${JSON.stringify(approval, null, 2)}\n`;
52
+ const activeTransaction = getActiveTaskTransaction();
53
+ if (activeTransaction) {
54
+ await activeTransaction.stageText(relPath, serialized);
55
+ } else {
56
+ const absolute = ensureWithin(target, relPath);
57
+ await mkdir(path.dirname(absolute), { recursive: true });
58
+ await writeFile(absolute, serialized, "utf8");
59
+ }
60
+ }
61
+
62
+ async function listApprovalFiles(target, taskId) {
63
+ const relDir = `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.approvals}`;
64
+ await assertSafePath(target, relDir);
65
+ const absoluteDir = ensureWithin(target, relDir);
66
+ let entries;
67
+ try {
68
+ entries = await readdir(absoluteDir);
69
+ } catch (error) {
70
+ if (error?.code === "ENOENT") return [];
71
+ throw error;
72
+ }
73
+ const approvals = [];
74
+ for (const entry of entries) {
75
+ if (!entry.endsWith(".json")) continue;
76
+ const parsed = await readApprovalFile(target, taskId, entry.replace(/\.json$/, ""));
77
+ if (parsed) approvals.push(parsed);
78
+ }
79
+ approvals.sort((left, right) => String(left.requestedAt).localeCompare(String(right.requestedAt)));
80
+ return approvals;
81
+ }
82
+
83
+ function approvalBindingFields(approval) {
84
+ return {
85
+ taskId: approval.taskId,
86
+ actionId: approval.actionId,
87
+ actionFingerprint: approval.actionFingerprint,
88
+ contractFingerprint: approval.contractFingerprint,
89
+ taskRevision: approval.taskRevision,
90
+ capability: approval.capability,
91
+ };
92
+ }
93
+
94
+ function approvalFingerprintPayload(approval) {
95
+ return {
96
+ ...approvalBindingFields(approval),
97
+ status: approval.status,
98
+ decision: approval.decision ?? null,
99
+ resolvedAt: approval.resolvedAt ?? null,
100
+ authorityKind: approval.authorityKind ?? null,
101
+ hostGrantRef: approval.hostGrantRef ?? null,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Canonical approval fingerprint. Binds the full resolved approval content —
107
+ * including its resolution authority — so any post-authorization mutation of
108
+ * the approval artifact is detectable during ledger replay/audit.
109
+ */
110
+ export function approvalFingerprint(approval) {
111
+ return canonicalFingerprint(approvalFingerprintPayload(approval));
112
+ }
113
+
114
+ export function assertApprovalFresh(approval, expectedBinding) {
115
+ assertApprovalBinding(approval, expectedBinding);
116
+ if (approval.status !== "APPROVED") {
117
+ throw approvalError(E_APPROVAL_INVALID, `approval ${approval.approvalId} is not APPROVED`);
118
+ }
119
+ return true;
120
+ }
121
+
122
+ /**
123
+ * Validate only the immutable binding between an approval and the current
124
+ * action/task revision. Pending approvals use this check before they become
125
+ * the active guidance target; approval status is intentionally evaluated by
126
+ * the current capability policy, not by historical approval state.
127
+ */
128
+ export function assertApprovalBinding(approval, expectedBinding) {
129
+ if (!expectedBinding || typeof expectedBinding !== "object") {
130
+ throw approvalError(E_APPROVAL_INVALID, "expected binding must be an object");
131
+ }
132
+ for (const [key, value] of Object.entries(approvalBindingFields(approval))) {
133
+ if (expectedBinding[key] !== value) {
134
+ throw approvalError(
135
+ E_APPROVAL_STALE,
136
+ `approval ${approval.approvalId} is stale: bound ${key} does not match the current task/action state`,
137
+ );
138
+ }
139
+ }
140
+ return true;
141
+ }
142
+
143
+ function assertRequestInput(input) {
144
+ if (!input || typeof input !== "object") {
145
+ throw approvalError(E_APPROVAL_INVALID, "approval input must be an object");
146
+ }
147
+ assertApprovalIdFormat(input.approvalId);
148
+ for (const key of ["actionId", "actionFingerprint", "contractFingerprint"]) {
149
+ if (typeof input[key] !== "string" || !input[key]) {
150
+ throw approvalError(E_APPROVAL_INVALID, `approval input.${key} must be a non-empty string`);
151
+ }
152
+ }
153
+ if (!/^[a-f0-9]{64}$/.test(input.actionFingerprint)) {
154
+ throw approvalError(
155
+ E_APPROVAL_INVALID,
156
+ "approval input.actionFingerprint must be a lowercase sha256 hex digest",
157
+ );
158
+ }
159
+ if (!/^[a-f0-9]{64}$/.test(input.contractFingerprint)) {
160
+ throw approvalError(
161
+ E_APPROVAL_INVALID,
162
+ "approval input.contractFingerprint must be a lowercase sha256 hex digest",
163
+ );
164
+ }
165
+ if (!Number.isInteger(input.taskRevision) || input.taskRevision < 0) {
166
+ throw approvalError(E_APPROVAL_INVALID, "approval input.taskRevision must be a non-negative integer");
167
+ }
168
+ if (input.reason !== undefined && input.reason !== null && (typeof input.reason !== "string" || input.reason.length > 512)) {
169
+ throw approvalError(E_APPROVAL_INVALID, "approval input.reason must be a string of at most 512 characters");
170
+ }
171
+ }
172
+
173
+ export async function requestApproval(target, { packageRoot, taskId, input }) {
174
+ assertRequestInput(input);
175
+ return withTaskTransaction(
176
+ { target, taskId, operation: "request-approval" },
177
+ async () => {
178
+ const existing = await readApprovalFile(target, taskId, input.approvalId);
179
+ if (existing) {
180
+ validateApprovalArtifact(existing);
181
+ const existingBinding = approvalBindingFields(existing);
182
+ const nextBinding = { ...approvalBindingFields({ ...input }), taskId };
183
+ for (const key of Object.keys(existingBinding)) {
184
+ if (existingBinding[key] !== nextBinding[key]) {
185
+ throw approvalError(
186
+ E_APPROVAL_INVALID,
187
+ `approval ${input.approvalId} already exists with a different immutable binding`,
188
+ );
189
+ }
190
+ }
191
+ return { created: false, idempotent: true, approval: existing };
192
+ }
193
+
194
+ const now = new Date().toISOString();
195
+ const approval = {
196
+ schemaVersion: 1,
197
+ taskId,
198
+ approvalId: input.approvalId,
199
+ actionId: input.actionId,
200
+ actionFingerprint: input.actionFingerprint,
201
+ contractFingerprint: input.contractFingerprint,
202
+ taskRevision: input.taskRevision,
203
+ capability: input.capability,
204
+ status: "PENDING",
205
+ requestedAt: now,
206
+ reason: input.reason ?? null,
207
+ };
208
+ validateApprovalArtifact(approval);
209
+
210
+ await writeApprovalFile(target, taskId, approval);
211
+ await appendProtocolEvent(target, {
212
+ taskId,
213
+ event: "APPROVAL_REQUESTED",
214
+ fingerprint: approval.contractFingerprint,
215
+ details: {
216
+ approvalId: approval.approvalId,
217
+ actionId: approval.actionId,
218
+ actionFingerprint: approval.actionFingerprint,
219
+ contractFingerprint: approval.contractFingerprint,
220
+ taskRevision: approval.taskRevision,
221
+ capability: approval.capability,
222
+ },
223
+ }, packageRoot, { taskId });
224
+
225
+ return { created: true, idempotent: false, approval };
226
+ },
227
+ );
228
+ }
229
+
230
+ export async function resolveApproval(target, {
231
+ packageRoot,
232
+ taskId,
233
+ approvalId,
234
+ decision,
235
+ authorityKind,
236
+ hostGrantRef,
237
+ authorityContext,
238
+ reason,
239
+ }) {
240
+ assertApprovalIdFormat(approvalId);
241
+ if (!["APPROVED", "REJECTED"].includes(decision)) {
242
+ throw approvalError(E_APPROVAL_INVALID, "decision must be APPROVED or REJECTED");
243
+ }
244
+ if (!["CALLER_ACKNOWLEDGED", "HOST_ATTESTED"].includes(authorityKind)) {
245
+ throw approvalError(E_APPROVAL_INVALID, "authorityKind must be CALLER_ACKNOWLEDGED or HOST_ATTESTED");
246
+ }
247
+ if (authorityKind === "HOST_ATTESTED") {
248
+ if (!isTrustedHostAuthorityContext(authorityContext)) {
249
+ throw approvalError(
250
+ E_ACTION_AUTHORITY_REQUIRED,
251
+ "HOST_ATTESTED approval resolution requires a trusted host-boundary authority context",
252
+ );
253
+ }
254
+ if (typeof hostGrantRef !== "string" || !hostGrantRef || hostGrantRef.length > 256) {
255
+ throw approvalError(
256
+ E_APPROVAL_INVALID,
257
+ "HOST_ATTESTED resolution requires a bounded non-empty hostGrantRef supplied by the host boundary",
258
+ );
259
+ }
260
+ if (typeof authorityContext?.grantRef === "string" && authorityContext.grantRef !== hostGrantRef) {
261
+ throw approvalError(E_ACTION_AUTHORITY_REQUIRED, "hostGrantRef does not match the trusted host authority context");
262
+ }
263
+ } else if (hostGrantRef !== undefined && hostGrantRef !== null) {
264
+ throw approvalError(
265
+ E_APPROVAL_INVALID,
266
+ "CALLER_ACKNOWLEDGED resolutions cannot carry a hostGrantRef",
267
+ );
268
+ }
269
+ if (reason !== undefined && reason !== null && (typeof reason !== "string" || reason.length > 512)) {
270
+ throw approvalError(E_APPROVAL_INVALID, "reason must be a string of at most 512 characters");
271
+ }
272
+
273
+ return withTaskTransaction(
274
+ { target, taskId, operation: "resolve-approval" },
275
+ async () => {
276
+ const current = await readApprovalFile(target, taskId, approvalId);
277
+ if (!current) {
278
+ throw approvalError(E_ACTION_NOT_FOUND, `approval ${approvalId} does not exist for task ${taskId}`);
279
+ }
280
+ validateApprovalArtifact(current);
281
+ if (current.status !== "PENDING") {
282
+ throw approvalError(
283
+ E_APPROVAL_ALREADY_RESOLVED,
284
+ `approval ${approvalId} is already ${current.status}`,
285
+ );
286
+ }
287
+
288
+ const resolved = {
289
+ ...current,
290
+ status: decision,
291
+ decision,
292
+ resolvedAt: new Date().toISOString(),
293
+ authorityKind,
294
+ hostGrantRef: authorityKind === "HOST_ATTESTED" ? hostGrantRef : null,
295
+ reason: reason ?? current.reason ?? null,
296
+ };
297
+ validateApprovalArtifact(resolved);
298
+ await writeApprovalFile(target, taskId, resolved);
299
+
300
+ const details = {
301
+ approvalId: resolved.approvalId,
302
+ actionId: resolved.actionId,
303
+ actionFingerprint: resolved.actionFingerprint,
304
+ decision,
305
+ authorityKind,
306
+ };
307
+ if (authorityKind === "HOST_ATTESTED") details.hostGrantRef = hostGrantRef;
308
+ assertApprovalEventDetails({ event: "APPROVAL_RESOLVED", details });
309
+ await appendProtocolEvent(target, {
310
+ taskId,
311
+ event: "APPROVAL_RESOLVED",
312
+ fingerprint: resolved.contractFingerprint,
313
+ details,
314
+ }, packageRoot, { taskId });
315
+
316
+ return resolved;
317
+ },
318
+ );
319
+ }
320
+
321
+ export async function readApproval(target, { packageRoot, taskId, approvalId }) {
322
+ const approval = await readApprovalFile(target, taskId, approvalId);
323
+ if (!approval) {
324
+ throw approvalError(E_ACTION_NOT_FOUND, `approval ${approvalId} does not exist for task ${taskId}`);
325
+ }
326
+ return validateApprovalArtifact(approval);
327
+ }
328
+
329
+ export async function listApprovals(target, { packageRoot, taskId }) {
330
+ const approvals = await listApprovalFiles(target, taskId);
331
+ return approvals.map((approval) => validateApprovalArtifact(approval));
332
+ }
333
+
334
+ export async function validateApprovalForAction(target, {
335
+ packageRoot,
336
+ taskId,
337
+ action,
338
+ actionId,
339
+ approvalId,
340
+ requireApproved = true,
341
+ }) {
342
+ const currentAction = action ?? await readAction(target, { packageRoot, taskId, actionId });
343
+ if (currentAction.taskId !== taskId) {
344
+ throw approvalError(E_APPROVAL_STALE, `action ${currentAction.actionId} belongs to a different task`);
345
+ }
346
+ const state = await readWorkState(target, { packageRoot, taskId });
347
+ if (!state) {
348
+ throw approvalError(E_APPROVAL_INVALID, `task ${taskId} has no canonical work state`);
349
+ }
350
+ const approval = await readApproval(target, { packageRoot, taskId, approvalId });
351
+ assertApprovalBinding(approval, {
352
+ taskId,
353
+ actionId: currentAction.actionId,
354
+ actionFingerprint: currentAction.actionFingerprint,
355
+ contractFingerprint: state.contractFingerprint,
356
+ taskRevision: state.revision ?? 0,
357
+ capability: currentAction.capability,
358
+ });
359
+ if (requireApproved && approval.status !== "APPROVED") {
360
+ throw approvalError(E_APPROVAL_INVALID, `approval ${approval.approvalId} is not APPROVED`);
361
+ }
362
+ return approval;
363
+ }
364
+
365
+ /**
366
+ * Validate that a resolved approval still hashes to the fingerprint bound at
367
+ * action authorization. Any post-authorization mutation of the approval
368
+ * artifact fails closed (INV-FINAL-APPROVAL-01).
369
+ */
370
+ export async function validateBoundApprovalFingerprint(target, {
371
+ packageRoot,
372
+ taskId,
373
+ approvalId,
374
+ expectedFingerprint,
375
+ }) {
376
+ if (
377
+ typeof expectedFingerprint !== "string"
378
+ || !/^[a-f0-9]{64}$/.test(expectedFingerprint)
379
+ ) {
380
+ throw approvalError(
381
+ E_APPROVAL_INVALID,
382
+ "expected approval fingerprint must be a lowercase sha256 hex digest",
383
+ );
384
+ }
385
+
386
+ const approval = await readApproval(target, {
387
+ packageRoot,
388
+ taskId,
389
+ approvalId,
390
+ });
391
+
392
+ const actualFingerprint = approvalFingerprint(approval);
393
+
394
+ if (actualFingerprint !== expectedFingerprint) {
395
+ throw approvalError(
396
+ E_APPROVAL_INVALID,
397
+ `approval ${approvalId} no longer matches the fingerprint bound at action authorization`,
398
+ );
399
+ }
400
+
401
+ return {
402
+ approval,
403
+ fingerprint: actualFingerprint,
404
+ };
405
+ }
@@ -235,4 +235,52 @@ export const ARTIFACT_REGISTRY = Object.freeze({
235
235
  isPersisted: true,
236
236
  description: "Durable task recovery state recording claim release and explicit resume requirements.",
237
237
  }),
238
+ actions: Object.freeze({
239
+ key: "actions",
240
+ scope: "TASK",
241
+ path: `${TASK_STATE_ROOT}/<task-key>/actions/action-<id>.json`,
242
+ schema: "action",
243
+ owner: "PROTOCOL_MANAGED",
244
+ mutability: "STATE_MACHINE_TRANSITIONS",
245
+ trustRole: "EXTERNAL_ACTION_PROVENANCE",
246
+ isPublic: true,
247
+ isPersisted: true,
248
+ description: "Durable external action artifacts recording intent, policy, authority, execution, ambiguity, and reconciliation state.",
249
+ }),
250
+ approvals: Object.freeze({
251
+ key: "approvals",
252
+ scope: "TASK",
253
+ path: `${TASK_STATE_ROOT}/<task-key>/approvals/approval-<id>.json`,
254
+ schema: "approval",
255
+ owner: "PROTOCOL_MANAGED",
256
+ mutability: "APPEND_DECISION_ONCE",
257
+ trustRole: "ACTION_APPROVAL_ATTESTATION",
258
+ isPublic: true,
259
+ isPersisted: true,
260
+ description: "Crash-safe approval requests cryptographically bound to action fingerprint, contract fingerprint, task revision, and capability.",
261
+ }),
262
+ capabilityPolicy: Object.freeze({
263
+ key: "capabilityPolicy",
264
+ scope: "PROJECT",
265
+ path: PROJECT_ARTIFACT_PATHS.capabilityPolicy,
266
+ schema: "capability-policy",
267
+ owner: "OPERATOR_OR_AGENT",
268
+ mutability: "MUTABLE_CONFIGURATION",
269
+ trustRole: "CAPABILITY_POLICY_SPECIFICATION",
270
+ isPublic: true,
271
+ isPersisted: true,
272
+ description: "Project-local capability policy mapping canonical capabilities to ALLOW, DENY, REQUIRE_AUTHORITY, or REQUIRE_APPROVAL decisions; this is policy specification, never host authority.",
273
+ }),
274
+ evaluations: Object.freeze({
275
+ key: "evaluations",
276
+ scope: "TASK",
277
+ path: `${TASK_STATE_ROOT}/<task-key>/evaluations/eval-<id>.json`,
278
+ schema: "trajectory-evaluation",
279
+ owner: "PROTOCOL_COMPILED",
280
+ mutability: "IMMUTABLE_ONCE_WRITTEN",
281
+ trustRole: "TRAJECTORY_EVALUATION",
282
+ isPublic: true,
283
+ isPersisted: true,
284
+ description: "Immutable trajectory evaluation results compiled from the canonical trace against a local reference scenario.",
285
+ }),
238
286
  });
package/src/core/audit.js CHANGED
@@ -7,6 +7,7 @@ import { currentChangedPaths } from "./repository.js";
7
7
  import { validateReadyProtocolConsistency } from "./preflight.js";
8
8
  import { taskArtifactPath } from "./task-paths.js";
9
9
  import { findTaskById } from "./task-discovery.js";
10
+ import { validateActionLedgerConsistency } from "./actions.js";
10
11
 
11
12
  function sortErrors(errors) {
12
13
  return [...errors].sort((left, right) => left.code.localeCompare(right.code)
@@ -118,6 +119,30 @@ export async function evaluateAudit({
118
119
  }))
119
120
  : [];
120
121
  const errors = sortErrors([...completion.errors, ...readyConsistencyErrors, ...ownershipErrors]);
122
+ if (taskId) {
123
+ for (const actionError of await validateActionLedgerConsistency(target, { packageRoot, taskId })) {
124
+ errors.push({ ...actionError, artifacts: [taskArtifactPath(taskId, "actions"), taskArtifactPath(taskId, "events")] });
125
+ }
126
+ // Surface specific untrusted/ambiguous required actions with reasons.
127
+ const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
128
+ const readiness = await evaluateRequiredActionReadiness({ target, packageRoot, taskId });
129
+ for (const item of readiness.actions) {
130
+ if (item.status === "SATISFIED" || item.status === "PENDING") continue;
131
+ const code = item.status === "AMBIGUOUS"
132
+ ? "E_ACTION_RECONCILIATION_REQUIRED"
133
+ : item.status === "UNTRUSTED"
134
+ ? "E_ACTION_VERIFICATION_REQUIRED"
135
+ : "E_ACTION_STATE_MISMATCH";
136
+ errors.push({
137
+ code,
138
+ message: `Required action ${item.actionId} is not trusted-satisfied (${item.status}): ${item.reasons[0] ?? ""}`,
139
+ artifacts: [taskArtifactPath(taskId, "actions"), taskArtifactPath(taskId, "events")],
140
+ actionId: item.actionId,
141
+ readiness: item.status,
142
+ reasons: item.reasons,
143
+ });
144
+ }
145
+ }
121
146
  const changedPaths = await compareChangedPaths(target, packageRoot, { taskId, receiptPath });
122
147
  if (changedPaths.status === "MISMATCH") {
123
148
  errors.push({
@@ -10,6 +10,7 @@ import { assertContinuitySemantics } from "./continuity.js";
10
10
  import { taskArtifactPath, taskDirectory } from "./task-paths.js";
11
11
  import { resolveTaskClaimState } from "./task-claim-state.js";
12
12
  import { E_TASK_CLAIM_OWNERSHIP_INCONSISTENT } from "./error-codes.js";
13
+ import { listActions } from "./actions.js";
13
14
 
14
15
  export const BUNDLE_SCHEMA_VERSION = 1;
15
16
  const BUNDLE_ROOT = ".forgeloop/tasks";
@@ -129,6 +130,13 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
129
130
  if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
130
131
  }
131
132
 
133
+ const actionArtifacts = await listActions(target, { packageRoot, taskId });
134
+ for (const action of actionArtifacts) {
135
+ const destination = `${directory}/actions/${action.actionId}.json`;
136
+ await writeJsonArtifact(target, destination, action, "action", packageRoot);
137
+ artifacts.push(`actions/${action.actionId}.json`);
138
+ }
139
+
132
140
  const executionRefs = [...new Set([
133
141
  ...(stateSource.value.checks ?? []),
134
142
  ...(receiptSource?.value?.checks ?? []),
@@ -195,6 +203,7 @@ export async function readTaskBundle(target, taskId, packageRoot) {
195
203
  "continuity.json": ["continuity", "continuity"],
196
204
  "task.json": ["descriptor", "task-descriptor"],
197
205
  "recovery.json": ["recovery", "task-recovery"],
206
+ ...Object.fromEntries(manifest.value.artifacts.filter((artifact) => artifact.startsWith("actions/")).map((artifact) => [artifact, ["action", "action"]])),
198
207
  };
199
208
  const executions = {};
200
209
  for (const artifact of manifest.value.artifacts) {
@@ -203,6 +212,12 @@ export async function readTaskBundle(target, taskId, packageRoot) {
203
212
  executions[execution.value.executionId] = execution.value;
204
213
  continue;
205
214
  }
215
+ if (artifact.startsWith("actions/") && artifact.endsWith(".json")) {
216
+ const action = await readJsonArtifact(target, `${directory}/${artifact}`, "action", packageRoot);
217
+ loaded.actions ??= {};
218
+ loaded.actions[action.value.actionId] = action.value;
219
+ continue;
220
+ }
206
221
  const mapping = mappings[artifact];
207
222
  if (!mapping) continue;
208
223
  const loadedArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, mapping[1], packageRoot);