{"record":{"id":"d595720428fa1225","repo":"coleam00/Archon","slug":"workflow-compose-fan-out-shared-checkout-collision","errorCode":"workflow.compose_fan_out_shared_checkout_collision","errorMessage":"composed fan-out node '${node.id}': up to ${String(plannedConcurrency)} instances of '${node.include}' would run at once in this run's checkout, and that block does not declare `mutates_checkout: false`. Concurrent runs on one checkout take a path-exclusive lock, so all but the first would cancel themselves — and a lock-cancelled instance is not recoverable by resume (#2180). Choose one: add `mutates_checkout: false` to '${node.include}' if it only reads the repo; or set `fan_out.max_parallel: 1` on '${node.id}' to run the instances one at a time.","messagePattern":"composed fan-out node '\\$\\{node\\.id\\}': up to \\$\\{String\\(plannedConcurrency\\)\\} instances of '\\$\\{node\\.include\\}' would run at once in this run's checkout, and that block does not declare `mutates_checkout: false`\\. Concurrent runs on one checkout take a path-exclusive lock, so all but the first would cancel themselves — and a lock-cancelled instance is not recoverable by resume \\(#2180\\)\\. Choose one: add `mutates_checkout: false` to '\\$\\{node\\.include\\}' if it only reads the repo; or set `fan_out\\.max_parallel: 1` on '\\$\\{node\\.id\\}' to run the instances one at a time\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/workflows/src/dag-executor.ts","lineNumber":9060,"sourceCode":"      'items' in currentItems\n        ? buildInstanceSnapshots(currentItems.items, {}, fanOut.as)\n        : [...persistedSnapshots];\n  }\n  const snapshots = persistedSnapshots ?? computedSnapshots;\n\n  // Concurrent instances share the parent checkout. Use the authoritative persisted\n  // width on resume so a changed producer cannot bypass the original safety preflight.\n  const plannedConcurrency = Math.min(fanOut.max_parallel, snapshots.length);\n  if (plannedConcurrency > 1 && resolved.definition.mutates_checkout !== false) {\n    const msg =\n      `composed fan-out node '${node.id}': up to ${String(plannedConcurrency)} instances of ` +\n      `'${node.include}' would run at once in this run's checkout, and that block does not ` +\n      'declare `mutates_checkout: false`. Concurrent runs on one checkout take a ' +\n      'path-exclusive lock, so all but the first would cancel themselves — and a ' +\n      'lock-cancelled instance is not recoverable by resume (#2180). Choose one: add ' +\n      `\\`mutates_checkout: false\\` to '${node.include}' if it only reads the repo; or set ` +\n      `\\`fan_out.max_parallel: 1\\` on '${node.id}' to run the instances one at a time.`;\n    getLog().warn(\n      { parentRunId: parentRun.id, nodeId: node.id, include: node.include, plannedConcurrency },\n      'workflow.compose_fan_out_shared_checkout_collision'\n    );\n    await notify(`❌ **Composed fan-out blocked** (node \\`${node.id}\\`): ${msg}`);\n    return failResult(msg);\n  }\n\n  if (persistedSnapshots === undefined) {\n    try {\n      await deps.store.persistWorkflowEvent({\n        workflow_run_id: parentRun.id,\n        event_type: 'fan_out_instances',\n        step_name: fanOutScopeName,\n        data: { instances: computedSnapshots },\n      });\n    } catch (err) {\n      const msg =\n        `composed fan-out node '${node.id}' could not persist its item snapshot before ` +","sourceCodeStart":9042,"sourceCodeEnd":9078,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/dag-executor.ts#L9042-L9078","documentation":"Refused at planning time: a composed fan-out would run multiple instances of an `include:` block that can mutate the run's shared checkout and does not declare `mutates_checkout: false`. Concurrent instances take a path-exclusive lock, so all but the first would self-cancel, and lock-cancelled instances are not recoverable by resume (#2180).","triggerScenarios":"Composed fan-out node where plannedConcurrency > 1, no `isolation: worktree` alternative applies, and the included block lacks `mutates_checkout: false`.","commonSituations":"Fan-out of a block that writes files or runs git commands; block authored as read-only later gaining writes; forgetting max_parallel: 1 for shared-checkout blocks.","solutions":["Add `mutates_checkout: false` to the included block if it only reads the repo","Set `fan_out.max_parallel: 1` on the node to run instances one at a time"],"exampleFix":"# before\n- id: loop\n  include: transform\n  fan_out: { over: items }\n# after\n- id: loop\n  include: transform   # block frontmatter: mutates_checkout: false (read-only)\n  fan_out: { over: items }\n# or\n  fan_out: { over: items, max_parallel: 1 }","handlingStrategy":"validation","validationCode":"// Pre-flight composed fan-out against checkout mutation\nconst block = await composer.resolveBlock(node.include);\nconst parallel = node.fan_out?.max_parallel ?? Infinity;\nif (parallel > 1 && block.frontmatter.mutates_checkout !== false) {\n  throw new Error(`${node.include} may mutate the checkout; declare mutates_checkout: false or set max_parallel: 1`);\n}","typeGuard":"function isComposedFanOutSafe(node: { fan_out?: { max_parallel?: number } }, block: { frontmatter: { mutates_checkout?: boolean } }): boolean {\n  return (node.fan_out?.max_parallel ?? 2) <= 1 || block.frontmatter.mutates_checkout === false;\n}","tryCatchPattern":null,"preventionTips":["Tag read-only blocks with `mutates_checkout: false` so they are composable in parallel","Prefer max_parallel: 1 for blocks that touch the working tree","Review block frontmatter whenever a block gains file writes"],"tags":["workflow","fan-out","composition","concurrency","checkout-lock"],"backgroundTag":"shared-checkout-lock-collision","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}