@danmoisan/drm-copilot-mcp 1.1.16 → 1.1.18

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 (105) hide show
  1. package/out/mcp-server.js +1139 -133
  2. package/package.json +4 -3
  3. package/resources/claude-customizations/.claude/agents/feature-review.md +10 -1
  4. package/resources/claude-customizations/.claude/agents/orchestrator.md +16 -3
  5. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +18 -6
  6. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  7. package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  14. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  19. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
  20. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  21. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  22. package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +4 -2
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  24. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  25. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +375 -0
  28. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
  29. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
  30. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
  31. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  32. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  33. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  34. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  35. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  36. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  37. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +119 -3
  38. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  39. package/resources/claude-customizations/.claude/rules/quality-tiers.md +1 -1
  40. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  41. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +2 -0
  42. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
  43. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
  44. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +169 -5
  45. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
  46. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
  47. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +4 -15
  48. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +15 -6
  49. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  50. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
  51. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +3 -1
  52. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  53. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
  54. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  55. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +33 -13
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +50 -21
  57. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  58. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  59. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +5 -1
  60. package/resources/claude-customizations/pack-manifests/core.json +10 -0
  61. package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
  62. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  63. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +5 -3
  64. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  65. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  66. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
  67. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +3 -1
  68. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
  69. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  70. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +30 -7
  71. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
  72. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +18 -3
  73. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  74. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +1 -1
  75. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
  76. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
  77. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
  78. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
  79. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
  80. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
  81. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
  82. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +1 -1
  83. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +1 -1
  84. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +1 -1
  85. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +1 -1
  86. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +1 -1
  87. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +1 -1
  88. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  89. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  90. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  91. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  92. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  93. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  94. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  95. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  96. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  97. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
  98. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  99. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  100. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  101. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  102. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  103. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  104. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  105. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -1
@@ -24,7 +24,7 @@ description: Checkpoint invariants for the orchestration state artifact and the
24
24
 
25
25
  # Orchestrator-State Remediation-Cycle and Human-Interaction Invariants
26
26
 
27
- This rule governs remediation-cycle records and the optional `human_interaction` block in the orchestrator-state checkpoint at `artifacts/orchestration/orchestrator-state.json`. It documents three invariants that must hold for each remediation cycle, plus three invariants for the `human_interaction` block, so that resume and review workflows do not depend on a structurally invalid checkpoint.
27
+ This rule governs remediation-cycle records and the optional `human_interaction` block in the orchestrator-state checkpoint at `artifacts/orchestration/orchestrator-state.json`. It documents the invariants that must hold for each remediation cycle and for the remediation-loop review outcomes and attempt count, plus three invariants for the `human_interaction` block, so that resume and review workflows do not depend on a structurally invalid checkpoint.
28
28
 
29
29
  ## Foreign Schema Warning (do not copy verbatim)
30
30
 
@@ -34,7 +34,7 @@ This prohibition is specific to the disqualified foreign schema identified by th
34
34
 
35
35
  ## Scope and Backward Compatibility
36
36
 
37
- These invariants apply only when the checkpoint contains a top-level `remediation_loop` with a `cycles` array. A checkpoint with no `remediation_loop` (the existing step-based checkpoint shape) is unaffected: it validates exactly as before and produces no new errors. The invariants are additive.
37
+ These invariants apply only when the checkpoint contains a top-level `remediation_loop`; the per-cycle invariants apply when it has a `cycles` array, and the review-outcome and attempt-count invariants apply when their keys are present. A checkpoint with no `remediation_loop` (the existing step-based checkpoint shape) is unaffected: it validates exactly as before and produces no new errors. The invariants are additive.
38
38
 
39
39
  ## Invariants (per remediation cycle)
40
40
 
@@ -44,6 +44,45 @@ These invariants apply only when the checkpoint contains a top-level `remediatio
44
44
 
45
45
  3. **Exit gate requires zero blocking findings.** When a cycle's `exit_condition_met == true`, its `blocking_count` must be `0`. A non-zero `blocking_count` with `exit_condition_met == true` is a malformed cycle (the exit gate was marked satisfied while blocking findings remained).
46
46
 
47
+ 4. **`candidate_applied` is a boolean (R5).** When a cycle object carries `candidate_applied`, its value must be a JSON boolean, and any other value is a malformed cycle.
48
+
49
+ 5. **A completed attempt requires completed execution (R6).** A cycle whose `candidate_applied == true` must have `execution_status == "complete"`, and `candidate_applied == true` with any other execution status is a malformed cycle.
50
+
51
+ 6. **`opened_by_review` references a remediation review (R11).** When a cycle object carries `opened_by_review`, its value must be a non-negative integer (not a boolean) that indexes an object entry of the `remediation_loop.review_outcomes` list whose `verdict` is `REMEDIATION_REQUIRED`, and a value that is not such an integer, is out of range, or references an outcome with any other verdict is a malformed cycle.
52
+
53
+ 7. **`completed_attempts` counts completed attempts (R7).** When `remediation_loop.completed_attempts` is present, it must be a non-negative integer (not a boolean) equal to the number of object cycles whose `candidate_applied` is `true` (zero when `cycles` is absent or not a list), and any other value is a malformed loop.
54
+
55
+ ## Invariants (remediation_loop.review_outcomes)
56
+
57
+ These invariants apply only when `remediation_loop` contains a `review_outcomes` key. The orchestrator appends one entry per review, including a `PASS` review, in the shape `{"verdict": <verdict>, "findings": [{"remediability": <class>}, ...]}`, where `findings` lists the review's blocking findings only. A halt or wait verdict records no remediation cycle.
58
+
59
+ Review verdicts (exact, case-sensitive):
60
+
61
+ | Verdict | Condition over the classes of the review's blocking findings |
62
+ |---|---|
63
+ | `PASS` | No blocking findings. |
64
+ | `REMEDIATION_REQUIRED` | At least one `autonomous` finding. |
65
+ | `HALT_NON_REMEDIABLE` | No `autonomous` finding, and at least one `external_dependency`, `policy_hold`, or `human_decision_required` finding. |
66
+ | `AWAITING_CI` | At least one finding, and every finding is `awaiting_ci`. |
67
+
68
+ Remediability classes (exact, case-sensitive) and the `blocked_reason` member (#523) that a halt or wait records for each class:
69
+
70
+ | Class | Meaning | `blocked_reason` member |
71
+ |---|---|---|
72
+ | `autonomous` | Repository remediation can resolve the finding without a human decision. | none (remediation loop) |
73
+ | `external_dependency` | A system, service, runtime, or published artifact outside the repository is unavailable or mismatched. | `external_dependency` |
74
+ | `policy_hold` | Proceeding requires a policy decision, exception, or authorization that the orchestrator may not grant. | `policy_hold` |
75
+ | `awaiting_ci` | The finding resolves when a CI result that has not completed becomes available. | `awaiting_ci` |
76
+ | `human_decision_required` | A human must choose between alternatives or approve a direction. | `human_decision_required` |
77
+
78
+ Derivation order. The verdict is a pure function of the findings' classes, evaluated in this order: no findings yields `PASS`; any `autonomous` finding yields `REMEDIATION_REQUIRED`; any `external_dependency`, `policy_hold`, or `human_decision_required` finding yields `HALT_NON_REMEDIABLE`; otherwise the verdict is `AWAITING_CI`.
79
+
80
+ 1. **`review_outcomes` is a list of objects (R8).** `review_outcomes` must be a list, and each entry must be an object. A non-list value or a non-object entry is malformed.
81
+
82
+ 2. **Verdict and class membership (R9).** Each outcome's `verdict` must be a string among the four verdicts, its `findings` must be a list (a missing key is not a list), and each finding must be an object whose `remediability` is a string among the five classes. Comparisons are case-sensitive, so a case variant such as `Pass` or `Autonomous` is malformed.
83
+
84
+ 3. **Verdict matches its findings (R10).** When an outcome passes the membership checks, its `verdict` must equal the verdict that the derivation order yields for its findings' classes. A mismatch is a malformed outcome.
85
+
47
86
  ## Human-Interaction Scope and Backward Compatibility
48
87
 
49
88
  These invariants apply only when the checkpoint contains a top-level `human_interaction` block. A checkpoint with no `human_interaction` key (the existing checkpoint shape) is unaffected: it validates exactly as before and produces no new errors. The invariants are additive and support the autonomous-execution mandate documented in `.claude/skills/orchestrate/SKILL.md`.
@@ -56,6 +95,41 @@ These invariants apply only when the checkpoint contains a top-level `human_inte
56
95
 
57
96
  3. **Exception requires `runbook_path`.** A requirement whose `response == "exception"` must carry a non-empty `runbook_path` string. A missing, non-string, or empty/whitespace-only `runbook_path` on an `exception` requirement is a malformed requirement.
58
97
 
98
+ ## Blocked-Reason Vocabulary
99
+
100
+ The checkpoint key `blocked_reason` accepts exactly the twelve members below. The authoritative definition is `scripts/dev_tools/_orchestrator_state_blocked_reason.py`; the TypeScript module `extensions/drm-copilot/src/lib/validate/orchestrator-state-blocked-reason.ts` and the grouped arrays in `.claude/lib/orchestrator-state/OrchestratorState.psm1` mirror it.
101
+
102
+ | Member | Partition | Definition |
103
+ |---|---|---|
104
+ | `none` | not blocked | The run is not blocked. JSON `null` is equivalent for classification. |
105
+ | `spawn_agent_unavailable` | mechanical | Existing member; meaning unchanged. |
106
+ | `delegation_launch_failed` | mechanical | Existing member; meaning unchanged. |
107
+ | `delegate_no_receipt` | mechanical | Existing member; meaning unchanged. |
108
+ | `delegate_contract_incomplete` | mechanical | Existing member; meaning unchanged. |
109
+ | `validator_failed` | mechanical | Existing member; meaning unchanged. |
110
+ | `user_requested_stop` | mechanical | Existing member; meaning unchanged. |
111
+ | `premise_falsified` | non-mechanical | Every delegation and validator succeeded, but evidence gathered during execution falsified the premise on which the plan was built, so continuing would implement an invalid plan. |
112
+ | `external_dependency` | non-mechanical | The run cannot proceed because a system, service, runtime, or artifact outside the repository's control is unavailable or mismatched, and no in-repository remediation can resolve it. |
113
+ | `policy_hold` | non-mechanical | The run is stopped because proceeding requires a policy decision, exception, or authorization that the orchestrator is not permitted to grant autonomously. |
114
+ | `awaiting_ci` | non-mechanical | The run is waiting for a CI result that has not yet completed, and no remediation is warranted until that result is available. |
115
+ | `human_decision_required` | non-mechanical | The run requires a human to choose between alternatives or approve a direction before it can continue. |
116
+
117
+ Partition definitions:
118
+
119
+ - **Not blocked:** `none`, JSON `null`, or key absent at a gate. Plain validation still reports an absent key through the required-key check.
120
+ - **Mechanical:** the orchestration process itself did not complete a step: a tool or delegation failed, a delegate returned no or an incomplete receipt, a validator failed, or the operator stopped execution. Membership is defined by the published constant, not by the wording.
121
+ - **Non-mechanical:** every process step could proceed, but the work cannot continue for a reason that no retry or remediation cycle of the process resolves.
122
+
123
+ The classification is recoverable from `blocked_reason` alone by looking the value up in the published partition (`classify_blocked_reason` in Python, `classifyBlockedReason` in TypeScript).
124
+
125
+ Enforcement parity: the Python, PowerShell, and TypeScript validators enforce this vocabulary identically, with case-sensitive comparison. A value outside the vocabulary, including a case variant of any member, is rejected with `Checkpoint has invalid blocked_reason: <value>`. Every non-`none` member, including the five non-mechanical members, blocks completion and PR-creation readiness with the existing messages.
126
+
127
+ Relationship to `human_interaction.requirements[].response == "halt"`: a human-decision halt may appear in both places. `blocked_reason: "human_decision_required"` classifies the run's halt, and a `human_interaction.requirements[]` entry with `response: "halt"` describes the specific requirement. There is no cross-field rule between them: either may appear without the other, and neither is validated against the other.
128
+
129
+ Producer guidance for `premise_falsified`: set it when every delegation and validator succeeded but evidence gathered during execution falsified the plan's premise. The evidence path is still recorded in free-form checkpoint keys; `blocked_reason` carries the classification only.
130
+
131
+ Extension point for #484: #484 consumes `external_dependency`, `policy_hold`, `awaiting_ci`, and `human_decision_required` without renaming, removing, or re-partitioning them; any further halt class is a separate contract change that appends the literal to the non-mechanical partition in all three runtimes, updates the partition oracle and corpus, and updates this section.
132
+
59
133
  ## Complexity-Assessment Scope and Backward Compatibility
60
134
 
61
135
  These invariants apply only when the checkpoint contains a top-level `complexity_assessments` array. A checkpoint with no `complexity_assessments` key (the existing checkpoint shape) is unaffected: it validates exactly as before and produces no new errors. The invariants are additive and support the two-axis model-selection mechanism documented in `.claude/skills/orchestrate/SKILL.md` (`## Model Selection`). Enforcement is the Python validator, not an imported schema.
@@ -92,7 +166,11 @@ The session `model_budget.fable_policy` switch is a three-way enum `disabled | a
92
166
 
93
167
  ## Require-Model-Routing Mode Scope and Backward Compatibility
94
168
 
95
- The complexity-assessment and model-routing-receipt invariants above are key-gated: they run only when their key is present, so a checkpoint that omits both arrays passes at every stage. The `require_model_routing` mode adds an existence gate that closes that gap without changing the default behavior. It is an opt-in keyword on `validate_orchestrator_state_text(..., require_model_routing=False)` (CLI flag `--require-model-routing`; MCP parameter `require_model_routing`), defaulting off. Plain, `require_complete`, and `require_pr_creation_ready` calls are unaffected and produce byte-identical results.
169
+ The complexity-assessment and model-routing-receipt invariants above are key-gated: they run only when their key is present, so a checkpoint that omits both arrays passes at every stage. The `require_model_routing` mode adds an existence gate that closes that gap without changing the default behavior. It is an opt-in keyword on `validate_orchestrator_state_text(..., require_model_routing=False)` (CLI flag `--require-model-routing`, accepted by the dispatcher `orchestrator-state` subcommand and by the bare-module CLI; MCP parameter `require_model_routing`), defaulting off. Plain, `require_complete`, and `require_pr_creation_ready` calls are unaffected and produce byte-identical results.
170
+
171
+ ## Bare-Module CLI Contract
172
+
173
+ The validator module runs directly as `python -m scripts.dev_tools.validate_orchestrator_state <path> [--require-complete] [--require-model-routing] [--require-pr-creation-ready] [--require-codex-model-routing] [--require-codex-topology]`. The dispatcher `python -m scripts.dev_tools.validate_orchestration_artifacts orchestrator-state` stays available and accepts the same flags. Exit code 0 means the checkpoint passed and the success line `orchestrator-state validation passed: <path>` is written to stdout. Exit code 1 means validation errors, written one per stderr line. Exit code 2 means the path is missing, unreadable, or not UTF-8, with one stderr diagnostic and no traceback; argparse usage errors also exit 2.
96
174
 
97
175
  ## Invariants (require_model_routing mode)
98
176
 
@@ -146,6 +224,43 @@ not a cryptographic or security control.
146
224
 
147
225
  Field checks 3 through 9 run in the fixed order `pr_url`, `issue_num`, `branch_name`, `authorized_by`, `authorized_at`, `basis`, `run_slug`, `session_id`; the first failure wins and the gate denies with `STANDALONE_MERGE_AUTHORIZATION_MALFORMED` naming that field.
148
226
 
227
+ ## Issue-Adoption Scope and Backward Compatibility
228
+
229
+ These invariants apply only when a checkpoint contains a top-level `issue_adoption` key. The key records that the orchestration adopted a GitHub issue that already existed before orchestration started (transferred, filed by hand, or created by epic decomposition), so the issue was never created through `potential_to_issue`. The key is presence-gated: a checkpoint without it validates byte-identically to before and produces no new errors. It is evaluated only by the routing-contract completion check (`validate_routing_contract` and its TypeScript and PowerShell ports). It is not a member of `REQUIRED_STATE_KEYS`, it is not read by plain (non-completion) validation, and it is not read by the PR-creation-readiness gate. No JSON Schema file is authored, imported, or read for it.
230
+
231
+ Honest disclosure: the validators perform no network I/O. They cannot confirm that the issue exists, so `evidence` and `verified_via` are an auditable declaration of the read-only verification the orchestrator performed, not proof of it. This matches the `standalone_merge_authorizations` precedent above: the record is a policy-level declaration, not a security control.
232
+
233
+ ## Invariants (issue_adoption object)
234
+
235
+ The object has the shape `{ issue_num, issue_url, origin, verified_via, verified_at, evidence, waived_tools, potential_record? }`.
236
+
237
+ | Field | Rule |
238
+ | --- | --- |
239
+ | `issue_num` | A string of decimal digits without a leading zero; equal to the checkpoint `issue-num` when that is a string. |
240
+ | `issue_url` | A string ending with `/issues/` followed by `issue_num`. |
241
+ | `origin` | One of `transferred`, `filed_before_orchestration`, `epic_decomposition`. |
242
+ | `verified_via` | One of `gh_issue_view`, `gh_api_get`, `github_mcp_issue_read`. |
243
+ | `verified_at` | Present, not `null`, and not a blank string. |
244
+ | `evidence` | A string with non-whitespace content. |
245
+ | `waived_tools` | A non-empty list of non-blank tool names that includes `potential_to_issue`. |
246
+ | `potential_record` | Required when waiving a promotion-entry tool: a path under `docs/features/potential/` ending in `.md`. |
247
+
248
+ The rules run in this order, and their errors accumulate:
249
+
250
+ 1. A present value that is not an object (including `null`) yields `Checkpoint issue_adoption must be an object when present.` and evaluation stops.
251
+ 2. `issue_num` must be a string of decimal digits without a leading zero (a JSON number is invalid). Only when it is valid and the checkpoint `issue-num` is a different string, `issue_num` must equal the checkpoint issue-num.
252
+ 3. `issue_url` must end with `/issues/` followed by a valid `issue_num`.
253
+ 4. `origin` must be one of the three origin values.
254
+ 5. `verified_via` must be one of the three verification values.
255
+ 6. `verified_at` must be present: absent, `null`, and blank strings are rejected; any other value passes.
256
+ 7. `evidence` must be a non-empty string.
257
+ 8. `waived_tools` must be a non-empty list of non-blank strings; otherwise no further rule-8 or rule-9 check runs. Each entry, in list order, receives the first applicable error of: listed more than once; not in the closed waivable set; not required by the selected route after promotion-type resolution; already holding a successful MCP receipt. The list must include `potential_to_issue`.
258
+ 9. Only when the rule-8 shape check passed: each distinct waived promotion-entry tool (`new_potential_entry` or `new_potential_bug_entry`) requires a valid `potential_record`.
259
+
260
+ The closed waivable set is `potential_to_issue`, `new_potential_entry`, and `new_potential_bug_entry`. `new_active_feature_folder`, `collect_pr_context`, `validate_orchestration_artifacts`, and every other tool can never be waived. All comparisons are ordinal and case-sensitive.
261
+
262
+ Fail-closed behavior: any adoption error waives nothing, so each missing receipt is still reported as `Checkpoint missing successful MCP receipt: <tool>.`. Adoption errors are placed after the receipt-loop errors and before the `local_execution_overrides` errors. The waiver affects only receipt presence: the declared `required_mcp_tools` equality check with the routing matrix is unchanged, so a checkpoint must still declare every route tool it waives.
263
+
149
264
  ## Enforcement
150
265
 
151
266
  - `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated invariant when a `remediation_loop` is present, using the existing validator message style (literal, checkpoint-context prefixed). The validator returns a list of error strings and does not mutate its input.
@@ -157,3 +272,4 @@ Field checks 3 through 9 run in the fixed order `pr_url`, `issue_num`, `branch_n
157
272
  - The standalone-merge authorization invariants are enforced by the `PreToolUse` merge gate (`.claude/hooks/enforce-epic-merge-gate.ps1` with its dot-sourced helpers file, and `.codex/hooks/enforce-epic-merge-gate.ps1`) at merge time. The Python checkpoint validator
158
273
  does not currently validate standalone_merge_authorizations
159
274
  entries; adding that check is recorded as follow-up FU-3.
275
+ - The `issue_adoption` invariants are enforced at completion by the routing-contract check in all three runtimes: `scripts/dev_tools/_orchestrator_state_issue_adoption.py` (Python authority), `extensions/drm-copilot/src/lib/validate/orchestrator-state-issue-adoption.ts`, and `.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1`, with byte-identical error strings pinned by the shared corpus under `tests/fixtures/orchestrator_state_issue_adoption/`.
@@ -611,6 +611,27 @@ in `tests/scripts/dev_tools/test_blast_radius_config_parity.py`, mirrored in
611
611
  union of both copies' top-level keys is exhaustively covered by the three declared classes, and
612
612
  an unclassified key or a key present in only one copy fails loudly and names itself.
613
613
 
614
+ **A destination records its own entries in `config/blast-radius.local.json` (issue #508).** The
615
+ push-down regenerates a destination's `config/blast-radius.json` on every push, so an entry
616
+ hand-added to that file does not survive the next push. The supported extension point is the
617
+ destination-owned overlay `config/blast-radius.local.json`. No payload contains it, and the
618
+ push-down never writes or publishes it: both implementations list it in `EXCLUDED_RELATIVE_PATHS`.
619
+ At push time the overlay is composed onto the freshly generated base document (the derived document
620
+ in TypeScript, the published document in Python), and the composed result is written to
621
+ `config/blast-radius.json`, so consumers continue to read one file. Per-key semantics: the
622
+ string-list keys `shared_surfaces`, `shared_surface_globs`, `mandate_reads`, `mergeable_paths`, and
623
+ `path_roots` take an ordered union (base entries first, then overlay-only entries, duplicates
624
+ removed); an overlay module absent from the base is added and an overlay module with the same name
625
+ replaces that module's glob list, with module names emitted in ordinal order; `conflict_tolerance`
626
+ merges recursively (nested lists by union, nested objects per member, nested scalars overlay-wins);
627
+ the scalars `over_breadth_fraction` and `write_intent_extraction` take the overlay value; overlay-only
628
+ keys are appended after the base keys. An overlay `version` must equal the base `version`, and the
629
+ forbidden-glob guard (`**`, `docs/**`, `tests/**`) applies to the composed module map, including
630
+ overlay-authored modules. A malformed overlay, a `version` mismatch, or a forbidden glob raises an
631
+ error before any write, so `config/blast-radius.json` keeps its prior bytes. With no overlay
632
+ present, the written file is byte-identical to the base document. The overlay cannot remove a
633
+ shipped surface or a derived module.
634
+
614
635
  ## Enforcement
615
636
 
616
637
  - `scripts/dev_tools/validate_parallel_orchestrator_state.py`, with the helper modules `scripts/dev_tools/_parallel_state_common.py`, `scripts/dev_tools/_parallel_state_structures.py`, and `scripts/dev_tools/_parallel_state_records.py`, appends one error per violated orchestrator invariant. The completion-gate invariants 20 and 21 run only when the caller passes `require_complete=True`.
@@ -6,7 +6,7 @@ description: Module rigor tier system and uniform coverage thresholds.
6
6
 
7
7
  # Module Rigor Tiers
8
8
 
9
- This rule defines the T1–T4 module rigor tier system used by all CI gates in this repository. The tier system source of truth is `docs/ci.research.md` section 1; the file `quality-tiers.yml` at the repository root maps every project to a tier. Adding a project without a tier classification fails CI.
9
+ This rule defines the T1–T4 module rigor tier system used by all CI gates in this repository. The tier definitions and gate matrix in this document are the tier system's source of truth; the file `quality-tiers.yml` at the repository root maps every project to a tier. Adding a project without a tier classification fails CI.
10
10
 
11
11
  ## Tiers
12
12
 
@@ -70,6 +70,14 @@ During plan execution, after completing a task whose work satisfies an acceptanc
70
70
 
71
71
  Timing: Check off AC items as soon as the corresponding plan task passes verification — do not defer all AC updates to the end of plan execution.
72
72
 
73
+ ### CI-Dependent Criteria
74
+
75
+ A CI-dependent criterion is one whose verification requires the result of CI on the PR head. It cannot be verified before the pull request exists, so it stays unchecked and is listed as pending-CI in the AC verification artifact until CI has run.
76
+
77
+ - Owner: the item's own orchestrator run owns the check-off and performs it at S9; this is the one case in which an orchestrator run checks off AC. A parent or coordinating session never commits the check-off from its own root.
78
+ - Timing: after the CI green gate (S9) records `ci_gate.conclusion` as `success`, and before the run reports DONE.
79
+ - Push and re-verify: the check-off is committed in the item's own worktree and pushed to the PR branch, and the CI green gate is re-run against the new head SHA so that `ci_gate.head_sha` equals the final PR head before DONE.
80
+
73
81
  ### When Reviewers Check Off AC
74
82
 
75
83
  During feature review (feature-audit phase):
@@ -212,6 +212,8 @@ Rules:
212
212
  - **Observe a command's success-case output before asserting over that output (mandatory).** Run the command, or read a recorded run of it, and confirm the value you intend to assert is actually printed on a *successful* run. Do not infer it from the tool's documentation, from the plan, or from what the tool prints on failure. This requirement exists because the class of defect it prevents is not detectable by reading: an assertion on a line the tool prints only when it changed something is unsatisfiable on a clean run; a demand for a zero-diagnostic count from a tool that returns only an ok flag names a value with no source; a demand for separate line and branch percentages from a coverage run that prints one combined column reads two numbers that are never printed. G7 and G9 cover two decidable slices of this class. The remainder is covered by this requirement and by nothing else, so a reviewer cannot rely on the gate to catch it.
213
213
  - **Fix the evidence in the plan; never leave the executor to select it.** An executor free to choose the evidence it is judged against cannot fail. Do not write an acceptance condition that asks the executor to identify "the known-genuine pair", to choose a survivor list, or to pick any suitable instance. Name the instances, or state the mechanical derivation that produces them, so a third party re-running it obtains the same set. This is authoring guidance and not a rule: a validator rule that scanned acceptance text for selection vocabulary was proposed and rejected, because that vocabulary is ordinary plan prose used in roles carrying no selection semantics and a keyword scan over prose is not statically decidable for the property it would claim to detect. The judgment is the author's, and it is not automated. `.claude/rules/plan-acceptance-gates.md` records the rejection and the reason for it.
214
214
  - **Check that the task-ordering does not make the condition unsatisfiable.** No rule covers this. A gate that runs a test path containing deliberately-failing cases added by an earlier task, before the later task that makes them pass, cannot exit 0. A baseline captured after a write-mode formatter has already repaired pre-existing drift becomes either a blanket waiver or makes a later gate unsatisfiable. Read each acceptance condition against the state the plan will actually be in when its task runs.
215
+ - **Name the runner and job for a Pester acceptance criterion.** CI runs Pester in two jobs of `.github/workflows/_poshqc.yml`: `poshqc / PowerShell QC` runs every suite with coverage on `windows-latest`, and `poshqc / PowerShell hook suites (Linux)` runs only `tests/scripts/claude-hooks` and `tests/scripts/codex-hooks` on `ubuntu-latest`, with coverage disabled. A Pester acceptance criterion may cite `ubuntu-latest` only for suites under `tests/scripts/claude-hooks` or `tests/scripts/codex-hooks`, never for coverage, and must name the job it relies on (`poshqc / PowerShell QC` or `poshqc / PowerShell hook suites (Linux)`).
216
+ - **Do not assert a fixed-string search literal that contains a backslash.** Git for Windows grep 3.0 reads a doubled backslash in a `grep -F` pattern as one backslash, so the same check returns a different count locally than on a CI runner and cannot pass in both places. Assert a backslash-free substring of the literal, a named test, or a hash or byte-identity check instead.
215
217
 
216
218
  ## Plan-Path Continuity Contract (Mandatory)
217
219
 
@@ -128,9 +128,17 @@ The script emits pipe-delimited, `LC_ALL=C`-ordered records, one per line:
128
128
  per-file outcome is reported by the companion `ACTION|preserve-stage|...` record.
129
129
  - `ORPHAN_DIR|<path>|<size>` — a directory under a worktree-tracking root that carries
130
130
  no `.git` pointer file and no `git worktree list` entry. `<size>` is best-effort and
131
- may be the literal `unknown`. The record is advisory: it reports the directory, and
132
- nothing in apply mode acts on it. For the disposition, see the Dirty Worktree Triage
133
- Procedure's step 7, which governs how an orphaned directory is handled.
131
+ may be the literal `unknown`. The scanned roots are the default pair
132
+ `<main>/.claude/worktrees` and `<main>-wt`, or the `CLEANUP_WT_ORPHAN_ROOTS` entries
133
+ when that variable is set (the override replaces the default pair), plus the parent
134
+ directory of every non-main registered worktree, which is always added. A derived
135
+ parent is skipped when it is the main worktree or one of its ancestors, or is equal to
136
+ or inside a registered worktree. `CLEANUP_WT_ORPHAN_ROOTS` entries are separated by a
137
+ semicolon, a newline, or a colon; a colon that follows a single drive letter and
138
+ precedes `/` or `\` is part of the path, and empty or relative entries are dropped.
139
+ The record is advisory: it reports the directory, and nothing in apply mode acts on
140
+ it. For the disposition, see the Dirty Worktree Triage Procedure's step 7, which
141
+ governs how an orphaned directory is handled.
134
142
  - `STALE_REF|<refname>` — a `refs/remotes/<name>/*` ref whose `<name>` is not a
135
143
  configured remote, named in full ref form. Advisory only; no ref is ever pruned by
136
144
  this tool.
@@ -139,8 +139,17 @@ Environment overrides:
139
139
  nonexistent value is treated as missing (falls back to
140
140
  the bundled cleanup_worktrees_scan_helper.sh). This is
141
141
  the filesystem-scan test-stub seam.
142
- CLEANUP_WT_ORPHAN_ROOTS Colon-separated override for the worktree-tracking roots
143
- scanned for ORPHAN_DIR and WARN|registration-lost.
142
+ CLEANUP_WT_ORPHAN_ROOTS Worktree-tracking roots scanned for ORPHAN_DIR and
143
+ WARN|registration-lost. Entries are separated by a
144
+ semicolon, a newline, or a colon; a colon that follows
145
+ a single drive letter and precedes / or \ is part of
146
+ the path (C:/x). Empty and relative entries are
147
+ dropped. When set, it replaces the default pair
148
+ <main>/.claude/worktrees and <main>-wt. The parent of
149
+ every non-main registered worktree is always added,
150
+ except a parent that is the main worktree or its
151
+ ancestor, or is equal to or inside a registered
152
+ worktree.
144
153
  CLEANUP_WT_STUB_SCENARIO Scenario directory consumed by the checked-in git and
145
154
  scan stubs (tests only).
146
155
  CLEANUP_WT_CONSOLIDATION_PATH Override the derived consolidation worktree path
@@ -4,16 +4,19 @@
4
4
  # every file within the 500-line cap. Provides the git-binary test seam
5
5
  # (cleanup_wt_git), branch/worktree enumeration (enumerate_branches,
6
6
  # parse_worktree_list), path normalization (normalize_wt_path), the
7
- # current-worktree/branch and base-branch protection set (compute_protected), and
8
- # the main-freshness warning (check_main_freshness). The classification ladder,
9
- # consolidation, deletion, and the CLI live in sibling files (cleanup_worktrees_lib.sh,
10
- # cleanup_worktrees_actions_lib.sh, cleanup-worktrees.sh).
7
+ # current-worktree/branch and base-branch protection set (compute_protected), the
8
+ # main-freshness warning (check_main_freshness), the shared absolute-path predicate
9
+ # (cleanup_wt_is_absolute_path), and the worktree-tracking scan roots
10
+ # (cleanup_wt_split_roots, cleanup_wt_derive_scan_roots, cleanup_wt_scan_roots). The
11
+ # classification ladder, consolidation, deletion, and the CLI live in sibling files
12
+ # (cleanup_worktrees_lib.sh, cleanup_worktrees_actions_lib.sh, cleanup-worktrees.sh).
11
13
  #
12
14
  # Sourcing contract: defines functions and the single constant CLEANUP_WT_BASE_BRANCH
13
15
  # and runs no other work at source time, so wrapper and bats suites source it safely.
14
16
  # It MUST be sourced before cleanup_worktrees_lib.sh, whose classification functions
15
17
  # call cleanup_wt_git, parse_worktree_list, compute_protected, and normalize_wt_path
16
- # defined here.
18
+ # defined here. The filesystem-scan helper cleanup_worktrees_scan_helper.sh runs as a
19
+ # separate process and sources this library directly for cleanup_wt_is_absolute_path.
17
20
  #
18
21
  # All git commands go through cleanup_wt_git so tests can stub the git binary via
19
22
  # CLEANUP_WT_GIT_BIN. Git exit-code capture rule: the `git worktree list --porcelain`
@@ -250,3 +253,164 @@ check_main_freshness() {
250
253
  fi
251
254
  return 0
252
255
  }
256
+
257
+ cleanup_wt_is_absolute_path() {
258
+ # Return 0 when <path> is absolute, else 1. Pure: no filesystem access.
259
+ #
260
+ # Absolute forms are a leading `/` (POSIX paths, MSYS `/c/...` paths, and `//server`
261
+ # UNC paths) and a drive letter followed by `/` or `\` (`C:/...`, `c:/...`,
262
+ # `C:\...`), which is the form Git for Windows writes into a worktree's `.git`
263
+ # pointer file (issue #706). A drive letter with no separator (`C:rel`) is
264
+ # drive-relative and is not absolute; an empty path is not absolute. This is the one
265
+ # absolute-path predicate of the skill: the preserve library and the filesystem-scan
266
+ # helper both call it.
267
+ #
268
+ # Args: $1 = path. Returns 0 (absolute) or 1 (not absolute).
269
+ local path=${1:-}
270
+ [[ $path == /* || $path == [A-Za-z]:[/\\]* ]]
271
+ }
272
+
273
+ cleanup_wt_split_roots() {
274
+ # Split a CLEANUP_WT_ORPHAN_ROOTS value into absolute roots, one per line.
275
+ #
276
+ # Entries are separated by `;`, by a newline, or by `:`. A `:` is part of the path
277
+ # instead when the text before it in the current entry is exactly one ASCII letter
278
+ # and the character after it is `/` or `\` (a drive letter, as in `C:/x`). Empty
279
+ # entries are dropped. An entry that is not absolute (cleanup_wt_is_absolute_path) is
280
+ # dropped with a one-line stderr diagnostic naming it, because a relative root would
281
+ # be resolved against the process's current working directory.
282
+ #
283
+ # The value is walked one character at a time with parameter expansion and never
284
+ # word-split, so no pathname expansion occurs and a `*` is emitted literally.
285
+ #
286
+ # Args: $1 = the override value. Always returns 0.
287
+ local value=${1:-}
288
+ local -a entries=()
289
+ local entry="" ch next i
290
+ for ((i = 0; i < ${#value}; i++)); do
291
+ ch=${value:i:1}
292
+ if [[ $ch == ';' || $ch == $'\n' ]]; then
293
+ entries+=("$entry")
294
+ entry=""
295
+ elif [[ $ch == ':' ]]; then
296
+ next=${value:i+1:1}
297
+ if [[ ${#entry} -eq 1 && $entry == [A-Za-z] && $next == [/\\] ]]; then
298
+ entry+=$ch
299
+ else
300
+ entries+=("$entry")
301
+ entry=""
302
+ fi
303
+ else
304
+ entry+=$ch
305
+ fi
306
+ done
307
+ entries+=("$entry")
308
+ for entry in "${entries[@]}"; do
309
+ [[ -z $entry ]] && continue
310
+ if ! cleanup_wt_is_absolute_path "$entry"; then
311
+ printf 'cleanup-worktrees: CLEANUP_WT_ORPHAN_ROOTS entry is not absolute, dropped: %s\n' "$entry" >&2
312
+ continue
313
+ fi
314
+ printf '%s\n' "$entry"
315
+ done
316
+ return 0
317
+ }
318
+
319
+ cleanup_wt_derive_scan_roots() {
320
+ # Echo the registration-derived scan roots, one per line.
321
+ #
322
+ # Input is parse_worktree_list output; its first record is the main worktree. The
323
+ # candidate for every later record is the parent directory of its path (backslashes
324
+ # converted to `/`). A candidate is kept only when its normalize_wt_path value N:
325
+ # - is not the main worktree's normalized path and is not one of its ancestors
326
+ # (scanning an ancestor would list the main worktree's siblings), and
327
+ # - is not equal to, and not inside, any registered worktree, main included
328
+ # (its subdirectories are that worktree's own content, not worktrees).
329
+ # Kept candidates are emitted in LC_ALL=C order of N, once per N, using the first
330
+ # spelling seen in record order.
331
+ #
332
+ # Args: $1 = parse_worktree_list output. Always returns 0.
333
+ local records=${1:-}
334
+ local -a paths=() norms=()
335
+ local record path
336
+ while IFS= read -r record; do
337
+ [[ -z $record ]] && continue
338
+ path=${record%%|*}
339
+ paths+=("$path")
340
+ norms+=("$(normalize_wt_path "$path")")
341
+ done <<<"$records"
342
+ ((${#paths[@]} < 2)) && return 0
343
+ local main_norm=${norms[0]}
344
+ local -A seen=()
345
+ local -a kept=()
346
+ local i p parent n w inside
347
+ for ((i = 1; i < ${#paths[@]}; i++)); do
348
+ p=${paths[i]//\\//}
349
+ parent=${p%/*}
350
+ [[ -z $parent || $parent == "$p" ]] && continue
351
+ n=$(normalize_wt_path "$parent")
352
+ [[ -z $n || -n ${seen[$n]:-} ]] && continue
353
+ [[ $n == "$main_norm" || $main_norm == "$n"/* ]] && continue
354
+ inside=0
355
+ for w in "${norms[@]}"; do
356
+ [[ -z $w ]] && continue
357
+ if [[ $n == "$w" || $n == "$w"/* ]]; then
358
+ inside=1
359
+ break
360
+ fi
361
+ done
362
+ ((inside == 1)) && continue
363
+ seen[$n]=1
364
+ kept+=("$n|$parent")
365
+ done
366
+ ((${#kept[@]} == 0)) && return 0
367
+ local line
368
+ while IFS= read -r line; do
369
+ printf '%s\n' "${line#*|}"
370
+ done < <(printf '%s\n' "${kept[@]}" | LC_ALL=C sort -t '|' -k1,1)
371
+ return 0
372
+ }
373
+
374
+ cleanup_wt_scan_roots() {
375
+ # Echo the worktree-tracking roots to scan, one per line.
376
+ #
377
+ # Configured roots come first: the CLEANUP_WT_ORPHAN_ROOTS entries
378
+ # (cleanup_wt_split_roots) when that variable is set, otherwise the default pair
379
+ # derived from the main worktree path (the first `git worktree list --porcelain`
380
+ # stanza, the same derivation consolidation_worktree_path uses in
381
+ # cleanup_worktrees_actions_lib.sh): `<main>/.claude/worktrees` then `<main>-wt`.
382
+ # Registration-derived roots (cleanup_wt_derive_scan_roots) are always appended.
383
+ # The combined list is deduplicated by normalize_wt_path, keeping the first spelling.
384
+ #
385
+ # The listing is read once (`out=$(...) || rc=$?`). On a parse_worktree_list hard
386
+ # failure nothing is derived from it: with no override no root is emitted at all,
387
+ # rather than a bare relative path resolved against the current working directory,
388
+ # and with an override exactly the override roots are emitted.
389
+ # cleanup_wt_scan_records returns 0 with no record for an empty root list, so the
390
+ # advisory records degrade to silence rather than to a misleading scan.
391
+ # Always returns 0.
392
+ local out rc=0 first_record main_wt="" configured="" derived=""
393
+ out=$(parse_worktree_list) || rc=$?
394
+ if ((rc == 0)); then
395
+ first_record=${out%%$'\n'*}
396
+ main_wt=${first_record%%|*}
397
+ derived=$(cleanup_wt_derive_scan_roots "$out")
398
+ fi
399
+ if [[ -n ${CLEANUP_WT_ORPHAN_ROOTS:-} ]]; then
400
+ configured=$(cleanup_wt_split_roots "$CLEANUP_WT_ORPHAN_ROOTS")
401
+ elif [[ -n $main_wt ]]; then
402
+ configured="${main_wt}/.claude/worktrees"$'\n'"${main_wt}-wt"
403
+ fi
404
+ local -A seen=()
405
+ local root n
406
+ while IFS= read -r root; do
407
+ [[ -z $root ]] && continue
408
+ n=$(normalize_wt_path "$root")
409
+ # A root of `/` normalizes to empty, which is not a valid array key.
410
+ [[ -z $n ]] && n=/
411
+ [[ -n ${seen[$n]:-} ]] && continue
412
+ seen[$n]=1
413
+ printf '%s\n' "$root"
414
+ done <<<"${configured}"$'\n'"${derived}"
415
+ return 0
416
+ }
@@ -158,7 +158,7 @@ preserve_relative_path_reason() {
158
158
  local name=${1-} val=${2-}
159
159
  if [[ -z $val ]]; then
160
160
  printf '%s is empty' "$name"
161
- elif [[ $val == /* || $val == [A-Za-z]:[\\/]* ]]; then
161
+ elif cleanup_wt_is_absolute_path "$val"; then
162
162
  printf '%s is absolute: %s' "$name" "$val"
163
163
  elif [[ $val == ".." || $val == "../"* || $val == *"/../"* || $val == *"/.." ]]; then
164
164
  printf '%s contains a .. segment: %s' "$name" "$val"
@@ -6,10 +6,10 @@
6
6
  # whose gitdir pointer no longer resolves, and the ancestor relationship between two
7
7
  # branches that both resolve NOT_MERGED. The enumeration/protection seam
8
8
  # (cleanup_wt_git, enumerate_branches, parse_worktree_list, normalize_wt_path,
9
- # compute_protected, check_main_freshness) lives in cleanup_worktrees_enumerate_lib.sh
10
- # and the classification ladder (classify_branch) lives in cleanup_worktrees_lib.sh;
11
- # both MUST be sourced before this file. Keeping these records in a sibling file keeps
12
- # every file within the 500-line cap.
9
+ # compute_protected, check_main_freshness, cleanup_wt_scan_roots) lives in
10
+ # cleanup_worktrees_enumerate_lib.sh and the classification ladder (classify_branch)
11
+ # lives in cleanup_worktrees_lib.sh; both MUST be sourced before this file. Keeping
12
+ # these records in a sibling file keeps every file within the 500-line cap.
13
13
  #
14
14
  # Sourcing contract: this library defines functions only; it never runs work at source
15
15
  # time, so the wrapper and the bats suites can source it without side effects.
@@ -111,47 +111,10 @@ scan_stale_refs() {
111
111
  return 0
112
112
  }
113
113
 
114
- cleanup_wt_scan_roots() {
115
- # Echo the worktree-tracking roots to scan, one per line.
116
- #
117
- # CLEANUP_WT_ORPHAN_ROOTS overrides the derivation with a colon-separated list.
118
- # Otherwise BOTH roots derive from the main worktree path — the first
119
- # `git worktree list --porcelain` stanza, the same derivation
120
- # consolidation_worktree_path uses in cleanup_worktrees_actions_lib.sh. They are
121
- # `<main-worktree-path>` joined with `.claude/worktrees` (the agent worktree folder)
122
- # and `<main-worktree-path>-wt` (the sibling worktree folder), in that order.
123
- #
124
- # Deriving both from one read means a parse_worktree_list hard failure emits no root
125
- # at all, rather than a bare relative path that would be resolved against whatever
126
- # the process's current working directory happened to be. cleanup_wt_scan_records
127
- # returns 0 with no record for an empty root list, so the advisory records degrade to
128
- # silence rather than to a misleading scan. Always returns 0.
129
- local override=${CLEANUP_WT_ORPHAN_ROOTS:-}
130
- if [[ -n $override ]]; then
131
- local IFS=:
132
- local part
133
- for part in $override; do
134
- [[ -n $part ]] && printf '%s\n' "$part"
135
- done
136
- return 0
137
- fi
138
- local out rc=0 first_record main_wt=""
139
- out=$(parse_worktree_list) || rc=$?
140
- if ((rc != 0)); then
141
- return 0
142
- fi
143
- first_record=${out%%$'\n'*}
144
- main_wt=${first_record%%|*}
145
- if [[ -n $main_wt ]]; then
146
- printf '%s\n' "${main_wt}/.claude/worktrees"
147
- printf '%s\n' "${main_wt}-wt"
148
- fi
149
- return 0
150
- }
151
-
152
114
  cleanup_wt_scan_records() {
153
115
  # Echo the raw `<path>|<has_gitfile>|<gitdir_target_exists>|<size>` records for every
154
- # candidate directory under the resolved scan roots, one per line.
116
+ # candidate directory under the resolved scan roots, one per line. The roots come
117
+ # from cleanup_wt_scan_roots in cleanup_worktrees_enumerate_lib.sh.
155
118
  #
156
119
  # Every filesystem read goes through the CLEANUP_WT_SCAN_BIN seam, so the bats suites
157
120
  # replay canned records instead of touching the real filesystem. The scan output is
@@ -40,6 +40,9 @@
40
40
  # the SHELL_QC_<TOOL>_BIN seams substitute a checked-in stand-in for a real dependency.
41
41
  # An empty value is treated as missing and falls back to `.git`.
42
42
  set -euo pipefail
43
+ # shellcheck source=.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh
44
+ # shellcheck disable=SC1091
45
+ source "$(dirname -- "${BASH_SOURCE[0]}")/cleanup_worktrees_enumerate_lib.sh"
43
46
 
44
47
  scan_helper_dir_size() {
45
48
  # Echo the best-effort human-readable size of a directory, or `unknown`.
@@ -70,20 +73,6 @@ scan_helper_gitfile_name() {
70
73
  printf '%s\n' ".git"
71
74
  }
72
75
 
73
- scan_helper_is_absolute_path() {
74
- # Return 0 when <path> is absolute, else 1. Pure: no filesystem access.
75
- #
76
- # Absolute forms are a leading `/` (POSIX paths, MSYS `/c/...` paths, and `//server`
77
- # UNC paths) and a drive letter followed by `/` or `\` (`C:/...`, `c:/...`,
78
- # `C:\...`), which is the form Git for Windows writes into a worktree's `.git`
79
- # pointer file (issue #706). A drive letter with no separator (`C:rel`) is
80
- # drive-relative and is not absolute; an empty path is not absolute.
81
- #
82
- # Args: $1 = path. Returns 0 (absolute) or 1 (not absolute).
83
- local path=${1:-}
84
- [[ $path == /* || $path == [A-Za-z]:[/\\]* ]]
85
- }
86
-
87
76
  scan_helper_target_present() {
88
77
  # Return 0 when <path> exists, else non-zero. This is the helper's single
89
78
  # filesystem existence check, kept in its own function as a test seam: bats tests
@@ -113,7 +102,7 @@ scan_helper_gitdir_target_exists() {
113
102
  printf '0\n'
114
103
  return 0
115
104
  fi
116
- if ! scan_helper_is_absolute_path "$target"; then
105
+ if ! cleanup_wt_is_absolute_path "$target"; then
117
106
  target="$dir/$target"
118
107
  fi
119
108
  if scan_helper_target_present "$target"; then