@cassiomc1/forgeloop 1.2.3 → 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.
- package/AGENT_COMPATIBILITY.md +4 -0
- package/DOCS_INDEX.md +7 -0
- package/EXECUTION_STATE.md +8 -0
- package/LOOP_ENGINEERING.md +1 -1
- package/LOOP_SYSTEM_DESIGN.md +10 -0
- package/PROTOCOL_INTEGRATION.md +30 -0
- package/README.md +48 -0
- package/THREAT_MODEL.md +3 -1
- package/docs/ARTIFACT_REFERENCE.md +11 -0
- package/docs/CLI_REFERENCE.md +81 -3
- package/docs/CROSS_HARNESS_CONTINUITY.md +11 -0
- package/docs/DOCUMENTATION_GUIDE.md +17 -0
- package/docs/GETTING_STARTED.md +9 -0
- package/docs/RECIPES.md +5 -1
- package/docs/TROUBLESHOOTING.md +133 -40
- package/package.json +4 -1
- package/schemas/execution.schema.json +11 -1
- package/schemas/work-state.schema.json +1 -0
- package/src/cli.js +21 -2
- package/src/commands/doctor.js +22 -0
- package/src/commands/migrate-protocol.js +18 -0
- package/src/commands/protocol-info.js +16 -0
- package/src/commands/run-check.js +2 -0
- package/src/commands/task-create.js +3 -2
- package/src/commands/task-lock-status.js +29 -0
- package/src/commands/task-show.js +3 -3
- package/src/commands/task-unlock.js +8 -6
- package/src/core/artifacts.js +17 -4
- package/src/core/cli-command-definitions.js +44 -0
- package/src/core/completion-artifacts.js +15 -3
- package/src/core/completion.js +15 -3
- package/src/core/diagnosis.js +15 -11
- package/src/core/error-codes.js +18 -0
- package/src/core/events.js +109 -8
- package/src/core/execution.js +73 -9
- package/src/core/filesystem.js +20 -2
- package/src/core/phase.js +8 -2
- package/src/core/protocol-info.js +41 -0
- package/src/core/protocol-migration.js +59 -0
- package/src/core/reconcile-closure.js +22 -4
- package/src/core/resumability.js +8 -6
- package/src/core/task-command.js +3 -3
- package/src/core/task-lock.js +40 -4
- package/src/core/task-migration.js +24 -1
- package/src/core/task-paths.js +3 -1
- package/src/core/transaction.js +259 -0
- package/src/core/work-state.js +70 -6
package/src/core/work-state.js
CHANGED
|
@@ -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
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
280
|
-
|
|
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);
|