@danmoisan/drm-copilot-mcp 1.1.18 → 1.1.19

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 (206) hide show
  1. package/out/mcp-server.js +989 -662
  2. package/package.json +3 -2
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +11 -0
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +10 -0
  5. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +12 -4
  6. package/resources/claude-customizations/.claude/agents/epic-planner.md +5 -4
  7. package/resources/claude-customizations/.claude/agents/feature-review.md +6 -4
  8. package/resources/claude-customizations/.claude/agents/orchestrator.md +5 -4
  9. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +31 -13
  10. package/resources/claude-customizations/.claude/agents/parallel-planner.md +9 -6
  11. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -2
  12. package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -1
  13. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +7 -1
  14. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +7 -1
  15. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  16. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +111 -46
  17. package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +105 -0
  18. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +8 -1
  19. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +7 -1
  20. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +116 -59
  21. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +55 -23
  22. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +74 -75
  23. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +127 -35
  24. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +61 -57
  25. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +7 -1
  26. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +129 -39
  27. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +7 -1
  28. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +9 -3
  29. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +39 -51
  30. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  31. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  32. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  33. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +17 -12
  34. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +9 -3
  35. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +65 -50
  36. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +49 -42
  37. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +54 -72
  38. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  39. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +279 -0
  40. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +129 -75
  41. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.artifact-root.ps1 +233 -0
  42. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +224 -0
  43. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +69 -24
  44. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +29 -18
  45. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +10 -4
  46. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +14 -5
  47. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +8 -2
  48. package/resources/claude-customizations/.claude/hooks/feature-folder-resolution.ps1 +373 -0
  49. package/resources/claude-customizations/.claude/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  50. package/resources/claude-customizations/.claude/hooks/hook-command-heredoc.ps1 +119 -0
  51. package/resources/claude-customizations/.claude/hooks/hook-command-invocation-operands.ps1 +198 -0
  52. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +254 -255
  53. package/resources/claude-customizations/.claude/hooks/hook-command-payload-powershell.ps1 +201 -0
  54. package/resources/claude-customizations/.claude/hooks/hook-command-payload.ps1 +484 -0
  55. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +27 -110
  56. package/resources/claude-customizations/.claude/hooks/hook-dependency-guard.ps1 +114 -0
  57. package/resources/claude-customizations/.claude/hooks/hook-docs-only-diff.ps1 +493 -0
  58. package/resources/claude-customizations/.claude/hooks/subagentstop-envelope.ps1 +192 -0
  59. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +9 -3
  60. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +40 -42
  61. package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +66 -38
  62. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-metrics.ps1 +180 -0
  63. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +195 -0
  64. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage.ps1 +141 -395
  65. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-resolution.ps1 +311 -0
  66. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-routing.ps1 +170 -0
  67. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +115 -190
  68. package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +64 -32
  69. package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +39 -44
  70. package/resources/claude-customizations/.claude/hooks/validate-prd-feature-output.ps1 +43 -14
  71. package/resources/claude-customizations/.claude/hooks/validate-required-artifact-output.ps1 +66 -36
  72. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +31 -34
  73. package/resources/claude-customizations/.claude/lib/bash/mutate-parallel-run.sh +191 -0
  74. package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +13 -3
  75. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-cli.sh +152 -0
  76. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-ops.sh +193 -0
  77. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation.sh +322 -0
  78. package/resources/claude-customizations/.claude/lib/bash/remove-parallel-item.sh +146 -0
  79. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +23 -33
  80. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +86 -6
  81. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +91 -19
  82. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1 +346 -0
  83. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +24 -3
  84. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +79 -2
  85. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +3 -3
  86. package/resources/claude-customizations/.claude/rules/architecture-boundaries.md +5 -5
  87. package/resources/claude-customizations/.claude/rules/csharp.md +2 -2
  88. package/resources/claude-customizations/.claude/rules/general-code-change.md +1 -1
  89. package/resources/claude-customizations/.claude/rules/general-unit-test.md +3 -2
  90. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +37 -4
  91. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +50 -5
  92. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +47 -8
  93. package/resources/claude-customizations/.claude/rules/quality-tiers.md +11 -7
  94. package/resources/claude-customizations/.claude/rules/shell.md +10 -8
  95. package/resources/claude-customizations/.claude/rules/typescript.md +1 -1
  96. package/resources/claude-customizations/.claude/settings.json +3 -4
  97. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  98. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +5 -1
  99. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +1 -1
  100. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +2 -0
  101. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +2 -0
  102. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +33 -6
  103. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +1 -1
  104. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  105. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  106. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +4 -3
  107. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +14 -6
  108. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +52 -14
  109. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +22 -9
  110. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +79 -16
  111. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +63 -7
  112. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +35 -12
  113. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +3 -1
  114. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +1 -1
  115. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +2 -2
  116. package/resources/claude-customizations/pack-manifests/core.json +23 -0
  117. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  118. package/resources/codex-and-agents-customizations/.agents/skills/architecture-boundaries/SKILL.md +5 -5
  119. package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +11 -0
  120. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +4 -0
  121. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +10 -0
  122. package/resources/codex-and-agents-customizations/.agents/skills/csharp/SKILL.md +2 -2
  123. package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +2 -2
  124. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +8 -0
  125. package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +17 -1
  126. package/resources/codex-and-agents-customizations/.agents/skills/epic-run/SKILL.md +2 -1
  127. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  128. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +13 -5
  129. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +1 -1
  130. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +3 -2
  131. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +15 -6
  132. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +1 -1
  133. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +11 -7
  134. package/resources/codex-and-agents-customizations/.agents/skills/translate-claude-to-codex/SKILL.md +1 -1
  135. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +2 -2
  136. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +2 -2
  137. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -0
  138. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -0
  139. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -0
  140. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -0
  141. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -0
  142. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -0
  143. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +10 -0
  144. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +10 -0
  145. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +10 -0
  146. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +10 -0
  147. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +10 -0
  148. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +10 -0
  149. package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +2 -1
  150. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +6 -6
  151. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +2 -2
  152. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +2 -2
  153. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +2 -2
  154. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +2 -2
  155. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +2 -2
  156. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +2 -2
  157. package/resources/codex-and-agents-customizations/.codex/codex-web-setup.sh +318 -273
  158. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  159. package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +7 -1
  160. package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +7 -1
  161. package/resources/codex-and-agents-customizations/.codex/hooks/codex-epic-child-launch-attestation.ps1 +1 -1
  162. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  163. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +8 -2
  164. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +97 -44
  165. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +105 -0
  166. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +7 -1
  167. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +37 -5
  168. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +6 -0
  169. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-root-invocation.ps1 +7 -1
  170. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-wave-barrier.ps1 +7 -1
  171. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +54 -41
  172. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +7 -1
  173. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +35 -14
  174. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  175. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  176. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  177. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +24 -11
  178. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  179. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +13 -4
  180. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +8 -2
  181. package/resources/codex-and-agents-customizations/.codex/hooks/feature-folder-resolution.ps1 +373 -0
  182. package/resources/codex-and-agents-customizations/.codex/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  183. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-heredoc.ps1 +119 -0
  184. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation-operands.ps1 +198 -0
  185. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +254 -255
  186. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload-powershell.ps1 +201 -0
  187. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload.ps1 +484 -0
  188. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +27 -110
  189. package/resources/codex-and-agents-customizations/.codex/hooks/hook-dependency-guard.ps1 +114 -0
  190. package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +493 -0
  191. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +8 -2
  192. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +7 -1
  193. package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +24 -8
  194. package/resources/codex-and-agents-customizations/pack-manifests/core.json +9 -0
  195. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +2 -2
  196. package/resources/customizations/.github/instructions/csharp-code-change.instructions.md +4 -4
  197. package/resources/customizations/.github/instructions/csharp-unit-test.instructions.md +2 -2
  198. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  199. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +4 -0
  200. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  201. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +1 -1
  202. package/resources/powershell/PoshQC/PoshQC.Coverage.psm1 +314 -0
  203. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +25 -28
  204. package/resources/powershell/PoshQC/PoshQC.psm1 +1 -0
  205. package/resources/powershell/PoshQC/README.md +8 -2
  206. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -319
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env bash
2
+ # mutate-parallel-run.sh: destination-portable command-line entry point for the
3
+ # decision and audit-record steps of the parallel-add, parallel-close, and
4
+ # parallel-orchestrate skills (issue #890). It exists so a workspace that
5
+ # received the Claude customization payload can run those steps with nothing
6
+ # but bash -- no interpreter, no repository checkout.
7
+ #
8
+ # Usage:
9
+ # bash .claude/lib/bash/mutate-parallel-run.sh admit --candidate <key> \
10
+ # [--edges "<a>:<b> ..."] --in-flight "<k> ..." \
11
+ # --current-cohort-members "<k> ..."
12
+ # bash .claude/lib/bash/mutate-parallel-run.sh recolor --unstarted "<k> ..." \
13
+ # [--edges "<a>:<b> ..."] --pinned "<k> ..." --generation <g> \
14
+ # --current-cohort <c> --highest-pinned-cohort <h>
15
+ # bash .claude/lib/bash/mutate-parallel-run.sh add-entry --item <key> \
16
+ # --deferred true|false --generation <g> [--at <timestamp>]
17
+ # bash .claude/lib/bash/mutate-parallel-run.sh close --items "<k>:<state> ..."
18
+ # bash .claude/lib/bash/mutate-parallel-run.sh close-entry --generation <g> \
19
+ # [--at <timestamp>]
20
+ # bash .claude/lib/bash/mutate-parallel-run.sh requeue-entry --item <key> \
21
+ # --generation <g> [--at <timestamp>]
22
+ #
23
+ # `--edges` is optional; `--in-flight`, `--current-cohort-members`,
24
+ # `--unstarted`, `--pinned`, and `--items` accept an empty string. Integer
25
+ # tokens match `-?(0|[1-9][0-9]*)`. An `--items` token is an integer, a colon,
26
+ # and a `[A-Za-z_]+` state; a duplicate key is a usage error. `--at` defaults to
27
+ # the current UTC time as yyyy-MM-ddTHH-mm; the clock is read only when `--at`
28
+ # is omitted.
29
+ #
30
+ # Exit-code contract (identical to remove-parallel-item.sh):
31
+ # exit 0 success; one compact JSON object on stdout
32
+ # exit 1 rule rejection; the reference implementation's exact message as
33
+ # one stderr line
34
+ # exit 2 usage error; one stderr line beginning
35
+ # "PARALLEL_MUTATION_ERROR: usage error:"
36
+ # Argument validation runs first, then rule rejection, then output.
37
+ #
38
+ # Parity reference: scripts/dev_tools/parallel_mutation_protocol.py
39
+ # (decide_admission, recolor_unstarted, decide_close) and
40
+ # scripts/dev_tools/_parallel_mutation_entries.py (build_add_entry,
41
+ # build_close_entry, build_requeue_entry).
42
+ #
43
+ # Declared divergences (also listed in both parity lanes):
44
+ # 1. A duplicate key in `close --items` is a usage error (exit 2); the Python
45
+ # mapping would silently keep the last record for that key.
46
+ # 2. `at` is a caller-supplied string instead of a `datetime`.
47
+ # 3. Option abbreviations are rejected.
48
+ # 4. `admit` has no rejection branch, in either implementation.
49
+ set -euo pipefail
50
+
51
+ # Resolve this script's own directory so the libraries source regardless of cwd.
52
+ MPR_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
53
+ # shellcheck source=.claude/lib/bash/parallel-mutation-ops.sh
54
+ # shellcheck disable=SC1091
55
+ source "$MPR_SCRIPT_DIR/parallel-mutation-ops.sh"
56
+ # shellcheck source=.claude/lib/bash/parallel-mutation-cli.sh
57
+ # shellcheck disable=SC1091
58
+ source "$MPR_SCRIPT_DIR/parallel-mutation-cli.sh"
59
+
60
+ pc_enforce_c_locale
61
+
62
+ mpr_admit() {
63
+ # Run the admit subcommand.
64
+ local candidate edges in_flight members rc=0
65
+ pmcli_parse_options "candidate edges in-flight current-cohort-members" "$@"
66
+ pmcli_require candidate in-flight current-cohort-members
67
+ pmcli_integer "${PMCLI_OPTS[candidate]}" "--candidate"
68
+ candidate="$PMCLI_VALUE"
69
+ pmcli_edge_list "${PMCLI_OPTS[edges]-}"
70
+ edges="$PMCLI_VALUE"
71
+ pmcli_integer_list "${PMCLI_OPTS["in-flight"]}" "--in-flight"
72
+ in_flight="$PMCLI_VALUE"
73
+ pmcli_integer_list "${PMCLI_OPTS["current-cohort-members"]}" "--current-cohort-members"
74
+ members="$PMCLI_VALUE"
75
+ pm_decide_admission "$candidate" "$edges" "$in_flight" "$members" || rc=$?
76
+ pmcli_emit "$rc"
77
+ }
78
+
79
+ mpr_recolor() {
80
+ # Run the recolor subcommand; same options as remove-parallel-item.sh.
81
+ local unstarted edges pinned generation current highest rc=0
82
+ pmcli_parse_options "unstarted edges pinned generation current-cohort highest-pinned-cohort" "$@"
83
+ pmcli_require unstarted pinned generation current-cohort highest-pinned-cohort
84
+ pmcli_integer_list "${PMCLI_OPTS[unstarted]}" "--unstarted"
85
+ unstarted="$PMCLI_VALUE"
86
+ pmcli_edge_list "${PMCLI_OPTS[edges]-}"
87
+ edges="$PMCLI_VALUE"
88
+ pmcli_integer_list "${PMCLI_OPTS[pinned]}" "--pinned"
89
+ pinned="$PMCLI_VALUE"
90
+ pmcli_integer "${PMCLI_OPTS[generation]}" "--generation"
91
+ generation="$PMCLI_VALUE"
92
+ pmcli_integer "${PMCLI_OPTS["current-cohort"]}" "--current-cohort"
93
+ current="$PMCLI_VALUE"
94
+ pmcli_integer "${PMCLI_OPTS["highest-pinned-cohort"]}" "--highest-pinned-cohort"
95
+ highest="$PMCLI_VALUE"
96
+ pm_recolor_unstarted "$unstarted" "$edges" "$pinned" "$generation" "$current" "$highest" || rc=$?
97
+ pmcli_emit "$rc"
98
+ }
99
+
100
+ mpr_add_entry() {
101
+ # Run the add-entry subcommand.
102
+ local item deferred generation at rc=0
103
+ pmcli_parse_options "item deferred generation at" "$@"
104
+ pmcli_require item deferred generation
105
+ pmcli_integer "${PMCLI_OPTS[item]}" "--item"
106
+ item="$PMCLI_VALUE"
107
+ deferred="${PMCLI_OPTS[deferred]}"
108
+ [[ $deferred == true || $deferred == false ]] ||
109
+ pmcli_usage_error "--deferred must be true or false; found: $deferred"
110
+ pmcli_integer "${PMCLI_OPTS[generation]}" "--generation"
111
+ generation="$PMCLI_VALUE"
112
+ pmcli_timestamp
113
+ at="$PMCLI_VALUE"
114
+ pm_build_add_entry "$item" "$deferred" "$generation" "$at" || rc=$?
115
+ pmcli_emit "$rc"
116
+ }
117
+
118
+ mpr_close() {
119
+ # Run the close subcommand over every `<key>:<state>` token.
120
+ local token key state canonical="" seen=" " rc=0
121
+ pmcli_parse_options "items" "$@"
122
+ pmcli_require items
123
+ pcoh_split_words "${PMCLI_OPTS[items]}"
124
+ local -a tokens=("${PCOH_WORDS[@]}")
125
+ for token in "${tokens[@]}"; do
126
+ [[ $token =~ ^(-?(0|[1-9][0-9]*)):([A-Za-z_]+)$ ]] ||
127
+ pmcli_usage_error "--items token must be <key>:<state>; found: $token"
128
+ state="${BASH_REMATCH[3]}"
129
+ key=$((BASH_REMATCH[1]))
130
+ # A duplicate key would let one record silently replace another, so it
131
+ # is rejected before the library runs (declared divergence 1).
132
+ [[ $seen != *" $key "* ]] || pmcli_usage_error "--items key given more than once: $key"
133
+ seen="$seen$key "
134
+ canonical="$canonical $key:$state"
135
+ done
136
+ pm_decide_close "${canonical# }" || rc=$?
137
+ pmcli_emit "$rc"
138
+ }
139
+
140
+ mpr_close_entry() {
141
+ # Run the close-entry subcommand.
142
+ local generation at rc=0
143
+ pmcli_parse_options "generation at" "$@"
144
+ pmcli_require generation
145
+ pmcli_integer "${PMCLI_OPTS[generation]}" "--generation"
146
+ generation="$PMCLI_VALUE"
147
+ pmcli_timestamp
148
+ at="$PMCLI_VALUE"
149
+ pm_build_close_entry "$generation" "$at" || rc=$?
150
+ pmcli_emit "$rc"
151
+ }
152
+
153
+ mpr_requeue_entry() {
154
+ # Run the requeue-entry subcommand.
155
+ local item generation at rc=0
156
+ pmcli_parse_options "item generation at" "$@"
157
+ pmcli_require item generation
158
+ pmcli_integer "${PMCLI_OPTS[item]}" "--item"
159
+ item="$PMCLI_VALUE"
160
+ pmcli_integer "${PMCLI_OPTS[generation]}" "--generation"
161
+ generation="$PMCLI_VALUE"
162
+ pmcli_timestamp
163
+ at="$PMCLI_VALUE"
164
+ pm_build_requeue_entry "$item" "$generation" "$at" || rc=$?
165
+ pmcli_emit "$rc"
166
+ }
167
+
168
+ mpr_main() {
169
+ # Dispatch on the subcommand.
170
+ (($# > 0)) ||
171
+ pmcli_usage_error "missing subcommand; expected one of admit, recolor, add-entry, close, close-entry, requeue-entry"
172
+ local subcommand="$1"
173
+ shift
174
+ case "$subcommand" in
175
+ admit) mpr_admit "$@" ;;
176
+ recolor) mpr_recolor "$@" ;;
177
+ add-entry) mpr_add_entry "$@" ;;
178
+ close) mpr_close "$@" ;;
179
+ close-entry) mpr_close_entry "$@" ;;
180
+ requeue-entry) mpr_requeue_entry "$@" ;;
181
+ *) pmcli_usage_error "unknown subcommand: $subcommand" ;;
182
+ esac
183
+ }
184
+
185
+ # Guard so the file can be sourced without executing main. main's return code
186
+ # is captured and re-exited explicitly as the final statement.
187
+ if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
188
+ mpr_rc=0
189
+ mpr_main "$@" || mpr_rc=$?
190
+ exit "$mpr_rc"
191
+ fi
@@ -52,14 +52,19 @@ PCOH_RESULT=""
52
52
  PCOH_WORDS=()
53
53
 
54
54
  pcoh_split_words() {
55
- # Populate PCOH_WORDS by splitting a space-separated string.
55
+ # Populate PCOH_WORDS by splitting a whitespace-separated string.
56
56
  #
57
57
  # Args: $1 = the string to split. An empty or whitespace-only string yields
58
58
  # an empty array, which is the empty-graph and empty-cohort case. `read -ra`
59
59
  # is used rather than an unquoted expansion so a token is never subjected to
60
- # pathname expansion.
60
+ # pathname expansion. Newline, CR, VT, and FF are converted to spaces first:
61
+ # `read` consumes one newline-terminated record and splits it on IFS only, so
62
+ # without the conversion every token after the first newline is dropped and
63
+ # CR, VT, and FF are not separators. With it, the separator set is space, tab,
64
+ # LF, CR, VT, and FF, which matches Python str.split() for ASCII.
65
+ local text="${1-}"
61
66
  PCOH_WORDS=()
62
- read -ra PCOH_WORDS <<<"${1-}"
67
+ read -ra PCOH_WORDS <<<"${text//[$'\n\r\v\f']/ }"
63
68
  }
64
69
 
65
70
  pcoh_fail() {
@@ -123,9 +128,14 @@ pcoh_build_adjacency() {
123
128
  PCOH_ADJACENT=()
124
129
  pcoh_split_words "$keys"
125
130
  local -a key_list=("${PCOH_WORDS[@]}")
131
+ # Rebuild the membership haystack from the split tokens. The endpoint checks
132
+ # below match a space-delimited word, so they would miss a key that the caller
133
+ # separated with a newline, tab, CR, VT, or FF instead of a single space.
134
+ keys=""
126
135
  # Seed every declared key so an isolated vertex survives into the coloring
127
136
  # step; inferring vertices from the edge list alone would drop it.
128
137
  for key in "${key_list[@]}"; do
138
+ keys="$keys $key"
129
139
  PCOH_DEGREE["$key"]=0
130
140
  PCOH_NEIGHBORS["$key"]=""
131
141
  done
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env bash
2
+ # parallel-mutation-cli.sh: sourceable command-line helpers shared by the two
3
+ # parallel mutation entry points, remove-parallel-item.sh (issue #791) and
4
+ # mutate-parallel-run.sh (issue #890). It holds option parsing, required-option
5
+ # checks, integer, key-list, and edge-list validation, the `--at` timestamp, and
6
+ # the result printer, so both entry points apply one grammar and one exit-code
7
+ # contract.
8
+ #
9
+ # Grammar. Every option takes exactly one value. An unknown name, an
10
+ # abbreviation, a `--name=value` form, a repeated option, or a missing value is
11
+ # a usage error; a value beginning with `--` counts as missing. Integer tokens
12
+ # match `-?(0|[1-9][0-9]*)`. `--at` matches `[0-9A-Za-z:.+-]+` and defaults to
13
+ # the current UTC minute as yyyy-MM-ddTHH-mm.
14
+ #
15
+ # Exit-code contract carried by these helpers:
16
+ # exit 2 usage error; one stderr line beginning
17
+ # "PARALLEL_MUTATION_ERROR: usage error:" (pmcli_usage_error)
18
+ # exit 1 rule rejection; PM_ERROR printed as one stderr line (pmcli_emit)
19
+ # exit 0 success; PM_RESULT printed as one stdout line (pmcli_emit)
20
+ #
21
+ # Dependencies. pmcli_emit reads PM_RESULT and PM_ERROR, which
22
+ # parallel-mutation.sh owns; an entry point sources that library before calling
23
+ # it. The only external command is `date`, run solely when `--at` is omitted.
24
+ #
25
+ # shellcheck disable=SC2034
26
+ # SC2034 is disabled file-wide because PMCLI_OPTS and PMCLI_VALUE are written
27
+ # here and read by the two entry points, which shellcheck analyses separately.
28
+
29
+ # Resolve this file's own directory so its dependencies source regardless of
30
+ # the caller's working directory.
31
+ PMCLI_LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
32
+ # shellcheck source=.claude/lib/bash/parallel-cohorts.sh
33
+ # shellcheck disable=SC1091
34
+ source "$PMCLI_LIB_DIR/parallel-cohorts.sh"
35
+
36
+ # Parsed option values, keyed by option name without the leading dashes.
37
+ declare -gA PMCLI_OPTS=()
38
+
39
+ # The canonical value stored by the most recent validator.
40
+ PMCLI_VALUE=""
41
+
42
+ pmcli_usage_error() {
43
+ # Print one usage-error line on stderr and exit 2.
44
+ #
45
+ # Args: $1 = the specific reason.
46
+ printf 'PARALLEL_MUTATION_ERROR: usage error: %s\n' "$1" >&2
47
+ exit 2
48
+ }
49
+
50
+ pmcli_parse_options() {
51
+ # Parse `--name value` pairs into PMCLI_OPTS against an allowed-name list.
52
+ #
53
+ # Args: $1 = space-separated allowed option names, then the arguments.
54
+ local allowed="$1" name
55
+ shift
56
+ PMCLI_OPTS=()
57
+ while (($# > 0)); do
58
+ name="${1#--}"
59
+ if [[ $1 != --* ]] || ! pc_contains_word "$allowed" "$name"; then
60
+ pmcli_usage_error "unknown option: $1"
61
+ fi
62
+ if [[ -n ${PMCLI_OPTS["$name"]+set} ]]; then
63
+ pmcli_usage_error "option given more than once: $1"
64
+ fi
65
+ if (($# < 2)) || [[ $2 == --* ]]; then
66
+ pmcli_usage_error "option $1 requires a value"
67
+ fi
68
+ PMCLI_OPTS["$name"]="$2"
69
+ shift 2
70
+ done
71
+ }
72
+
73
+ pmcli_require() {
74
+ # Exit with a usage error unless every named option was supplied.
75
+ #
76
+ # Args: the option names that are required.
77
+ local name
78
+ for name in "$@"; do
79
+ [[ -n ${PMCLI_OPTS["$name"]+set} ]] || pmcli_usage_error "missing required option: --$name"
80
+ done
81
+ }
82
+
83
+ pmcli_integer() {
84
+ # Validate one integer token and store it in canonical form in PMCLI_VALUE.
85
+ #
86
+ # Args: $1 = the token, $2 = a label naming where it came from. The
87
+ # validators store their result in a global instead of echoing it, because
88
+ # a usage error raised inside a command substitution would exit only the
89
+ # subshell.
90
+ if [[ ! $1 =~ ^-?(0|[1-9][0-9]*)$ ]]; then
91
+ pmcli_usage_error "$2 must be a decimal integer matching -?(0|[1-9][0-9]*); found: $1"
92
+ fi
93
+ PMCLI_VALUE=$(($1))
94
+ }
95
+
96
+ pmcli_integer_list() {
97
+ # Validate a whitespace-separated key list; store it space separated.
98
+ #
99
+ # Args: $1 = the list, $2 = a label naming the option.
100
+ local token canonical=""
101
+ pcoh_split_words "$1"
102
+ local -a tokens=("${PCOH_WORDS[@]}")
103
+ for token in "${tokens[@]}"; do
104
+ pmcli_integer "$token" "$2 key"
105
+ canonical="$canonical $PMCLI_VALUE"
106
+ done
107
+ PMCLI_VALUE="${canonical# }"
108
+ }
109
+
110
+ pmcli_edge_list() {
111
+ # Validate a whitespace-separated `a:b` edge list; store it canonical.
112
+ #
113
+ # Args: $1 = the edge list.
114
+ local token first canonical=""
115
+ pcoh_split_words "$1"
116
+ local -a tokens=("${PCOH_WORDS[@]}")
117
+ for token in "${tokens[@]}"; do
118
+ if [[ $token != *:* || $token == *:*:* ]]; then
119
+ pmcli_usage_error "edge must be <a>:<b>; found: $token"
120
+ fi
121
+ pmcli_integer "${token%%:*}" "edge endpoint"
122
+ first="$PMCLI_VALUE"
123
+ pmcli_integer "${token#*:}" "edge endpoint"
124
+ canonical="$canonical $first:$PMCLI_VALUE"
125
+ done
126
+ PMCLI_VALUE="${canonical# }"
127
+ }
128
+
129
+ pmcli_timestamp() {
130
+ # Store the validated `--at` value, or the current UTC minute when omitted.
131
+ if [[ -n ${PMCLI_OPTS[at]+set} ]]; then
132
+ PMCLI_VALUE="${PMCLI_OPTS[at]}"
133
+ [[ $PMCLI_VALUE =~ ^[0-9A-Za-z:.+-]+$ ]] ||
134
+ pmcli_usage_error "--at must match [0-9A-Za-z:.+-]+; found: $PMCLI_VALUE"
135
+ else
136
+ # The only clock read of either entry point, taken only when --at is
137
+ # omitted.
138
+ PMCLI_VALUE=$(date -u +%Y-%m-%dT%H-%M)
139
+ fi
140
+ }
141
+
142
+ pmcli_emit() {
143
+ # Print the library outcome and return the matching exit code.
144
+ #
145
+ # Args: $1 = the library function's return code.
146
+ if (($1 != 0)); then
147
+ printf '%s\n' "$PM_ERROR" >&2
148
+ return 1
149
+ fi
150
+ printf '%s\n' "$PM_RESULT"
151
+ return 0
152
+ }
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env bash
2
+ # parallel-mutation-ops.sh: sourceable bash port of the add, close, and requeue
3
+ # functions of the parallel mutation engine (issue #890), so a workspace that
4
+ # received the Claude customization payload can decide an admission, gate a run
5
+ # close, and build the add, close, and requeue mutations[] records without an
6
+ # interpreter. It sources parallel-mutation.sh, which carries the removal
7
+ # functions and the shared message builders.
8
+ #
9
+ # Parity reference (the repository authority; this file reproduces its
10
+ # observable outputs and rejection messages byte for byte):
11
+ # decide_admission scripts/dev_tools/parallel_mutation_protocol.py
12
+ # decide_close scripts/dev_tools/parallel_mutation_protocol.py
13
+ # build_add_entry scripts/dev_tools/_parallel_mutation_entries.py
14
+ # build_close_entry scripts/dev_tools/_parallel_mutation_entries.py
15
+ # build_requeue_entry scripts/dev_tools/_parallel_mutation_entries.py
16
+ # ItemRecord, MutationEntry scripts/dev_tools/_parallel_mutation_models.py
17
+ # rejection messages scripts/dev_tools/_parallel_mutation_errors.py
18
+ #
19
+ # Declared divergences (also listed in both parity lanes):
20
+ # 1. A duplicate key in `close --items` is a usage error (exit 2) raised by
21
+ # the command line; the Python mapping would keep the last record.
22
+ # 2. `at` is a caller-supplied string instead of a `datetime`.
23
+ # 3. Option abbreviations are rejected.
24
+ # 4. `admit` has no rejection branch, in either implementation.
25
+ #
26
+ # Contract. Every public function is pure: no file I/O, no network access, no
27
+ # clock read, and no external utility beyond `sort`. Each sets PM_RESULT to one
28
+ # compact JSON object and returns 0 on success, or sets PM_ERROR to the exact
29
+ # reference message and returns 1 on a rule rejection. Integer arguments are
30
+ # expected in canonical decimal form; the entry point mutate-parallel-run.sh
31
+ # validates them before calling here. Entry validation order follows
32
+ # MutationEntry: the stamped recolor_generation first, then item_key.
33
+ #
34
+ # shellcheck disable=SC2034
35
+ # SC2034 is disabled file-wide because PM_RESULT and PM_ERROR are written here
36
+ # and read by the entry point mutate-parallel-run.sh and by the bats suites,
37
+ # which shellcheck analyses separately.
38
+
39
+ # Resolve this file's own directory so its dependencies source regardless of
40
+ # the caller's working directory.
41
+ PMOPS_LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
42
+ # shellcheck source=.claude/lib/bash/parallel-mutation.sh
43
+ # shellcheck disable=SC1091
44
+ source "$PMOPS_LIB_DIR/parallel-mutation.sh"
45
+
46
+ pm_decide_admission() {
47
+ # Decide whether a candidate joins the current cohort or is deferred.
48
+ #
49
+ # Args: $1 = candidate key, $2 = space-separated `a:b` conflict edges over
50
+ # all items, $3 = space-separated in-flight keys, $4 = space-separated
51
+ # current-cohort member keys. The candidate is deferred when any edge joins
52
+ # it to a key in the union of $3 and $4, in either endpoint position.
53
+ # Returns 0 with PM_RESULT holding the decision; admission never rejects.
54
+ local candidate="$1" edges="$2" edge first second outcome="admit_current_cohort"
55
+ local recompute=false
56
+ PM_RESULT=""
57
+ PM_ERROR=""
58
+ pcoh_split_words "$3 $4"
59
+ local blocking=" ${PCOH_WORDS[*]} "
60
+ pcoh_split_words "$edges"
61
+ local -a edge_list=("${PCOH_WORDS[@]}")
62
+ # Scan every edge for one joining the candidate to a blocking key; the
63
+ # first such edge settles the decision.
64
+ for edge in "${edge_list[@]}"; do
65
+ first="${edge%%:*}"
66
+ second="${edge#*:}"
67
+ if { [[ $first == "$candidate" ]] && [[ $blocking == *" $second "* ]]; } ||
68
+ { [[ $second == "$candidate" ]] && [[ $blocking == *" $first "* ]]; }; then
69
+ outcome="defer_and_recolor"
70
+ recompute=true
71
+ break
72
+ fi
73
+ done
74
+ PM_RESULT="{\"candidate\":$candidate,\"outcome\":\"$outcome\",\"triggers_recompute\":$recompute}"
75
+ return 0
76
+ }
77
+
78
+ pm_build_add_entry() {
79
+ # Build the single mutations[] record for an accepted add.
80
+ #
81
+ # Args: $1 = item key, $2 = `true` for a deferred add (stamps g + 1),
82
+ # otherwise `false` (stamps g), $3 = the generation before the add, $4 =
83
+ # the `at` timestamp string. Returns 0 with PM_RESULT holding the record in
84
+ # F3 field order, 1 with PM_ERROR holding the reference contract message.
85
+ local item_key="$1" deferred="$2" generation="$3" at="$4" stamped
86
+ PM_RESULT=""
87
+ PM_ERROR=""
88
+ stamped="$generation"
89
+ if [[ $deferred == true ]]; then
90
+ stamped=$((generation + 1))
91
+ fi
92
+ if ((stamped < 0)); then
93
+ pm_fail "$(pm_entry_contract_message recolor_generation "$stamped" add)"
94
+ return 1
95
+ fi
96
+ if ((item_key <= 0)); then
97
+ pm_fail "$(pm_entry_contract_message item_key "$item_key" add)"
98
+ return 1
99
+ fi
100
+ PM_RESULT="{\"op\":\"add\",\"item_key\":$item_key,\"at\":$(pm_json_string "$at"),\"prior_state\":null,\"new_state\":\"scheduled\",\"disposition\":null,\"recolor_generation\":$stamped}"
101
+ return 0
102
+ }
103
+
104
+ pm_in_flight_list() {
105
+ # Echo a key list as Python renders a list of ints, ascending.
106
+ #
107
+ # Args: $1 = space-separated keys, at least one.
108
+ local ordered key rendered=""
109
+ pcoh_split_words "$1"
110
+ ordered=$(printf '%s\n' "${PCOH_WORDS[@]}" | LC_ALL=C sort -n)
111
+ while IFS= read -r key; do
112
+ [[ -n $key ]] || continue
113
+ [[ -z $rendered ]] || rendered="$rendered, "
114
+ rendered="$rendered$key"
115
+ done <<<"$ordered"
116
+ printf '[%s]' "$rendered"
117
+ }
118
+
119
+ pm_decide_close() {
120
+ # Gate a run close on no item being in flight.
121
+ #
122
+ # Args: $1 = space-separated `<key>:<state>` tokens, one per tracked item.
123
+ # Each token is validated in supplied order as the reference ItemRecord
124
+ # constructor validates it: a non-positive key names nothing the run
125
+ # tracks, and an out-of-enum state is rejected with the item-state members.
126
+ # Returns 0 with PM_RESULT `{"close_permitted":true}`, or 1 with PM_ERROR
127
+ # naming every in-flight key ascending.
128
+ local token key state in_flight=""
129
+ PM_RESULT=""
130
+ PM_ERROR=""
131
+ pcoh_split_words "$1"
132
+ local -a tokens=("${PCOH_WORDS[@]}")
133
+ for token in "${tokens[@]}"; do
134
+ key="${token%%:*}"
135
+ state="${token#*:}"
136
+ if ((key <= 0)); then
137
+ pm_fail "$(pm_unknown_item_message "$key")"
138
+ return 1
139
+ fi
140
+ if ! pc_enum_members_contains "$PC_VALID_ITEM_STATES" "$state"; then
141
+ pm_fail "$PM_REJECTION_PREFIX item $key state $(pc_repr str "$state") is not one of $PC_VALID_ITEM_STATES."
142
+ return 1
143
+ fi
144
+ if [[ $state == "$PM_PINNED_STATE" ]]; then
145
+ in_flight="$in_flight $key"
146
+ fi
147
+ done
148
+ if [[ -n $in_flight ]]; then
149
+ pm_fail "$PM_REJECTION_PREFIX close requires no item in flight; still in flight: $(pm_in_flight_list "$in_flight")."
150
+ return 1
151
+ fi
152
+ PM_RESULT='{"close_permitted":true}'
153
+ return 0
154
+ }
155
+
156
+ pm_build_close_entry() {
157
+ # Build the single run-scoped mutations[] record for an accepted close.
158
+ #
159
+ # Args: $1 = the generation, stamped unchanged, $2 = the `at` timestamp
160
+ # string. Returns 0 with PM_RESULT holding the record, 1 with PM_ERROR when
161
+ # the generation is negative.
162
+ local generation="$1" at="$2"
163
+ PM_RESULT=""
164
+ PM_ERROR=""
165
+ if ((generation < 0)); then
166
+ pm_fail "$(pm_entry_contract_message recolor_generation "$generation" close)"
167
+ return 1
168
+ fi
169
+ PM_RESULT="{\"op\":\"close\",\"item_key\":null,\"at\":$(pm_json_string "$at"),\"prior_state\":null,\"new_state\":null,\"disposition\":null,\"recolor_generation\":$generation}"
170
+ return 0
171
+ }
172
+
173
+ pm_build_requeue_entry() {
174
+ # Build the single mutations[] record for a drift-induced requeue.
175
+ #
176
+ # Args: $1 = item key, $2 = the generation before the requeue (stamps
177
+ # g + 1), $3 = the `at` timestamp string. Returns 0 with PM_RESULT holding
178
+ # the record, 1 with PM_ERROR holding the reference contract message.
179
+ local item_key="$1" generation="$2" at="$3" stamped
180
+ PM_RESULT=""
181
+ PM_ERROR=""
182
+ stamped=$((generation + 1))
183
+ if ((stamped < 0)); then
184
+ pm_fail "$(pm_entry_contract_message recolor_generation "$stamped" requeue)"
185
+ return 1
186
+ fi
187
+ if ((item_key <= 0)); then
188
+ pm_fail "$(pm_entry_contract_message item_key "$item_key" requeue)"
189
+ return 1
190
+ fi
191
+ PM_RESULT="{\"op\":\"requeue\",\"item_key\":$item_key,\"at\":$(pm_json_string "$at"),\"prior_state\":\"$PM_PINNED_STATE\",\"new_state\":\"blocked\",\"disposition\":null,\"recolor_generation\":$stamped}"
192
+ return 0
193
+ }