@cassiomc1/forgeloop 2.0.0-rc.2 → 2.0.0-rc.4

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/README.md CHANGED
@@ -11,9 +11,9 @@
11
11
  [![Package smoke](https://github.com/cassiomc1/forgeloop/actions/workflows/package-smoke.yml/badge.svg?branch=main)](https://github.com/cassiomc1/forgeloop/actions/workflows/package-smoke.yml)
12
12
  [![Release notes](https://github.com/cassiomc1/forgeloop/actions/workflows/release-notes.yml/badge.svg?branch=main)](https://github.com/cassiomc1/forgeloop/actions/workflows/release-notes.yml)
13
13
 
14
- ForgeLoop is the deterministic governor for AI-assisted engineering. Jev provides bounded System One semantic input; the host coding model provides System Two implementation. ForgeLoop owns lifecycle, claims, gates, evidence, completion, recovery, and publication truth.
14
+ ForgeLoop governs AI-assisted engineering. Jev supplies bounded semantic input; the host model implements changes. ForgeLoop owns lifecycle, claims, gates, evidence, completion, recovery, and publication truth.
15
15
 
16
- Operational sources: [`DOCS_INDEX.md`](./DOCS_INDEX.md); [`LOOP_ENGINEERING.md`](./LOOP_ENGINEERING.md) is canonical; [`PROTOCOL_INTEGRATION.md`](./PROTOCOL_INTEGRATION.md) defines discovery; [`PROJECT_PROFILE.md`](./PROJECT_PROFILE.md) stores project facts; [`GUIDE_ROUTER.md`](./GUIDE_ROUTER.md) selects guides.
16
+ Sources: [`DOCS_INDEX.md`](./DOCS_INDEX.md); canonical [`LOOP_ENGINEERING.md`](./LOOP_ENGINEERING.md); discovery [`PROTOCOL_INTEGRATION.md`](./PROTOCOL_INTEGRATION.md); project facts [`PROJECT_PROFILE.md`](./PROJECT_PROFILE.md); guides [`GUIDE_ROUTER.md`](./GUIDE_ROUTER.md).
17
17
 
18
18
  ## Where should I start?
19
19
 
@@ -33,7 +33,6 @@ Operational sources: [`DOCS_INDEX.md`](./DOCS_INDEX.md); [`LOOP_ENGINEERING.md`]
33
33
  - **Troubleshooting & error codes** → [`docs/TROUBLESHOOTING.md`](./docs/TROUBLESHOOTING.md)
34
34
  - **Code attestation & revision coverage** → [`docs/CODE_ATTESTATION.md`](./docs/CODE_ATTESTATION.md)
35
35
  - **System architecture & safety** → [`LOOP_SYSTEM_DESIGN.md`](./LOOP_SYSTEM_DESIGN.md) & [`THREAT_MODEL.md`](./THREAT_MODEL.md)
36
- - **Documentation index & ownership** → [`DOCS_INDEX.md`](./DOCS_INDEX.md)
37
36
 
38
37
  ## Real execution proof
39
38
 
@@ -78,7 +77,9 @@ not establish project identity. Public project evidence remains v1.
78
77
  ## Distribution and license
79
78
 
80
79
  GitHub hosts source and issues. The core npm package is publicly available;
81
- the MCP companion retains restricted distribution.
80
+ the MCP companion retains restricted distribution. The owner-selected default
81
+ `latest` channel targets `2.0.0-rc.2`, which remains a release candidate with
82
+ incomplete stable SQLite acceptance.
82
83
  ForgeLoop-owned source and documentation are licensed under the
83
84
  [PolyForm Noncommercial License 1.0.0](./LICENSE). Commercial use is not granted
84
85
  by this license. Third-party notices are documented in
@@ -7,7 +7,7 @@
7
7
  ForgeLoop is a portable protocol and support CLI for verifiable engineering workflows. It records and validates task state, contracts, routing, checks, evidence, continuity, and optional code attestations. It does not become an agent scheduler, delegation service, source-control authority, or secret manager.
8
8
 
9
9
  Protocol version: 1
10
- Package version: 2.0.0-rc.2
10
+ Package version: 2.0.0-rc.4
11
11
 
12
12
  ## Canonical loop
13
13
 
@@ -4,11 +4,30 @@ This is the current release checklist for the PolyForm Noncommercial-licensed
4
4
  `@cassiomc1/forgeloop` package. It is a preparation and verification checklist;
5
5
  it does not authorize publication.
6
6
 
7
+ ## Runtime corrections after RC2
8
+
9
+ RC3 and later candidates with runtime changes require fresh full repository
10
+ verification and the clean-user audit on Windows, macOS and Linux. The bounded
11
+ source-equivalence shortcut below applies only to RC1 and RC2. Stable SQLite
12
+ acceptance remains separate; a passing new-user audit does not change
13
+ `soleSQLiteWriter` or authorize a stable 2.x release. Candidates publish to
14
+ `next` under the existing release-channel policy.
15
+
16
+ The `Published npm new-user SQLite audit` workflow supports `candidate` and
17
+ `npm` package sources. Before a PR, test the packed candidate in an isolated
18
+ installation, home directory and project on each platform. After publication,
19
+ dispatch the same workflow with source `npm` and the exact new version. Require
20
+ every check to pass, including live Jev health, concurrent task creation,
21
+ SQLite integrity and a completed-task backup restored to a fresh directory.
22
+ Supply `FORGELOOP_NPM_AUDIT_JEV_KEY` through GitHub Actions secrets and remove
23
+ temporary credentials after the runs finish.
24
+
7
25
  ## Bounded 2.0.0-rc.2 publication
8
26
 
9
27
  The owner approved short verification and an RC fallback on 2026-10-13. This
10
- candidate is published only to npm `next`; stable `latest` remains blocked while
11
- `soleSQLiteWriter` is false. A release candidate does not assert completed
28
+ candidate is published to npm `next`. The owner subsequently authorized moving
29
+ `latest` to the existing RC2 through a guarded tag-only promotion. Publishing a
30
+ new stable version remains blocked while `soleSQLiteWriter` is false. A release candidate does not assert completed
12
31
  migration acceptance or production readiness.
13
32
 
14
33
  After npm rejected restricted RC1 publication with `E402`, the owner authorized
@@ -50,6 +69,16 @@ confirmation `PUBLISH PUBLIC PACKAGE`, then verify npm version, Git commit,
50
69
  tarball integrity, and provenance. Stable publishing retains full checks and
51
70
  additionally refuses incomplete sole-writer acceptance.
52
71
 
72
+ ## Owner-authorized RC2 default channel
73
+
74
+ Dispatch `npm-publish.yml` on `main` with operation `promote-latest` and
75
+ confirmation `POINT LATEST TO RC2`. This moves only `latest` to the already
76
+ published public `2.0.0-rc.2`; it neither republishes nor modifies the RC2 tag.
77
+ The promotion checks the exact release commit and downloaded SHA-512, refuses
78
+ unexpected existing tags, and requires npm trusted-publisher `manageDistTags`
79
+ authority. No credentials are logged or stored. The version remains an RC;
80
+ stable SQLite acceptance and production readiness are still incomplete.
81
+
53
82
  ## ForgeLoop 2.0.0 candidate scope
54
83
 
55
84
  The 2.0.0 candidate is not release-ready while SQLite migration plan §10 has
@@ -1,5 +1,38 @@
1
1
  # ForgeLoop Troubleshooting Guide
2
2
 
3
+ ## Windows Repository Index ZIP extraction
4
+
5
+ When `init` reports `E_REPOSITORY_INDEX_ENGINE_EXTRACTION_FAILED` and
6
+ `tar: This does not look like a tar archive`, a GNU tar from Git may be
7
+ shadowing Windows' ZIP-capable system tool. RC3 selects
8
+ `%SystemRoot%\System32\tar.exe` explicitly for inspection and extraction.
9
+ Archive and binary checksum verification and unsafe-entry rejection still
10
+ apply. A missing system tool remains an explicit extraction failure; ForgeLoop
11
+ does not install replacement software automatically.
12
+
13
+ ## Restoring completed tasks into another directory
14
+
15
+ Use an attachment-inclusive `storage-backup` and `storage-restore` with
16
+ externally stopped writers. Historical execution records keep their original
17
+ project root and exact bytes. RC3 accepts them only when they exactly match
18
+ the snapshot bound to the destination's restore journal and storage marker.
19
+ Keep `.forgeloop/storage-restores/<operation-id>/snapshot` and its journal;
20
+ they are retained provenance evidence, not a disposable search cache.
21
+ An altered record, missing snapshot or broken binding remains rejected.
22
+
23
+ Storage restore does not install project code or instruction adapters. Restore
24
+ the project files separately, then run `forgeloop init` in the destination to
25
+ install the kit before expecting the complete `doctor` report to be healthy.
26
+
27
+ ## Concurrent task creation
28
+
29
+ Task creation revalidates write claims atomically. If the catalog changes while
30
+ a command is prepared, RC3 refreshes authoritative claim inputs with bounded
31
+ backoff and jitter without replaying the command or its external effects.
32
+ Overlapping claims still fail. Sustained contention can exhaust the retry
33
+ bound and return a catalog-change refusal; inspect fresh task discovery
34
+ before deciding whether to retry.
35
+
3
36
  This guide provides symptom-first recovery procedures for common ForgeLoop protocol, state, and verification issues.
4
37
 
5
38
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cassiomc1/forgeloop",
3
- "version": "2.0.0-rc.2",
3
+ "version": "2.0.0-rc.4",
4
4
  "description": "Evidence-first engineering protocol for AI coding environments and developer workflows",
5
5
  "repository": {
6
6
  "type": "git",
@@ -124,6 +124,19 @@ function commandProvenanceError(message = "Observed command evidence requires a
124
124
  return artifactError("E_COMMAND_PROVENANCE_UNATTESTED", message, [ARTIFACT_PATHS.receipt]);
125
125
  }
126
126
 
127
+ async function validateExecutionProjectRoot({ target, taskId, check, execution, protocolProjectRoot, allowForeignCwd, executionArtifacts }) {
128
+ if (allowForeignCwd || path.resolve(protocolProjectRoot) === path.resolve(target)) return;
129
+ const { isRetainedRestoreExecution } = await import("../storage/restore-execution-provenance.js");
130
+ const restored = !executionArtifacts && await isRetainedRestoreExecution(target, execution, { taskId });
131
+ if (!restored) {
132
+ throw artifactError(
133
+ "E_EXECUTION_REF_INVALID",
134
+ "Execution artifact protocol project root does not match the current target",
135
+ [taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
136
+ );
137
+ }
138
+ }
139
+
127
140
  /**
128
141
  * Revalidate the ForgeLoop-owned execution artifact behind an observed command
129
142
  * check. This is intentionally asynchronous so completion and audit can verify
@@ -194,13 +207,7 @@ export async function validateCheckExecutionProvenance(check, {
194
207
  [taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
195
208
  );
196
209
  }
197
- if (!allowForeignCwd && path.resolve(protocolProjectRoot) !== path.resolve(target)) {
198
- throw artifactError(
199
- "E_EXECUTION_REF_INVALID",
200
- "Execution artifact protocol project root does not match the current target",
201
- [taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
202
- );
203
- }
210
+ await validateExecutionProjectRoot({ target, taskId, check, execution, protocolProjectRoot, allowForeignCwd, executionArtifacts });
204
211
  if (check.status === "passed" && (execution.status !== "passed" || execution.exitCode !== 0)) {
205
212
  throw artifactError(
206
213
  "E_EXECUTION_REF_INVALID",
@@ -181,8 +181,14 @@ export function validateArchiveEntry(entry) {
181
181
  return normalized;
182
182
  }
183
183
 
184
+ export function resolveArchiveTool(archive, { platform = process.platform, env = process.env } = {}) {
185
+ // Windows ships a ZIP-capable bsdtar. Git's GNU tar can shadow it on PATH.
186
+ if (platform === "win32") return path.win32.join(env.SystemRoot || "C:\\Windows", "System32", "tar.exe");
187
+ return archive === "zip" ? "unzip" : "tar";
188
+ }
189
+
184
190
  async function listArchiveEntries(archivePath, { archive = "tar.gz", execFileImpl = nodeExecFile } = {}) {
185
- const listCommand = archive === "zip" && process.platform !== "win32" ? "unzip" : "tar";
191
+ const listCommand = resolveArchiveTool(archive);
186
192
  const archiveName = path.basename(archivePath);
187
193
  const listArgs = listCommand === "unzip" ? ["-Z1", archiveName] : ["-tf", archiveName];
188
194
  try {
@@ -195,7 +201,7 @@ async function listArchiveEntries(archivePath, { archive = "tar.gz", execFileImp
195
201
 
196
202
  async function extractArchive(archivePath, extractionDirectory, { archive = "tar.gz", execFileImpl = nodeExecFile } = {}) {
197
203
  await listArchiveEntries(archivePath, { archive, execFileImpl });
198
- const extractCommand = archive === "zip" && process.platform !== "win32" ? "unzip" : "tar";
204
+ const extractCommand = resolveArchiveTool(archive);
199
205
  const destination = path.relative(path.dirname(archivePath), extractionDirectory).replaceAll("\\", "/") || ".";
200
206
  const extractArgs = extractCommand === "unzip"
201
207
  ? ["-q", path.basename(archivePath), "-d", destination]
@@ -26,7 +26,7 @@ function artifactSchema(row) {
26
26
  }
27
27
 
28
28
  /** Validate imported payloads with their existing protocol schemas and rules. */
29
- export async function validateMigrationDatabase(db, { target, packageRoot }) {
29
+ export async function validateMigrationDatabase(db, { target, packageRoot, archivedExecutions = false }) {
30
30
  const schemas = new Map();
31
31
  const validate = async (text, name, label, taskId = null) => {
32
32
  if (!name) throw Object.assign(new Error(`Unmapped operational artifact: ${label}`), { code: "E_STORAGE_MIGRATION_UNMAPPED_SOURCE" });
@@ -55,13 +55,13 @@ export async function validateMigrationDatabase(db, { target, packageRoot }) {
55
55
  validator?.(value);
56
56
  }
57
57
  }
58
- return withOperationalStore({ db, target }, async () => {
58
+ return withOperationalStore({ db, target, readOnly: true }, async () => {
59
59
  const tasks = [];
60
60
  for (const row of db.prepare("SELECT task_id FROM tasks ORDER BY task_id").iterate()) {
61
61
  const state = await readWorkState(target, { taskId: row.task_id, packageRoot });
62
62
  for (const check of state?.checks ?? []) {
63
63
  if (check.provenance === "FORGELOOP_EXECUTED") {
64
- await validateCheckExecutionProvenance(check, { target, packageRoot, taskId: row.task_id });
64
+ await validateCheckExecutionProvenance(check, { target, packageRoot, taskId: row.task_id, allowForeignCwd: archivedExecutions });
65
65
  }
66
66
  }
67
67
  const ledger = await validateEventLedger(target, packageRoot, { taskId: row.task_id });
@@ -77,7 +77,7 @@ export async function resumeProjectReplacementPreparation(target, { operationId,
77
77
  if (intent.phase === "PREPARING") {
78
78
  await ensurePreparedRestoreSnapshot(target, current.root, intent, { journalFilename: "replacement-intent.json" });
79
79
  await withVerifiedProjectStorageBackup(await assertSafePath(current.root, "snapshot"), async retained => {
80
- await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot });
80
+ await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot, archivedExecutions: true });
81
81
  await withVerifiedProjectStorageBackup(intent.source, source => {
82
82
  if (canonicalFingerprint(source.manifest) !== intent.sourceFingerprint
83
83
  || canonicalFingerprint(logicalSnapshot(source.db)) !== canonicalFingerprint(logicalSnapshot(retained.db))) throw invalid("Replacement recovery snapshot differs from recorded source");
@@ -99,7 +99,7 @@ export async function resumeProjectReplacementPreparation(target, { operationId,
99
99
  export async function replaceActiveProjectStorage(source, target, { writersQuiesced = false, packageRoot = getPackageRoot() } = {}) {
100
100
  if (writersQuiesced !== true) throw Object.assign(new Error("Exclude writers and close their database connections before active replacement"), { code: "E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED" });
101
101
  return withVerifiedProjectStorageBackup(source, async ({ db, root, manifest }) => {
102
- await validateMigrationDatabase(db, { target: root, packageRoot });
102
+ await validateMigrationDatabase(db, { target: root, packageRoot, archivedExecutions: true });
103
103
  const relative = path.relative(path.resolve(target), root).split(path.sep).join("/");
104
104
  if (REPLACEMENT_ACTIVE_ROOTS.some(name => relative === name || relative.startsWith(`${name}/`))) throw invalid("Replacement source cannot be archived outgoing operational state");
105
105
  return withStorageMaintenance(target, async () => {
@@ -119,7 +119,7 @@ export async function replaceActiveProjectStorage(source, target, { writersQuies
119
119
  const snapshot = await assertSafePath(operationRoot, "snapshot");
120
120
  await backupProjectStorage(db, root, snapshot);
121
121
  await withVerifiedProjectStorageBackup(snapshot, async retained => {
122
- await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot });
122
+ await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot, archivedExecutions: true });
123
123
  await withVerifiedProjectStorageBackup(root, current => {
124
124
  if (canonicalFingerprint(current.manifest) !== intent.sourceFingerprint
125
125
  || canonicalFingerprint(logicalSnapshot(current.db)) !== canonicalFingerprint(logicalSnapshot(retained.db))) throw invalid("Incoming source changed during replacement preparation");
@@ -92,7 +92,7 @@ export async function restoreProjectStorageToFreshProject(source, target, { writ
92
92
  if (canonicalFingerprint(current.manifest) !== canonicalFingerprint(sourceManifest)
93
93
  || canonicalFingerprint(logicalSnapshot(current.db)) !== canonicalFingerprint(logicalSnapshot(snapshot))) throw invalid("Source backup changed during restore preparation");
94
94
  });
95
- await validateMigrationDatabase(snapshot, { target: retained, packageRoot });
95
+ await validateMigrationDatabase(snapshot, { target: retained, packageRoot, archivedExecutions: true });
96
96
  const binding = { operationId, databaseSchemaVersion: readStorageMeta(snapshot).schema_version,
97
97
  sourceInventoryFingerprint: canonicalFingerprint({ database: manifest.databaseSha256, inventory: manifest.inventorySha256, references: manifest.references }) };
98
98
  Object.assign(journal, { phase: "READY", binding, snapshotFingerprint: canonicalFingerprint(manifest) });
@@ -111,7 +111,7 @@ async function publishReadyRestore(target, operationRoot, journal, { packageRoot
111
111
  const persist = () => writeFileAtomic(journalPath, `${JSON.stringify(journal, null, 2)}\n`);
112
112
  const { operationId, binding } = journal;
113
113
  return withVerifiedProjectStorageBackup(retained, async ({ db: snapshot, manifest }) => {
114
- await validateMigrationDatabase(snapshot, { target: retained, packageRoot });
114
+ await validateMigrationDatabase(snapshot, { target: retained, packageRoot, archivedExecutions: true });
115
115
  await assertOwnedStorageMaintenance(target);
116
116
  await verifyProjectRestoreJournal(target, operationId, { expectedOwnerId: await assertOwnedStorageMaintenance(target) });
117
117
  await assertRestorePublicationLayout(target, snapshot);
@@ -186,7 +186,7 @@ async function validatePreparedRestore(target, root, journal, packageRoot) {
186
186
  await withVerifiedProjectStorageBackup(journal.source, async source => {
187
187
  if (canonicalFingerprint(source.manifest) !== journal.sourceFingerprint
188
188
  || canonicalFingerprint(logicalSnapshot(source.db)) !== canonicalFingerprint(logicalSnapshot(snapshot))) throw invalid("Prepared restore differs from its recorded source backup");
189
- await validateMigrationDatabase(snapshot, { target: snapshotPath, packageRoot });
189
+ await validateMigrationDatabase(snapshot, { target: snapshotPath, packageRoot, archivedExecutions: true });
190
190
  });
191
191
  await assertOwnedStorageMaintenance(target);
192
192
  Object.assign(journal, { phase: "READY", snapshotFingerprint: canonicalFingerprint(manifest), binding: {
@@ -0,0 +1,26 @@
1
+ import { canonicalFingerprint } from "../core/artifacts.js";
2
+ import { readStorageVersionMarker } from "./storage-marker.js";
3
+ import { verifyProjectRestoreJournal } from "./restore-journal.js";
4
+ import { withVerifiedProjectStorageBackup } from "./project-backup.js";
5
+ import { operationalContext } from "./operational-context.js";
6
+
7
+ /** Historical roots remain valid only for exact records in the bound snapshot. */
8
+ export async function isRetainedRestoreExecution(target, execution, { taskId } = {}) {
9
+ // Replacement journals also audit incoming archives. Those read-only audits
10
+ // must not select their snapshot as the caller's live operational project.
11
+ return operationalContext.run(null, () => verifyRetainedExecution(target, execution, taskId));
12
+ }
13
+
14
+ async function verifyRetainedExecution(target, execution, taskId) {
15
+ const marker = await readStorageVersionMarker(target);
16
+ if (!marker?.operationId) return false;
17
+ let retained;
18
+ try { retained = await verifyProjectRestoreJournal(target, marker.operationId); }
19
+ catch (error) { if (error.code === "ENOENT") return false; throw error; }
20
+ if (!retained.snapshotVerified || !["PUBLISHING", "ACTIVE"].includes(retained.journal.phase)) return false;
21
+ return withVerifiedProjectStorageBackup(retained.snapshot, ({ db }) => {
22
+ const row = db.prepare("SELECT payload_json FROM executions WHERE task_id = ? AND execution_id = ?")
23
+ .get(taskId ?? execution.taskId, execution.executionId);
24
+ return Boolean(row && canonicalFingerprint(JSON.parse(row.payload_json)) === canonicalFingerprint(execution));
25
+ });
26
+ }
@@ -22,7 +22,7 @@ const belongs = (name, root) => name === root || name.startsWith(`${root}/`);
22
22
  async function incomingSnapshot(target, operationId, packageRoot) {
23
23
  const root = await assertSafePath(target, `.forgeloop/storage-restores/${operationId}/snapshot`);
24
24
  return withVerifiedProjectStorageBackup(root, async ({ db, manifest }) => {
25
- await validateMigrationDatabase(db, { target: root, packageRoot });
25
+ await validateMigrationDatabase(db, { target: root, packageRoot, archivedExecutions: true });
26
26
  return { root, fingerprint: canonicalFingerprint(manifest) };
27
27
  });
28
28
  }
@@ -80,7 +80,7 @@ export async function prepareActiveStorageReplacement(target, operationId, { wri
80
80
  const backup = await assertSafePath(root, "backup");
81
81
  await backupProjectStorage(db, target, backup);
82
82
  await withVerifiedProjectStorageBackup(backup, async retained => {
83
- await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot });
83
+ await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot, archivedExecutions: true });
84
84
  const logicalFingerprint = canonicalFingerprint(logicalSnapshot(retained.db));
85
85
  if (logicalFingerprint !== canonicalFingerprint(logicalSnapshot(db))) throw invalid("Outgoing state changed while its retained backup was prepared");
86
86
  Object.assign(manifest, { backupFingerprint: canonicalFingerprint(retained.manifest), logicalFingerprint });
@@ -1,7 +1,8 @@
1
1
  import { publishAttachmentFile, verifyAttachmentFile } from "./attachment-files.js";
2
2
  import { insertVerifiedAttachmentReference, validateAttachmentReference } from "./attachment-references.js";
3
3
  import path from "node:path";
4
- import { randomUUID, createHash } from "node:crypto";
4
+ import { randomUUID, createHash, randomInt } from "node:crypto";
5
+ import { setTimeout as delay } from "node:timers/promises";
5
6
  import { operationalContext, getOperationalStore } from "./operational-context.js";
6
7
  import { canonicalFingerprint } from "../core/artifacts.js";
7
8
  import { taskStorageKey } from "../core/task-identity.js";
@@ -547,7 +548,10 @@ async function commitPreparedStore(store, packageRoot) {
547
548
  for (let refresh = 0; ; refresh += 1) {
548
549
  try { store.commit(); return; }
549
550
  catch (error) {
550
- if (error.code !== "E_STORAGE_CATALOG_CHANGED" || refresh >= 3) throw error;
551
+ if (error.code !== "E_STORAGE_CATALOG_CHANGED" || refresh >= 16) throw error;
552
+ // Spread simultaneous creators before refreshing their authoritative
553
+ // claim inputs. The prepared command and external effects are not replayed.
554
+ await delay(Math.min(5 * (2 ** refresh), 100) + randomInt(0, 20));
551
555
  store.reads.delete("task-catalog");
552
556
  const { discoverTasks } = await import("../core/task-discovery.js");
553
557
  const { assertNoScopeConflicts } = await import("../core/task-scope.js");