@cassiomc1/forgeloop 1.0.0 → 1.1.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 (95) hide show
  1. package/.cursor/rules/project-loop.mdc +3 -2
  2. package/.github/copilot-instructions.md +1 -0
  3. package/AGENTS.md +2 -1
  4. package/CLAUDE.md +1 -0
  5. package/DOCS_INDEX.md +36 -0
  6. package/ENG/design-code-eng.md +31 -0
  7. package/ENG/documentation-quality-eng.md +400 -0
  8. package/EXECUTION_STATE.md +23 -13
  9. package/GUIDE_ROUTER.md +23 -8
  10. package/LOOP_ENGINEERING.md +77 -12
  11. package/PROTOCOL_INTEGRATION.md +9 -6
  12. package/README.md +82 -39
  13. package/docs/ARTIFACT_REFERENCE.md +442 -0
  14. package/docs/CLI_REFERENCE.md +892 -0
  15. package/docs/CROSS_HARNESS_CONTINUITY.md +198 -0
  16. package/docs/DOCUMENTATION_GUIDE.md +161 -0
  17. package/docs/GETTING_STARTED.md +348 -0
  18. package/docs/RECIPES.md +250 -0
  19. package/docs/TROUBLESHOOTING.md +345 -0
  20. package/docs/assets/forgeloop-flow.svg +1 -1
  21. package/docs/forgeloop-flow.mmd +1 -1
  22. package/package.json +13 -2
  23. package/schemas/task-descriptor.schema.json +56 -0
  24. package/schemas/work-state.schema.json +18 -1
  25. package/scripts/CI_VALIDATORS.md +7 -0
  26. package/src/cli.js +280 -388
  27. package/src/commands/advance.js +5 -2
  28. package/src/commands/audit.js +11 -1
  29. package/src/commands/clear-continuity.js +5 -2
  30. package/src/commands/clear-state.js +5 -2
  31. package/src/commands/complete.js +9 -1
  32. package/src/commands/continuity.js +5 -2
  33. package/src/commands/inspect.js +10 -2
  34. package/src/commands/next.js +5 -2
  35. package/src/commands/preflight.js +9 -1
  36. package/src/commands/prepare-completion.js +5 -2
  37. package/src/commands/reconcile-continuity.js +5 -2
  38. package/src/commands/record-check.js +7 -1
  39. package/src/commands/record-continuity.js +21 -14
  40. package/src/commands/record-terminal-result.js +7 -1
  41. package/src/commands/route.js +22 -18
  42. package/src/commands/run-check.js +52 -44
  43. package/src/commands/status.js +18 -12
  44. package/src/commands/task-create.js +94 -0
  45. package/src/commands/task-list.js +48 -0
  46. package/src/commands/task-migrate.js +34 -0
  47. package/src/commands/task-scope.js +75 -0
  48. package/src/commands/task-show.js +81 -0
  49. package/src/commands/task-unlock.js +35 -0
  50. package/src/commands/validate-protocol.js +37 -20
  51. package/src/commands/validate-state.js +24 -18
  52. package/src/config/guides.json +42 -0
  53. package/src/core/activation.js +8 -4
  54. package/src/core/artifact-registry.js +166 -0
  55. package/src/core/audit.js +65 -12
  56. package/src/core/bundles.js +76 -50
  57. package/src/core/cli-command-definitions.js +611 -0
  58. package/src/core/cli-metadata.js +23 -0
  59. package/src/core/completion-artifacts.js +161 -74
  60. package/src/core/completion.js +134 -76
  61. package/src/core/continuity.js +20 -13
  62. package/src/core/contract.js +6 -3
  63. package/src/core/error-codes.js +197 -0
  64. package/src/core/events.js +19 -14
  65. package/src/core/execution.js +38 -6
  66. package/src/core/gate-artifact.js +12 -9
  67. package/src/core/gates.js +4 -2
  68. package/src/core/guide-metadata.js +7 -11
  69. package/src/core/guide-registry.js +29 -0
  70. package/src/core/inspect.js +7 -4
  71. package/src/core/native-adapters.js +6 -0
  72. package/src/core/phase.js +85 -33
  73. package/src/core/preflight-consistency.js +24 -14
  74. package/src/core/preflight-loaders.js +16 -11
  75. package/src/core/preflight.js +44 -25
  76. package/src/core/protocol.js +2 -11
  77. package/src/core/receipt.js +1 -1
  78. package/src/core/report.js +2 -2
  79. package/src/core/repository.js +46 -12
  80. package/src/core/resumability.js +6 -4
  81. package/src/core/route-artifact.js +9 -5
  82. package/src/core/router.js +11 -7
  83. package/src/core/schema-validation.js +1 -0
  84. package/src/core/task-command.js +41 -0
  85. package/src/core/task-context.js +126 -0
  86. package/src/core/task-descriptor.js +81 -0
  87. package/src/core/task-discovery.js +116 -0
  88. package/src/core/task-identity.js +76 -0
  89. package/src/core/task-lock.js +209 -0
  90. package/src/core/task-migration-validation.js +140 -0
  91. package/src/core/task-migration.js +361 -0
  92. package/src/core/task-paths.js +96 -0
  93. package/src/core/task-scope.js +179 -0
  94. package/src/core/templates.js +3 -9
  95. package/src/core/work-state.js +24 -13
package/src/core/audit.js CHANGED
@@ -5,20 +5,40 @@ import { PROTOCOL_VERSION } from "./protocol.js";
5
5
  import { readJsonArtifact } from "./artifacts.js";
6
6
  import { currentChangedPaths } from "./repository.js";
7
7
  import { validateReadyProtocolConsistency } from "./preflight.js";
8
+ import { taskArtifactPath } from "./task-paths.js";
9
+ import { readTaskDescriptor } from "./task-descriptor.js";
8
10
 
9
11
  function sortErrors(errors) {
10
12
  return [...errors].sort((left, right) => left.code.localeCompare(right.code)
11
13
  || left.message.localeCompare(right.message));
12
14
  }
13
15
 
14
- async function compareChangedPaths(target, packageRoot) {
16
+ async function compareChangedPaths(target, packageRoot, options = {}) {
17
+ const receiptRel = options.receiptPath ?? (options.taskId ? taskArtifactPath(options.taskId, "receipt") : ARTIFACT_PATHS.receipt);
15
18
  let receipt;
16
19
  try {
17
- receipt = (await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot)).value;
20
+ receipt = (await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot)).value;
18
21
  } catch {
19
22
  return { status: "NOT_VERIFIED", expected: [], observed: [], missing: [], unexpected: [] };
20
23
  }
21
- const observed = await currentChangedPaths(target);
24
+
25
+ let writeClaims = [];
26
+ if (options.taskId) {
27
+ try {
28
+ const desc = await readTaskDescriptor(target, options.taskId, packageRoot);
29
+ writeClaims = desc.value.writeClaims ?? [];
30
+ } catch {
31
+ // ignore
32
+ }
33
+ }
34
+
35
+ let observed;
36
+ if (writeClaims.length > 0) {
37
+ observed = await currentChangedPaths(target, { paths: writeClaims });
38
+ } else {
39
+ observed = await currentChangedPaths(target);
40
+ }
41
+
22
42
  if (observed === null) {
23
43
  return { status: "NOT_VERIFIED", expected: receipt.changedPaths ?? [], observed: [], missing: [], unexpected: [] };
24
44
  }
@@ -34,8 +54,40 @@ async function compareChangedPaths(target, packageRoot) {
34
54
  };
35
55
  }
36
56
 
37
- export async function evaluateAudit({ target, packageRoot, strict = false, authorityContext, runtimeContext } = {}) {
38
- const completion = await evaluateCompletion({ target, packageRoot, strict, authorityContext, runtimeContext });
57
+ export async function evaluateAudit({
58
+ target,
59
+ packageRoot,
60
+ strict = false,
61
+ authorityContext,
62
+ runtimeContext,
63
+ taskId = null,
64
+ contractPath = null,
65
+ routePath = null,
66
+ statePath = null,
67
+ receiptPath = null,
68
+ eventsPath = null,
69
+ preflightPath = null,
70
+ } = {}) {
71
+ const completion = await evaluateCompletion({
72
+ target,
73
+ packageRoot,
74
+ strict,
75
+ authorityContext,
76
+ runtimeContext,
77
+ taskId,
78
+ contractPath,
79
+ routePath,
80
+ statePath,
81
+ receiptPath,
82
+ eventsPath,
83
+ preflightPath,
84
+ });
85
+ const preflightRel = preflightPath ?? (taskId ? taskArtifactPath(taskId, "preflight") : ARTIFACT_PATHS.preflight);
86
+ const receiptRel = receiptPath ?? (taskId ? taskArtifactPath(taskId, "receipt") : ARTIFACT_PATHS.receipt);
87
+ const stateRel = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
88
+ const contractRel = contractPath ?? (taskId ? taskArtifactPath(taskId, "contract") : ARTIFACT_PATHS.contract);
89
+ const routeRel = routePath ?? (taskId ? taskArtifactPath(taskId, "route") : ARTIFACT_PATHS.route);
90
+
39
91
  let manifest = null;
40
92
  let manifestError = null;
41
93
  try {
@@ -45,25 +97,26 @@ export async function evaluateAudit({ target, packageRoot, strict = false, autho
45
97
  }
46
98
  let readyConsistencyErrors = [];
47
99
  try {
48
- const persistedPreflight = await readJsonArtifact(target, ARTIFACT_PATHS.preflight, "preflight", packageRoot);
100
+ const persistedPreflight = await readJsonArtifact(target, preflightRel, "preflight", packageRoot);
49
101
  if (persistedPreflight.value.status === "READY") {
50
102
  readyConsistencyErrors = await validateReadyProtocolConsistency({
51
103
  target,
52
104
  packageRoot,
53
105
  persisted: persistedPreflight.value,
54
106
  current: completion.preflight,
107
+ taskId,
55
108
  });
56
109
  }
57
110
  } catch {
58
111
  // Completion already reports missing or invalid preflight artifacts.
59
112
  }
60
113
  const errors = sortErrors([...completion.errors, ...readyConsistencyErrors]);
61
- const changedPaths = await compareChangedPaths(target, packageRoot);
114
+ const changedPaths = await compareChangedPaths(target, packageRoot, { taskId, receiptPath });
62
115
  if (changedPaths.status === "MISMATCH") {
63
116
  errors.push({
64
117
  code: "E_RECEIPT_PATH_MISMATCH",
65
118
  message: "Receipt changedPaths do not match observed repository paths",
66
- artifacts: [ARTIFACT_PATHS.receipt],
119
+ artifacts: [receiptRel],
67
120
  missing: changedPaths.missing,
68
121
  unexpected: changedPaths.unexpected,
69
122
  next: "Run forgeloop prepare-completion to refresh changed paths, then rerun audit.",
@@ -94,10 +147,10 @@ export async function evaluateAudit({ target, packageRoot, strict = false, autho
94
147
  publicationStatus: completion.publicationStatus,
95
148
  productionReadiness: completion.productionReadiness,
96
149
  artifacts: {
97
- contract: ARTIFACT_PATHS.contract,
98
- route: ARTIFACT_PATHS.route,
99
- state: ARTIFACT_PATHS.state,
100
- receipt: ARTIFACT_PATHS.receipt,
150
+ contract: contractRel,
151
+ route: routeRel,
152
+ state: stateRel,
153
+ receipt: receiptRel,
101
154
  },
102
155
  };
103
156
  }
@@ -1,12 +1,13 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
 
3
3
  import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
4
- import { readContract, validateContract } from "./contract.js";
4
+ import { validateContract } from "./contract.js";
5
5
  import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
6
6
  import { PROTOCOL_VERSION } from "./protocol.js";
7
7
  import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
8
8
  import { readExecutionArtifact } from "./execution.js";
9
9
  import { assertContinuitySemantics } from "./continuity.js";
10
+ import { taskArtifactPath, taskDirectory } from "./task-paths.js";
10
11
 
11
12
  export const BUNDLE_SCHEMA_VERSION = 1;
12
13
  const BUNDLE_ROOT = ".forgeloop/tasks";
@@ -36,88 +37,116 @@ async function copyJson(target, sourcePath, destinationPath, schemaName, package
36
37
  }
37
38
  }
38
39
 
40
+ async function tryReadJson(target, taskPath, legacyPath, schemaName, packageRoot) {
41
+ try {
42
+ return await readJsonArtifact(target, taskPath, schemaName, packageRoot);
43
+ } catch (error) {
44
+ if (error.code === "ARTIFACT_MISSING" && legacyPath) {
45
+ return await readJsonArtifact(target, legacyPath, schemaName, packageRoot);
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+
39
51
  export async function exportTaskBundle(target, taskId, packageRoot) {
40
52
  safeTaskId(taskId);
41
53
  const directory = bundleDirectory(taskId);
42
54
  const artifacts = [];
43
- const stateSource = await readJsonArtifact(target, ARTIFACT_PATHS.state, "work-state", packageRoot);
55
+
56
+ const stateSource = await tryReadJson(target, taskArtifactPath(taskId, "state"), ARTIFACT_PATHS.state, "work-state", packageRoot);
44
57
  let receiptSource = null;
45
58
  try {
46
- receiptSource = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
59
+ receiptSource = await tryReadJson(target, taskArtifactPath(taskId, "receipt"), ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
47
60
  } catch (error) {
48
61
  if (error.code !== "ARTIFACT_MISSING") throw error;
49
62
  }
50
- const provenanceErrors = [
51
- ...(await validateChecksExecutionProvenance(stateSource.value.checks, {
52
- target,
53
- packageRoot,
54
- taskId,
55
- artifactPath: ARTIFACT_PATHS.state,
56
- })),
57
- ...(await validateChecksExecutionProvenance(receiptSource?.value?.checks, {
63
+
64
+ await validateChecksExecutionProvenance(stateSource.value.checks, {
65
+ target,
66
+ packageRoot,
67
+ taskId,
68
+ artifactPath: ARTIFACT_PATHS.state,
69
+ });
70
+ if (receiptSource?.value?.checks) {
71
+ await validateChecksExecutionProvenance(receiptSource.value.checks, {
58
72
  target,
59
73
  packageRoot,
60
74
  taskId,
61
75
  artifactPath: ARTIFACT_PATHS.receipt,
62
- })),
63
- ];
64
- if (provenanceErrors.length > 0) {
65
- const first = provenanceErrors[0];
66
- const error = new Error(first.message);
67
- error.code = first.code;
68
- error.artifacts = first.artifacts;
69
- throw error;
76
+ });
70
77
  }
78
+
71
79
  const required = [
72
- [ARTIFACT_PATHS.contract, "contract.json", "current-contract"],
73
- [ARTIFACT_PATHS.route, "route.json", "routing-result"],
74
- [ARTIFACT_PATHS.state, "state.json", "work-state"],
80
+ [taskArtifactPath(taskId, "contract"), ARTIFACT_PATHS.contract, "contract.json", "current-contract"],
81
+ [taskArtifactPath(taskId, "route"), ARTIFACT_PATHS.route, "route.json", "routing-result"],
82
+ [taskArtifactPath(taskId, "state"), ARTIFACT_PATHS.state, "state.json", "work-state"],
75
83
  ];
76
- for (const [sourcePath, destinationName, schemaName] of required) {
77
- const source = schemaName === "current-contract"
78
- ? await readContract(target, packageRoot)
79
- : await readJsonArtifact(target, sourcePath, schemaName, packageRoot);
84
+ for (const [taskRel, legacyRel, destinationName, schemaName] of required) {
85
+ const source = await tryReadJson(target, taskRel, legacyRel, schemaName, packageRoot);
86
+ if (schemaName === "current-contract") {
87
+ await validateContract(source.value, packageRoot);
88
+ }
80
89
  if (source.value.taskId !== undefined && source.value.taskId !== taskId) {
81
- const error = new Error(`${sourcePath} belongs to ${source.value.taskId}, not ${taskId}`);
90
+ const error = new Error(`${taskRel} belongs to ${source.value.taskId}, not ${taskId}`);
82
91
  error.code = "E_BUNDLE_TASK_MISMATCH";
83
92
  throw error;
84
93
  }
85
94
  await writeJsonArtifact(target, `${directory}/${destinationName}`, source.value, schemaName, packageRoot);
86
95
  artifacts.push(destinationName);
87
96
  }
97
+
88
98
  const optional = [
89
- [ARTIFACT_PATHS.preflight, "preflight.json", "preflight"],
90
- [ARTIFACT_PATHS.receipt, "receipt.json", "execution-receipt"],
91
- [ARTIFACT_PATHS.sources, "sources.json", "source-registry"],
92
- [ARTIFACT_PATHS.config, "config.json", "config"],
93
- [ARTIFACT_PATHS.continuity, "continuity.json", "continuity"],
99
+ [taskArtifactPath(taskId, "preflight"), ARTIFACT_PATHS.preflight, "preflight.json", "preflight"],
100
+ [taskArtifactPath(taskId, "receipt"), ARTIFACT_PATHS.receipt, "receipt.json", "execution-receipt"],
101
+ [taskArtifactPath(taskId, "descriptor"), null, "task.json", "task-descriptor"],
102
+ [ARTIFACT_PATHS.sources, null, "sources.json", "source-registry"],
103
+ [ARTIFACT_PATHS.config, null, "config.json", "config"],
104
+ [taskArtifactPath(taskId, "continuity"), ARTIFACT_PATHS.continuity, "continuity.json", "continuity"],
94
105
  ];
95
- for (const [sourcePath, destinationName, schemaName] of optional) {
96
- const copied = await copyJson(target, sourcePath, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
106
+ for (const [taskRel, legacyRel, destinationName, schemaName] of optional) {
107
+ let copied = null;
108
+ try {
109
+ copied = await copyJson(target, taskRel, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
110
+ } catch {
111
+ if (legacyRel) {
112
+ copied = await copyJson(target, legacyRel, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
113
+ }
114
+ }
97
115
  if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
98
116
  }
117
+
99
118
  const executionRefs = [...new Set([
100
119
  ...(stateSource.value.checks ?? []),
101
120
  ...(receiptSource?.value?.checks ?? []),
102
121
  ].map((check) => check?.executionRef).filter(Boolean))].sort();
103
122
  for (const executionRef of executionRefs) {
104
- const execution = await readExecutionArtifact({ target, executionRef, packageRoot });
123
+ const execution = await readExecutionArtifact({ target, executionRef, packageRoot, taskId });
105
124
  const destination = `${directory}/executions/${execution.value.executionId}.json`;
106
125
  await writeJsonArtifact(target, destination, execution.value, "execution", packageRoot);
107
126
  artifacts.push(`executions/${execution.value.executionId}.json`);
108
127
  }
109
- const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
128
+
129
+ // Events
130
+ let eventsPath = ensureWithin(target, taskArtifactPath(taskId, "events"));
131
+ if (!(await fileExists(eventsPath))) {
132
+ eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
133
+ }
110
134
  if (await fileExists(eventsPath)) {
111
135
  await assertSafePath(target, `${directory}/events.ndjson`);
112
136
  await writeFileAtomic(ensureWithin(target, `${directory}/events.ndjson`), await readBytes(eventsPath));
113
137
  artifacts.push("events.ndjson");
114
138
  }
115
- const gateDirectory = ensureWithin(target, ARTIFACT_PATHS.gates);
139
+
140
+ // Gates
141
+ let gateDirectory = ensureWithin(target, `${taskDirectory(taskId)}/gates`);
142
+ if (!(await fileExists(gateDirectory))) {
143
+ gateDirectory = ensureWithin(target, ARTIFACT_PATHS.gates);
144
+ }
116
145
  if (await fileExists(gateDirectory)) {
117
146
  const entries = await readdir(gateDirectory, { withFileTypes: true });
118
147
  for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name))) {
119
148
  const gateName = entry.name.slice(0, -5);
120
- const sourcePath = `${ARTIFACT_PATHS.gates}/${entry.name}`;
149
+ const sourcePath = `${gateDirectory.replace(target + "/", "")}/${entry.name}`;
121
150
  const destinationPath = `${directory}/gates/${entry.name}`;
122
151
  const gate = await readJsonArtifact(target, sourcePath, "gate", packageRoot);
123
152
  if (gate.value.taskId !== taskId) continue;
@@ -125,6 +154,7 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
125
154
  artifacts.push(`gates/${gateName}.json`);
126
155
  }
127
156
  }
157
+
128
158
  artifacts.sort();
129
159
  const manifest = {
130
160
  schemaVersion: BUNDLE_SCHEMA_VERSION,
@@ -149,6 +179,7 @@ export async function readTaskBundle(target, taskId, packageRoot) {
149
179
  "sources.json": ["sources", "source-registry"],
150
180
  "config.json": ["config", "config"],
151
181
  "continuity.json": ["continuity", "continuity"],
182
+ "task.json": ["descriptor", "task-descriptor"],
152
183
  };
153
184
  const executions = {};
154
185
  for (const artifact of manifest.value.artifacts) {
@@ -169,30 +200,25 @@ export async function readTaskBundle(target, taskId, packageRoot) {
169
200
  loaded[mapping[0]] = loadedArtifact.value;
170
201
  }
171
202
  if (Object.keys(executions).length > 0) loaded.executions = executions;
172
- const provenanceErrors = [
173
- ...(await validateChecksExecutionProvenance(loaded.state?.checks, {
203
+ if (loaded.state?.checks) {
204
+ await validateChecksExecutionProvenance(loaded.state.checks, {
174
205
  target,
175
206
  packageRoot,
176
207
  taskId,
177
208
  executionArtifacts: executions,
178
209
  allowForeignCwd: true,
179
210
  artifactPath: "state.json",
180
- })),
181
- ...(await validateChecksExecutionProvenance(loaded.receipt?.checks, {
211
+ });
212
+ }
213
+ if (loaded.receipt?.checks) {
214
+ await validateChecksExecutionProvenance(loaded.receipt.checks, {
182
215
  target,
183
216
  packageRoot,
184
217
  taskId,
185
218
  executionArtifacts: executions,
186
219
  allowForeignCwd: true,
187
220
  artifactPath: "receipt.json",
188
- })),
189
- ];
190
- if (provenanceErrors.length > 0) {
191
- const first = provenanceErrors[0];
192
- const error = new Error(first.message);
193
- error.code = first.code;
194
- error.artifacts = first.artifacts;
195
- throw error;
221
+ });
196
222
  }
197
223
  return { manifest: manifest.value, artifacts: loaded };
198
224
  }