{"record":{"id":"b2dfe8891af97452","repo":"coleam00/Archon","slug":"workflow-fan-out-shared-checkout-collision","errorCode":"workflow.fan_out_shared_checkout_collision","errorMessage":"fan_out node '${node.id}': up to ${String(concurrency)} children of '${node.workflow}' would run at once in the parent checkout, and that workflow 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 child is not recoverable by resume (#2180). Choose one: add `mutates_checkout: false` to '${node.workflow}' if it only reads the repo; set `isolation: worktree` on '${node.id}' if the children write to it; or set `fan_out.max_parallel: 1` to run them one at a time.","messagePattern":"fan_out node '\\$\\{node\\.id\\}': up to \\$\\{String\\(concurrency\\)\\} children of '\\$\\{node\\.workflow\\}' would run at once in the parent checkout, and that workflow 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 child is not recoverable by resume \\(#2180\\)\\. Choose one: add `mutates_checkout: false` to '\\$\\{node\\.workflow\\}' if it only reads the repo; set `isolation: worktree` on '\\$\\{node\\.id\\}' if the children write to it; or set `fan_out\\.max_parallel: 1` to run them one at a time\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/workflows/src/dag-executor.ts","lineNumber":8504,"sourceCode":"        `fan_out node '${node.id}': target workflow '${node.workflow}' is interactive-class ` +\n        \"('interactive: true') and may pause for human input — a fan-out has a single \" +\n        'approval-gate slot for N children, so this is refused before any child is created. ' +\n        `Remove the pause capability from '${node.workflow}', or invoke it as a single ` +\n        \"(non-fan-out) 'workflow:' node instead.\";\n      getLog().warn(\n        { parentRunId: parentRun.id, nodeId: node.id, childWorkflow: node.workflow },\n        'workflow.fan_out_interactive_target'\n      );\n      await notify(`❌ **Fan-out blocked** (node \\`${node.id}\\`): ${msg}`);\n      return failResult(msg);\n    }\n    if (\n      node.isolation !== 'worktree' &&\n      plannedConcurrency > 1 &&\n      resolved.definition.mutates_checkout !== false\n    ) {\n      const msg = fanOutSharedCheckoutMessage(node, plannedConcurrency);\n      getLog().warn(\n        {\n          parentRunId: parentRun.id,\n          nodeId: node.id,\n          childWorkflow: node.workflow,\n          plannedConcurrency,\n        },\n        'workflow.fan_out_shared_checkout_collision'\n      );\n      await notify(`❌ **Fan-out blocked** (node \\`${node.id}\\`): ${msg}`);\n      return failResult(msg);\n    }\n  }\n\n  // 6. Execute EVERY index through a bounded sliding window. Classification per index: an\n  //    existing completed child threads its recorded outcome (resume skip); an existing\n  //    failed OR fan-out-cancelled (recoverable) child is re-driven; a user-cancelled child\n  //    stays terminal; a missing index spawns fresh.\n  //","sourceCodeStart":8486,"sourceCodeEnd":8522,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/dag-executor.ts#L8486-L8522","documentation":"Refused at planning time: a fan-out would run more than one child of a workflow that can mutate the shared parent checkout. Concurrent runs on one checkout take a path-exclusive lock, so all but the first child would self-cancel, and lock-cancelled children are not recoverable by resume (#2180). The executor blocks the node before creating children.","triggerScenarios":"Executing a `fan_out` node where `node.isolation !== 'worktree'`, plannedConcurrency > 1, and the target workflow definition does not set `mutates_checkout: false`.","commonSituations":"Fanning out a workflow that runs `git commit`/build steps; forgetting `mutates_checkout: false` on a read-only workflow; forgetting `isolation: worktree` on write-heavy fan-outs.","solutions":["Add `mutates_checkout: false` to the target workflow if it only reads the repo","Set `isolation: worktree` on the fan-out node so each child gets its own checkout","Set `fan_out.max_parallel: 1` to run children sequentially under the lock"],"exampleFix":"# before\n- id: batch\n  workflow: repo-scan\n  fan_out: { over: repos }\n# after (read-only target)\nworkflow: repo-scan   # frontmatter now: mutates_checkout: false\n  fan_out: { over: repos }","handlingStrategy":"validation","validationCode":"// Pre-flight: fan-out into a shared checkout is only safe if read-only or serialized\nconst def = await workflows.resolve(node.workflow);\nconst parallel = node.fan_out?.max_parallel ?? Infinity;\nif (node.isolation !== 'worktree' && parallel > 1 && def.frontmatter.mutates_checkout !== false) {\n  throw new Error(`${node.workflow} mutates the checkout; use isolation: worktree, max_parallel: 1, or mutates_checkout: false`);\n}","typeGuard":"function isCheckoutSafeFanOut(node: { isolation?: string; fan_out?: { max_parallel?: number } }, def: { frontmatter: { mutates_checkout?: boolean } }): boolean {\n  return node.isolation === 'worktree' || (node.fan_out?.max_parallel ?? 2) <= 1 || def.frontmatter.mutates_checkout === false;\n}","tryCatchPattern":null,"preventionTips":["Declare `mutates_checkout: false` on read-only workflows so they are fan-out friendly","Default write-heavy fan-outs to `isolation: worktree`","Set `fan_out.max_parallel: 1` whenever children share a checkout"],"tags":["workflow","fan-out","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"}