@cassiomc1/forgeloop 1.2.4 → 1.3.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 (47) hide show
  1. package/AGENT_COMPATIBILITY.md +4 -0
  2. package/DOCS_INDEX.md +7 -0
  3. package/EXECUTION_STATE.md +8 -0
  4. package/LOOP_ENGINEERING.md +1 -1
  5. package/LOOP_SYSTEM_DESIGN.md +10 -0
  6. package/PROTOCOL_INTEGRATION.md +30 -0
  7. package/README.md +48 -0
  8. package/THREAT_MODEL.md +3 -1
  9. package/docs/ARTIFACT_REFERENCE.md +11 -0
  10. package/docs/CLI_REFERENCE.md +81 -3
  11. package/docs/CROSS_HARNESS_CONTINUITY.md +11 -0
  12. package/docs/DOCUMENTATION_GUIDE.md +17 -0
  13. package/docs/GETTING_STARTED.md +9 -0
  14. package/docs/RECIPES.md +5 -1
  15. package/docs/TROUBLESHOOTING.md +133 -40
  16. package/package.json +4 -1
  17. package/schemas/execution.schema.json +11 -1
  18. package/schemas/work-state.schema.json +1 -0
  19. package/src/cli.js +21 -2
  20. package/src/commands/doctor.js +22 -0
  21. package/src/commands/migrate-protocol.js +18 -0
  22. package/src/commands/protocol-info.js +16 -0
  23. package/src/commands/run-check.js +2 -0
  24. package/src/commands/task-create.js +3 -2
  25. package/src/commands/task-lock-status.js +29 -0
  26. package/src/commands/task-show.js +3 -3
  27. package/src/commands/task-unlock.js +8 -6
  28. package/src/core/artifacts.js +17 -4
  29. package/src/core/cli-command-definitions.js +44 -0
  30. package/src/core/completion-artifacts.js +15 -3
  31. package/src/core/completion.js +15 -3
  32. package/src/core/diagnosis.js +15 -11
  33. package/src/core/error-codes.js +18 -0
  34. package/src/core/events.js +109 -8
  35. package/src/core/execution.js +73 -9
  36. package/src/core/filesystem.js +20 -2
  37. package/src/core/phase.js +8 -2
  38. package/src/core/protocol-info.js +41 -0
  39. package/src/core/protocol-migration.js +59 -0
  40. package/src/core/reconcile-closure.js +22 -4
  41. package/src/core/resumability.js +8 -6
  42. package/src/core/task-command.js +3 -3
  43. package/src/core/task-lock.js +40 -4
  44. package/src/core/task-migration.js +24 -1
  45. package/src/core/task-paths.js +3 -1
  46. package/src/core/transaction.js +259 -0
  47. package/src/core/work-state.js +70 -6
@@ -19,6 +19,7 @@ import { assertJsonBytes } from "./json-safety.js";
19
19
  import { assertCoverageList } from "./coverage.js";
20
20
  import { canonicalFingerprint } from "./artifacts.js";
21
21
  import { taskArtifactPath } from "./task-paths.js";
22
+ import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
22
23
 
23
24
  export const WORK_STATE_PATH = ".forgeloop/work-state.json";
24
25
 
@@ -163,6 +164,9 @@ export function assertWorkStateSemantics(state) {
163
164
  if (state.verificationCycle !== undefined && (!Number.isInteger(state.verificationCycle) || state.verificationCycle < 1)) {
164
165
  throw new WorkStateError("verificationCycle must be a positive integer");
165
166
  }
167
+ if (state.revision !== undefined && (!Number.isInteger(state.revision) || state.revision < 0)) {
168
+ throw new WorkStateError("revision must be a non-negative integer");
169
+ }
166
170
  if (state.lastCompletionAttempt !== undefined) {
167
171
  if (!state.lastCompletionAttempt || typeof state.lastCompletionAttempt !== "object" || Array.isArray(state.lastCompletionAttempt)) {
168
172
  throw new WorkStateError("lastCompletionAttempt must be an object");
@@ -216,6 +220,7 @@ export function createWorkState(input) {
216
220
  blockers: [...(input.blockers ?? [])],
217
221
  verificationEvidence: [...(input.verificationEvidence ?? [])],
218
222
  lastUpdated: input.lastUpdated ?? new Date().toISOString(),
223
+ revision: input.revision ?? 0,
219
224
  };
220
225
  if (input.verificationCycle !== undefined) state.verificationCycle = input.verificationCycle;
221
226
  if (input.lastCompletionAttempt !== undefined) state.lastCompletionAttempt = structuredClone(input.lastCompletionAttempt);
@@ -238,12 +243,20 @@ export async function readWorkState(target, options = {}) {
238
243
 
239
244
  await assertSafePath(target, relPath);
240
245
  const statePath = ensureWithin(target, relPath);
241
- if (!(await fileExists(statePath))) return null;
242
246
  let state;
243
247
  try {
244
- const bytes = await readBytes(statePath);
245
- assertJsonBytes(bytes, relPath);
246
- state = JSON.parse(bytes.toString("utf8"));
248
+ const transaction = getActiveTaskTransaction();
249
+ const staged = transaction ? await transaction.readText(relPath) : null;
250
+ if (staged === null) {
251
+ if (!(await fileExists(statePath))) return null;
252
+ const bytes = await readBytes(statePath);
253
+ assertJsonBytes(bytes, relPath);
254
+ state = JSON.parse(bytes.toString("utf8"));
255
+ } else {
256
+ const bytes = Buffer.from(staged, "utf8");
257
+ assertJsonBytes(bytes, relPath);
258
+ state = JSON.parse(staged);
259
+ }
247
260
  } catch (error) {
248
261
  throw new WorkStateError(`Unable to parse ${relPath}: ${error.message}`);
249
262
  }
@@ -276,11 +289,62 @@ export async function writeWorkState(target, state, options = {}) {
276
289
 
277
290
  await validateStoredState(state, packageRoot);
278
291
  await assertSafePath(target, relPath);
279
- const statePath = ensureWithin(target, relPath);
280
- await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}\n`, { dryRun });
292
+ const serialized = `${JSON.stringify(state, null, 2)}\n`;
293
+ const transaction = getActiveTaskTransaction();
294
+ if (!dryRun && transaction) {
295
+ await transaction.stageText(relPath, serialized);
296
+ } else {
297
+ const statePath = ensureWithin(target, relPath);
298
+ await writeFileAtomic(statePath, serialized, { dryRun });
299
+ }
281
300
  return state;
282
301
  }
283
302
 
303
+ export async function mutateWorkState(target, { expectedRevision, packageRoot = getPackageRoot(), taskId, statePath } = {}, updater) {
304
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 0) {
305
+ const error = new WorkStateError("expectedRevision must be a non-negative integer");
306
+ error.code = "E_STATE_REVISION_CONFLICT";
307
+ throw error;
308
+ }
309
+ if (!getActiveTaskTransaction()) {
310
+ return withTaskTransaction({
311
+ target,
312
+ taskId: taskId ?? "legacy-work-state",
313
+ lockTaskId: taskId ?? "legacy-work-state",
314
+ operation: "mutate-work-state",
315
+ }, async () => mutateWorkState(target, { expectedRevision, packageRoot, taskId, statePath }, updater));
316
+ }
317
+ const current = await readWorkState(target, { packageRoot, taskId, statePath });
318
+ if (!current || (current.revision ?? 0) !== expectedRevision) {
319
+ const error = new WorkStateError("Work state revision does not match expected revision");
320
+ error.code = "E_STATE_REVISION_CONFLICT";
321
+ throw error;
322
+ }
323
+ const updated = await updater(structuredClone(current));
324
+ const next = createWorkState({
325
+ ...updated,
326
+ revision: expectedRevision + 1,
327
+ lastUpdated: updated.lastUpdated ?? new Date().toISOString(),
328
+ });
329
+ await writeWorkState(target, next, { packageRoot, taskId, statePath });
330
+ return next;
331
+ }
332
+
333
+ export async function initializeWorkState(target, state, { packageRoot = getPackageRoot(), taskId, statePath } = {}) {
334
+ if (getActiveTaskTransaction()) {
335
+ const current = await readWorkState(target, { packageRoot, taskId, statePath });
336
+ if (current) return current;
337
+ await writeWorkState(target, state, { packageRoot, taskId, statePath });
338
+ return state;
339
+ }
340
+ return withTaskTransaction({
341
+ target,
342
+ taskId: taskId ?? "legacy-work-state",
343
+ lockTaskId: taskId ?? "legacy-work-state",
344
+ operation: "initialize-work-state",
345
+ }, async () => initializeWorkState(target, state, { packageRoot, taskId, statePath }));
346
+ }
347
+
284
348
  export async function clearWorkState(target, options = {}) {
285
349
  const relPath = options?.statePath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "state") : WORK_STATE_PATH);
286
350
  await assertSafePath(target, relPath);