@cassiomc1/forgeloop 1.3.0 → 1.5.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 (76) 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 +7 -0
  5. package/EXECUTION_STATE.md +40 -0
  6. package/LOOP_ENGINEERING.md +54 -5
  7. package/LOOP_SYSTEM_DESIGN.md +22 -1
  8. package/PROTOCOL_INTEGRATION.md +41 -0
  9. package/README.md +38 -0
  10. package/TERMINOLOGY.md +15 -0
  11. package/THIRD_PARTY_NOTICES.md +15 -0
  12. package/THREAT_MODEL.md +20 -1
  13. package/docs/ARTIFACT_REFERENCE.md +43 -0
  14. package/docs/CLI_REFERENCE.md +97 -3
  15. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  16. package/docs/DOCUMENTATION_GUIDE.md +14 -0
  17. package/docs/GETTING_STARTED.md +1 -0
  18. package/docs/MCP.md +126 -0
  19. package/docs/RECIPES.md +82 -0
  20. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  21. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  22. package/docs/TROUBLESHOOTING.md +111 -1
  23. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  24. package/package.json +14 -3
  25. package/schemas/task-recovery.schema.json +61 -0
  26. package/src/cli.js +173 -347
  27. package/src/commands/audit.js +5 -0
  28. package/src/commands/inspect.js +6 -0
  29. package/src/commands/progress.js +6 -2
  30. package/src/commands/status.js +17 -0
  31. package/src/commands/task-create.js +39 -1
  32. package/src/commands/task-list.js +14 -1
  33. package/src/commands/task-lock-status.js +2 -2
  34. package/src/commands/task-recover.js +202 -0
  35. package/src/commands/task-repair-legacy-recovery.js +417 -0
  36. package/src/commands/task-resume.js +172 -0
  37. package/src/commands/task-scope.js +23 -4
  38. package/src/commands/task-show.js +18 -4
  39. package/src/commands/validate-protocol.js +19 -2
  40. package/src/core/artifact-registry.js +12 -0
  41. package/src/core/audit.js +20 -4
  42. package/src/core/bundles.js +15 -0
  43. package/src/core/cli-command-definitions.js +50 -4
  44. package/src/core/command-executors.js +387 -0
  45. package/src/core/command-input.js +107 -0
  46. package/src/core/command-runtime.js +106 -0
  47. package/src/core/completion-artifacts.js +2 -3
  48. package/src/core/completion-ownership.js +88 -0
  49. package/src/core/error-codes.js +118 -1
  50. package/src/core/events.js +130 -1
  51. package/src/core/filesystem.js +55 -6
  52. package/src/core/inspect.js +27 -0
  53. package/src/core/integration-invocation-policy.js +170 -0
  54. package/src/core/integration-limits.js +20 -0
  55. package/src/core/integration-resources.js +127 -0
  56. package/src/core/next-action-model.js +60 -0
  57. package/src/core/next-action.js +31 -0
  58. package/src/core/phase.js +2 -1
  59. package/src/core/project-root.js +21 -0
  60. package/src/core/protocol-info.js +13 -0
  61. package/src/core/reconcile-closure.js +32 -10
  62. package/src/core/recovery-history.js +116 -0
  63. package/src/core/schema-validation.js +1 -0
  64. package/src/core/task-claim-state.js +272 -0
  65. package/src/core/task-command.js +5 -1
  66. package/src/core/task-conflict-inspection.js +321 -0
  67. package/src/core/task-context.js +32 -29
  68. package/src/core/task-discovery.js +14 -1
  69. package/src/core/task-lock.js +216 -22
  70. package/src/core/task-paths.js +3 -2
  71. package/src/core/task-recovery-migration.js +192 -0
  72. package/src/core/task-recovery.js +205 -0
  73. package/src/core/task-scope.js +33 -1
  74. package/src/core/templates.js +1 -0
  75. package/src/core/transaction.js +28 -2
  76. package/src/integration.js +47 -0
@@ -1,4 +1,4 @@
1
- import { resolveTaskContext } from "../core/task-context.js";
1
+ import { resolveTaskContext, TASK_SELECTION_MODES } from "../core/task-context.js";
2
2
  import { readTaskDescriptor } from "../core/task-descriptor.js";
3
3
  import { classifyLockStaleness, readLockInfo } from "../core/task-lock.js";
4
4
  import { taskDirectory, taskArtifactPath } from "../core/task-paths.js";
@@ -6,6 +6,7 @@ import { readWorkState } from "../core/work-state.js";
6
6
  import { readContract } from "../core/contract.js";
7
7
  import { fileExists, ensureWithin } from "../core/filesystem.js";
8
8
  import { E_TASK_NOT_FOUND } from "../core/error-codes.js";
9
+ import { resolveTaskClaimState } from "../core/task-claim-state.js";
9
10
 
10
11
  function taskError(code, message, artifacts = []) {
11
12
  const error = new Error(message);
@@ -15,7 +16,7 @@ function taskError(code, message, artifacts = []) {
15
16
  }
16
17
 
17
18
  export async function runTaskShow({ target, packageRoot, taskId } = {}) {
18
- const context = await resolveTaskContext(target, { taskId, packageRoot, explicitRequired: true });
19
+ const context = await resolveTaskContext(target, { taskId, packageRoot, explicitRequired: true, selectionMode: TASK_SELECTION_MODES.READ });
19
20
  const effectiveTaskId = context.taskId;
20
21
 
21
22
  let descriptor = null;
@@ -30,6 +31,13 @@ export async function runTaskShow({ target, packageRoot, taskId } = {}) {
30
31
  }
31
32
 
32
33
  const state = await readWorkState(target, { packageRoot, taskId: effectiveTaskId });
34
+ const claimProjection = await resolveTaskClaimState(target, {
35
+ taskId: effectiveTaskId,
36
+ packageRoot,
37
+ descriptor,
38
+ state,
39
+ });
40
+ const recovery = claimProjection.recovery;
33
41
  let contract = null;
34
42
  try {
35
43
  const contractArtifact = await readContract(target, packageRoot, { taskId: effectiveTaskId });
@@ -40,7 +48,7 @@ export async function runTaskShow({ target, packageRoot, taskId } = {}) {
40
48
 
41
49
  const lockInfo = await readLockInfo(target, effectiveTaskId);
42
50
  const artifacts = {};
43
- for (const name of ["contract", "route", "state", "preflight", "receipt", "continuity", "events"]) {
51
+ for (const name of ["contract", "route", "state", "preflight", "receipt", "continuity", "events", "recovery"]) {
44
52
  const rel = taskArtifactPath(effectiveTaskId, name);
45
53
  artifacts[name] = {
46
54
  path: rel,
@@ -53,7 +61,8 @@ export async function runTaskShow({ target, packageRoot, taskId } = {}) {
53
61
  taskKey: context.taskKey,
54
62
  directory: taskDirectory(effectiveTaskId),
55
63
  phase: state?.phase ?? "UNINITIALIZED",
56
- writeClaims: descriptor.writeClaims ?? [],
64
+ ...claimProjection,
65
+ recovery,
57
66
  lock: lockInfo ? { ...lockInfo, classification: classifyLockStaleness(lockInfo) } : null,
58
67
  contract: contract ? { title: contract.title ?? null, taskType: contract.taskType ?? null } : null,
59
68
  artifacts,
@@ -73,6 +82,11 @@ export function formatTaskShowResult(result) {
73
82
  `Directory: ${result.directory}`,
74
83
  `Phase: ${result.phase}`,
75
84
  `Write Claims: ${claims}`,
85
+ `Claim State: ${result.claimState}`,
86
+ `Mutation Allowed: ${result.mutationAllowed ? "yes" : "no"}`,
87
+ ...(result.recovery
88
+ ? [`Recovery: ${result.recovery.recoveryId} at ${result.recovery.recoveredAt}`]
89
+ : []),
76
90
  `Lock: ${lockStatus}`,
77
91
  `Created: ${result.createdAt}`,
78
92
  `Updated: ${result.updatedAt}`,
@@ -12,6 +12,7 @@ import { validateEventLedger, validateStateLedgerCoherence } from "../core/event
12
12
  import { validateChecksExecutionProvenance } from "../core/completion-artifacts.js";
13
13
  import { assertContinuitySemantics } from "../core/continuity.js";
14
14
  import { currentChangedPaths, currentRepositoryFingerprint } from "../core/repository.js";
15
+ import { resolveTaskClaimState } from "../core/task-claim-state.js";
15
16
 
16
17
  async function readArtifact(target, relativePath, label) {
17
18
  if (!relativePath) return null;
@@ -164,6 +165,22 @@ export async function runValidateProtocol({
164
165
  })),
165
166
  ];
166
167
  }
168
+ const recoveryErrors = [];
169
+ if (effectiveTaskId) {
170
+ try {
171
+ const claimProjection = await resolveTaskClaimState(target, { taskId: effectiveTaskId, packageRoot });
172
+ recoveryErrors.push(...(claimProjection.ownershipErrors ?? []).map((error) => ({
173
+ ...error,
174
+ artifacts: [taskArtifactPath(effectiveTaskId, "recovery"), effectiveEventsFile],
175
+ })));
176
+ } catch (error) {
177
+ recoveryErrors.push({
178
+ code: "E_TASK_RECOVERY_INCONSISTENT",
179
+ message: `Recovery state is unreadable: ${error.message}`,
180
+ artifacts: [taskArtifactPath(effectiveTaskId, "recovery")],
181
+ });
182
+ }
183
+ }
167
184
  const continuityContext = continuity && state && !stateValidationError
168
185
  ? {
169
186
  contractFingerprint: state.contractFingerprint,
@@ -182,11 +199,11 @@ export async function runValidateProtocol({
182
199
  delegatedResults,
183
200
  events: ledgerEvents,
184
201
  });
185
- if (readErrors.length > 0 || schemaErrors.length > 0 || readyConsistencyErrors.length > 0 || ledgerErrors.length > 0) {
202
+ if (readErrors.length > 0 || schemaErrors.length > 0 || readyConsistencyErrors.length > 0 || ledgerErrors.length > 0 || recoveryErrors.length > 0) {
186
203
  return {
187
204
  ...result,
188
205
  status: "INVALID",
189
- errors: [...result.errors, ...readErrors, ...schemaErrors, ...readyConsistencyErrors, ...ledgerErrors]
206
+ errors: [...result.errors, ...readErrors, ...schemaErrors, ...readyConsistencyErrors, ...ledgerErrors, ...recoveryErrors]
190
207
  .sort((left, right) => left.code.localeCompare(right.code) || left.message.localeCompare(right.message)),
191
208
  };
192
209
  }
@@ -223,4 +223,16 @@ export const ARTIFACT_REGISTRY = Object.freeze({
223
223
  isPersisted: true,
224
224
  description: "Task-scoped policy snapshot binding task activation to effective policy digest.",
225
225
  }),
226
+ recovery: Object.freeze({
227
+ key: "recovery",
228
+ scope: "TASK",
229
+ path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.recovery}`,
230
+ schema: "task-recovery",
231
+ owner: "PROTOCOL_GENERATED",
232
+ mutability: "RECOVERY_STATE_TRANSITIONS",
233
+ trustRole: "TASK_RECOVERY_STATE",
234
+ isPublic: true,
235
+ isPersisted: true,
236
+ description: "Durable task recovery state recording claim release and explicit resume requirements.",
237
+ }),
226
238
  });
package/src/core/audit.js CHANGED
@@ -6,7 +6,7 @@ import { readJsonArtifact } from "./artifacts.js";
6
6
  import { currentChangedPaths } from "./repository.js";
7
7
  import { validateReadyProtocolConsistency } from "./preflight.js";
8
8
  import { taskArtifactPath } from "./task-paths.js";
9
- import { readTaskDescriptor } from "./task-descriptor.js";
9
+ import { findTaskById } from "./task-discovery.js";
10
10
 
11
11
  function sortErrors(errors) {
12
12
  return [...errors].sort((left, right) => left.code.localeCompare(right.code)
@@ -25,8 +25,8 @@ async function compareChangedPaths(target, packageRoot, options = {}) {
25
25
  let writeClaims = [];
26
26
  if (options.taskId) {
27
27
  try {
28
- const desc = await readTaskDescriptor(target, options.taskId, packageRoot);
29
- writeClaims = desc.value.writeClaims ?? [];
28
+ const task = await findTaskById(target, options.taskId, packageRoot);
29
+ writeClaims = task?.writeClaims ?? [];
30
30
  } catch {
31
31
  // ignore
32
32
  }
@@ -110,7 +110,14 @@ export async function evaluateAudit({
110
110
  } catch {
111
111
  // Completion already reports missing or invalid preflight artifacts.
112
112
  }
113
- const errors = sortErrors([...completion.errors, ...readyConsistencyErrors]);
113
+ const taskInfo = taskId ? await findTaskById(target, taskId, packageRoot) : null;
114
+ const ownershipErrors = taskInfo?.ownershipValid === false
115
+ ? (taskInfo.ownershipErrors ?? taskInfo.errors ?? []).map((error) => ({
116
+ ...error,
117
+ artifacts: error.artifacts ?? [taskArtifactPath(taskId, "recovery"), taskArtifactPath(taskId, "events")],
118
+ }))
119
+ : [];
120
+ const errors = sortErrors([...completion.errors, ...readyConsistencyErrors, ...ownershipErrors]);
114
121
  const changedPaths = await compareChangedPaths(target, packageRoot, { taskId, receiptPath });
115
122
  if (changedPaths.status === "MISMATCH") {
116
123
  errors.push({
@@ -167,6 +174,15 @@ export async function evaluateAudit({
167
174
  },
168
175
  policy: policyStatus,
169
176
  completion,
177
+ recovery: taskInfo?.recovery ?? null,
178
+ claims: taskInfo ? {
179
+ state: taskInfo.claimState,
180
+ historical: taskInfo.historicalWriteClaims,
181
+ effective: taskInfo.effectiveWriteClaims,
182
+ mutationAllowed: taskInfo.mutationAllowed,
183
+ ownershipValid: taskInfo.ownershipValid,
184
+ ownershipErrors: taskInfo.ownershipErrors ?? taskInfo.errors ?? [],
185
+ } : null,
170
186
  changedPaths,
171
187
  publicationStatus: completion.publicationStatus,
172
188
  productionReadiness: completion.productionReadiness,
@@ -8,6 +8,8 @@ import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
8
8
  import { readExecutionArtifact } from "./execution.js";
9
9
  import { assertContinuitySemantics } from "./continuity.js";
10
10
  import { taskArtifactPath, taskDirectory } from "./task-paths.js";
11
+ import { resolveTaskClaimState } from "./task-claim-state.js";
12
+ import { E_TASK_CLAIM_OWNERSHIP_INCONSISTENT } from "./error-codes.js";
11
13
 
12
14
  export const BUNDLE_SCHEMA_VERSION = 1;
13
15
  const BUNDLE_ROOT = ".forgeloop/tasks";
@@ -53,6 +55,17 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
53
55
  const directory = bundleDirectory(taskId);
54
56
  const artifacts = [];
55
57
 
58
+ if (await fileExists(ensureWithin(target, taskArtifactPath(taskId, "descriptor")))) {
59
+ const claimProjection = await resolveTaskClaimState(target, { taskId, packageRoot });
60
+ if (!claimProjection.valid) {
61
+ const error = new Error(`Task ${taskId} claim ownership is inconsistent and cannot be exported safely`);
62
+ error.code = E_TASK_CLAIM_OWNERSHIP_INCONSISTENT;
63
+ error.reasonCodes = claimProjection.reasonCodes;
64
+ error.errors = claimProjection.ownershipErrors;
65
+ throw error;
66
+ }
67
+ }
68
+
56
69
  const stateSource = await tryReadJson(target, taskArtifactPath(taskId, "state"), ARTIFACT_PATHS.state, "work-state", packageRoot);
57
70
  let receiptSource = null;
58
71
  try {
@@ -102,6 +115,7 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
102
115
  [ARTIFACT_PATHS.sources, null, "sources.json", "source-registry"],
103
116
  [ARTIFACT_PATHS.config, null, "config.json", "config"],
104
117
  [taskArtifactPath(taskId, "continuity"), ARTIFACT_PATHS.continuity, "continuity.json", "continuity"],
118
+ [taskArtifactPath(taskId, "recovery"), null, "recovery.json", "task-recovery"],
105
119
  ];
106
120
  for (const [taskRel, legacyRel, destinationName, schemaName] of optional) {
107
121
  let copied = null;
@@ -180,6 +194,7 @@ export async function readTaskBundle(target, taskId, packageRoot) {
180
194
  "config.json": ["config", "config"],
181
195
  "continuity.json": ["continuity", "continuity"],
182
196
  "task.json": ["descriptor", "task-descriptor"],
197
+ "recovery.json": ["recovery", "task-recovery"],
183
198
  };
184
199
  const executions = {};
185
200
  for (const artifact of manifest.value.artifacts) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Canonical, declarative definition of all 42 ForgeLoop CLI commands.
2
+ * Canonical, declarative definition of all ForgeLoop CLI commands.
3
3
  * This is the machine source of truth for CLI option parsing, help text,
4
4
  * metadata, documentation generation, and conformance validation.
5
5
  *
@@ -43,7 +43,7 @@ export const CLI_COMMON_OPTIONS = Object.freeze({
43
43
  */
44
44
  export const CLI_TASK_OPTION = Object.freeze({
45
45
  "--task": Object.freeze({
46
- targetKey: "task",
46
+ targetKey: "taskId",
47
47
  parseType: "string",
48
48
  takesValue: true,
49
49
  valueName: "id",
@@ -133,7 +133,7 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
133
133
  options: Object.freeze({
134
134
  ...CLI_COMMON_OPTIONS,
135
135
  ...CLI_TASK_OPTION,
136
- "--work": Object.freeze({ targetKey: "work", parseType: "string", takesValue: true, valueName: "type", missingValueMessage: "--work requires a type", description: "declared work type" }),
136
+ "--work": Object.freeze({ targetKey: "workType", parseType: "string", takesValue: true, valueName: "type", missingValueMessage: "--work requires a type", description: "declared work type" }),
137
137
  "--surface": Object.freeze({ targetKey: "surfaces", parseType: "string", takesValue: true, valueName: "value", repeatable: true, missingValueMessage: "--surface requires a value", description: "affected surface" }),
138
138
  "--risk": Object.freeze({ targetKey: "risks", parseType: "string", takesValue: true, valueName: "value", repeatable: true, missingValueMessage: "--risk requires a value", description: "task risk" }),
139
139
  "--platform": Object.freeze({ targetKey: "platforms", parseType: "string", takesValue: true, valueName: "value", repeatable: true, missingValueMessage: "--platform requires a value", description: "affected platform" }),
@@ -767,10 +767,56 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
767
767
  "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
768
768
  }),
769
769
  writes: [],
770
- removes: [".forgeloop/task-state/<taskKey>/.lock"],
770
+ removes: [".forgeloop/locks/<taskKey>.lock"],
771
771
  mayExecuteExternalProcess: false,
772
772
  description: "Removes an orphaned task lock file to recover an interrupted task.",
773
773
  }),
774
+ "task-recover": Object.freeze({
775
+ name: "task-recover",
776
+ category: "project-maintenance",
777
+ mutation: "MUTATING",
778
+ options: Object.freeze({
779
+ ...CLI_COMMON_OPTIONS,
780
+ ...CLI_TASK_OPTION,
781
+ "--acknowledge-recovery": Object.freeze({ targetKey: "acknowledgeRecovery", parseType: "boolean", takesValue: false, description: "acknowledge release of claims for a STALE or ABANDONED task (required; not host attestation)" }),
782
+ "--operator-authorized": Object.freeze({ targetKey: "operatorAuthorized", parseType: "boolean", takesValue: false, description: "deprecated alias for --acknowledge-recovery; does not attest operator authority" }),
783
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
784
+ }),
785
+ writes: [".forgeloop/task-state/<taskKey>/recovery.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
786
+ removes: [],
787
+ mayExecuteExternalProcess: false,
788
+ description: "Caller-acknowledged recovery of a STALE or ABANDONED task; records durable state and releases effective write claims.",
789
+ }),
790
+ "task-repair-legacy-recovery": Object.freeze({
791
+ name: "task-repair-legacy-recovery",
792
+ category: "project-maintenance",
793
+ mutation: "MUTATING",
794
+ options: Object.freeze({
795
+ ...CLI_COMMON_OPTIONS,
796
+ ...CLI_TASK_OPTION,
797
+ "--acknowledge-recovery": Object.freeze({ targetKey: "acknowledgeRecovery", parseType: "boolean", takesValue: false, description: "fresh explicit acknowledgement of the legacy boundary migration (required)" }),
798
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
799
+ }),
800
+ writes: [".forgeloop/task-state/<taskKey>/recovery.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
801
+ removes: [],
802
+ mayExecuteExternalProcess: false,
803
+ description: "Migrates one recognized legacy OPERATOR_RECOVERY_RECORDED boundary event into the modern durable recovery representation (append-only; original event unchanged).",
804
+ }),
805
+ "task-resume": Object.freeze({
806
+ name: "task-resume",
807
+ category: "project-maintenance",
808
+ mutation: "MUTATING",
809
+ options: Object.freeze({
810
+ ...CLI_COMMON_OPTIONS,
811
+ ...CLI_TASK_OPTION,
812
+ "--claim": Object.freeze({ targetKey: "claims", parseType: "string", takesValue: true, repeatable: true, valueName: "path", missingValueMessage: "--claim requires a path", description: "write claim to reacquire (defaults to all released claims)" }),
813
+ "--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structured output as JSON" }),
814
+ }),
815
+ writes: [".forgeloop/task-state/<taskKey>/task.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
816
+ removes: [".forgeloop/task-state/<taskKey>/recovery.json"],
817
+ mayExecuteExternalProcess: false,
818
+ description: "Reacquires a recovered task's write claims under project serialization and restores ordinary mutation authority.",
819
+ }),
774
820
  });
775
821
 
776
822
  /**