@danmoisan/drm-copilot-mcp 1.1.17 → 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 (225) hide show
  1. package/out/mcp-server.js +1405 -684
  2. package/package.json +6 -4
  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 +16 -5
  8. package/resources/claude-customizations/.claude/agents/orchestrator.md +21 -7
  9. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +37 -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-lane-assertion.sh +4 -2
  76. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-cli.sh +152 -0
  77. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-ops.sh +193 -0
  78. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation.sh +322 -0
  79. package/resources/claude-customizations/.claude/lib/bash/remove-parallel-item.sh +146 -0
  80. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +23 -33
  81. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +86 -6
  82. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +91 -19
  83. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
  84. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1 +346 -0
  85. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +396 -0
  86. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
  87. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
  88. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
  89. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +79 -2
  90. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +3 -3
  91. package/resources/claude-customizations/.claude/rules/architecture-boundaries.md +5 -5
  92. package/resources/claude-customizations/.claude/rules/csharp.md +2 -2
  93. package/resources/claude-customizations/.claude/rules/general-code-change.md +1 -1
  94. package/resources/claude-customizations/.claude/rules/general-unit-test.md +3 -2
  95. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +153 -4
  96. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +50 -5
  97. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +47 -8
  98. package/resources/claude-customizations/.claude/rules/quality-tiers.md +11 -7
  99. package/resources/claude-customizations/.claude/rules/shell.md +10 -8
  100. package/resources/claude-customizations/.claude/rules/typescript.md +1 -1
  101. package/resources/claude-customizations/.claude/settings.json +3 -4
  102. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +9 -1
  103. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +7 -1
  104. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
  105. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
  106. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +1 -1
  107. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +171 -5
  108. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
  109. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
  110. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +6 -15
  111. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +45 -11
  112. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +1 -1
  113. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  114. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
  115. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +7 -4
  116. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +42 -16
  117. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +52 -14
  118. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +22 -9
  119. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +97 -19
  120. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +63 -7
  121. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +35 -12
  122. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +3 -1
  123. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +1 -1
  124. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
  125. package/resources/claude-customizations/pack-manifests/core.json +25 -0
  126. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +9 -1
  127. package/resources/codex-and-agents-customizations/.agents/skills/architecture-boundaries/SKILL.md +5 -5
  128. package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +11 -0
  129. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +4 -0
  130. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +10 -0
  131. package/resources/codex-and-agents-customizations/.agents/skills/csharp/SKILL.md +2 -2
  132. package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +2 -2
  133. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +13 -3
  134. package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +17 -1
  135. package/resources/codex-and-agents-customizations/.agents/skills/epic-run/SKILL.md +2 -1
  136. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  137. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  138. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
  139. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +16 -6
  140. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +1 -1
  141. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +3 -2
  142. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +43 -11
  143. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
  144. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +19 -4
  145. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +11 -7
  146. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
  147. package/resources/codex-and-agents-customizations/.agents/skills/translate-claude-to-codex/SKILL.md +1 -1
  148. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +2 -2
  149. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +2 -2
  150. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -0
  151. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -0
  152. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -0
  153. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -0
  154. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -0
  155. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -0
  156. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +10 -0
  157. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +10 -0
  158. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +10 -0
  159. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +10 -0
  160. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +10 -0
  161. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +10 -0
  162. package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +2 -1
  163. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +6 -6
  164. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
  165. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
  166. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
  167. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
  168. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
  169. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
  170. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +3 -3
  171. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +3 -3
  172. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +3 -3
  173. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +3 -3
  174. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +3 -3
  175. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +3 -3
  176. package/resources/codex-and-agents-customizations/.codex/codex-web-setup.sh +318 -273
  177. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  178. package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +7 -1
  179. package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +7 -1
  180. package/resources/codex-and-agents-customizations/.codex/hooks/codex-epic-child-launch-attestation.ps1 +1 -1
  181. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  182. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +8 -2
  183. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +97 -44
  184. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +105 -0
  185. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +7 -1
  186. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +37 -5
  187. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +6 -0
  188. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-root-invocation.ps1 +7 -1
  189. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-wave-barrier.ps1 +7 -1
  190. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +54 -41
  191. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +7 -1
  192. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +35 -14
  193. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  194. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  195. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  196. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +24 -11
  197. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  198. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +13 -4
  199. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +8 -2
  200. package/resources/codex-and-agents-customizations/.codex/hooks/feature-folder-resolution.ps1 +373 -0
  201. package/resources/codex-and-agents-customizations/.codex/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  202. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-heredoc.ps1 +119 -0
  203. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation-operands.ps1 +198 -0
  204. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +254 -255
  205. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload-powershell.ps1 +201 -0
  206. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload.ps1 +484 -0
  207. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +27 -110
  208. package/resources/codex-and-agents-customizations/.codex/hooks/hook-dependency-guard.ps1 +114 -0
  209. package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +493 -0
  210. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +8 -2
  211. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +7 -1
  212. package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +24 -8
  213. package/resources/codex-and-agents-customizations/pack-manifests/core.json +9 -0
  214. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +2 -2
  215. package/resources/customizations/.github/instructions/csharp-code-change.instructions.md +4 -4
  216. package/resources/customizations/.github/instructions/csharp-unit-test.instructions.md +2 -2
  217. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +9 -1
  218. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +4 -0
  219. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  220. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +1 -1
  221. package/resources/powershell/PoshQC/PoshQC.Coverage.psm1 +314 -0
  222. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +25 -28
  223. package/resources/powershell/PoshQC/PoshQC.psm1 +1 -0
  224. package/resources/powershell/PoshQC/README.md +8 -2
  225. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -315
@@ -3,18 +3,25 @@
3
3
  Structural command-invocation matcher shared by the Claude and Codex enforcement hooks.
4
4
  .DESCRIPTION
5
5
  Realizes Piece 3 of the D2 behavior contract and the retrieval surface of D12 in
6
- docs/features/active/2026-08-25-enforcement-hook-trigger-matches-whole-command-text-545/spec.md.
6
+ docs/features/active/2026-08-25-enforcement-hook-trigger-matches-whole-command-text-545/spec.md,
7
+ as revised by issue #824.
7
8
 
8
- Answers, per segment produced by hook-command-scanner.ps1, whether a command line
9
- INVOKES a named command word with a named subcommand path, rather than merely
10
- containing those words adjacently. It also retrieves the operands and flag values of
11
- the matched invocation, taken from the matched segment only.
9
+ Answers whether a command line INVOKES a named command word with a named subcommand
10
+ path, rather than merely containing those words. Every record produced by
11
+ Read-CommandLineInvocationSegment (top-level segments, wrapper payloads, substitution
12
+ bodies, and xargs-injected commands) is classified Structural, Indeterminate, or not a
13
+ match. An Indeterminate match is fail-closed for every consumer: it means the command may
14
+ invoke the governed command and its target cannot be read. Classification never rests on
15
+ substring containment; every presence test is whole-token (Test-CommandLineWordPresent).
12
16
 
13
17
  Pure string logic only: no disk, process, network, clock, or environment access. It is
14
18
  dot-sourced as: . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
15
19
  #>
16
20
 
17
21
  . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
22
+ . (Join-Path $PSScriptRoot 'hook-command-payload.ps1')
23
+ . (Join-Path $PSScriptRoot 'hook-command-payload-powershell.ps1')
24
+ . (Join-Path $PSScriptRoot 'hook-command-invocation-operands.ps1')
18
25
 
19
26
  # The transparent-wrapper set of D2 Piece 3 step 2. These prefix a command without changing
20
27
  # which command runs, so the structural matcher skips them. All five are also members of the
@@ -22,25 +29,30 @@
22
29
  # both mechanisms. Pinned by test through Get-CommandLineTransparentWrapperName (rule R6).
23
30
  $script:CommandLineTransparentWrapperNames = @('command', 'env', 'nohup', 'time', 'timeout')
24
31
 
25
- # The modeled global-option tables of D2 Piece 3 steps 3 and the D6/D10 gh surface. An option
26
- # in WithArgument consumes the following token as its value, or carries the value inline in
27
- # the '--name=value' form; an option in Standalone consumes only itself. A dash-leading token
28
- # in neither list is UNMODELED, and an unmodeled token between the command word and the
29
- # subcommand classifies as a match: over-classification only forces a checkpoint check,
30
- # whereas under-classification is a bypass. Pinned by test through
32
+ # The modeled global-option tables of D2 Piece 3 step 3, the D6/D10 gh surface, and the
33
+ # issue #824 Terminal lists. An option in WithArgument consumes the following token as its
34
+ # value, or carries the value inline in the '--name=value' form; an option in Standalone
35
+ # consumes only itself ('--exec-path=<value>' is one token, so it is Standalone); a bare
36
+ # option in Terminal makes the command print and exit, so the segment is a resolved
37
+ # non-match. A dash-leading token in none of the lists is UNMODELED: its arity is unknown, so
38
+ # an unmodeled token between the command word and the subcommand classifies the segment
39
+ # Indeterminate, which every consumer treats fail-closed. Pinned by test through
31
40
  # Get-CommandLineGlobalOption (rule R6).
32
41
  $script:CommandLineGlobalOptions = @{
33
42
  git = [pscustomobject]@{
34
- WithArgument = @('-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path')
35
- Standalone = @('-p', '--paginate', '--no-pager', '--literal-pathspecs', '--no-optional-locks', '--bare')
43
+ WithArgument = @('-C', '-c', '--git-dir', '--work-tree', '--namespace')
44
+ Standalone = @('-p', '--paginate', '--no-pager', '--literal-pathspecs', '--no-optional-locks', '--bare', '--exec-path')
45
+ Terminal = @('--version', '-v', '--help', '-h', '--html-path', '--man-path', '--info-path', '--exec-path')
36
46
  }
37
47
  gh = [pscustomobject]@{
38
48
  WithArgument = @('-R', '--repo')
39
49
  Standalone = @()
50
+ Terminal = @('--version', '--help')
40
51
  }
41
52
  npx = [pscustomobject]@{
42
53
  WithArgument = @('-p', '--package', '-c', '--call', '--node-arg', '--userconfig')
43
54
  Standalone = @('-y', '--yes', '--no-install', '--ignore-existing', '-q', '--quiet')
55
+ Terminal = @()
44
56
  }
45
57
  }
46
58
 
@@ -69,11 +81,10 @@ function Get-CommandLineGlobalOption {
69
81
  .SYNOPSIS
70
82
  Return the modeled global-option table for one command word.
71
83
  .DESCRIPTION
72
- Returns a record carrying two string arrays: WithArgument, whose members consume a
73
- following token or an inline '=value', and Standalone, whose members consume only
74
- themselves. An unrecognized command word yields a record with two empty arrays, so
75
- every dash-leading token for that command word is treated as unmodeled and therefore
76
- classifies - the fail-closed direction.
84
+ Returns a record carrying three string arrays: WithArgument, Standalone, and Terminal.
85
+ An unrecognized command word yields three empty arrays, so every dash-leading token
86
+ for that command word is unmodeled and classifies Indeterminate - the fail-closed
87
+ direction.
77
88
  .OUTPUTS
78
89
  System.Management.Automation.PSCustomObject
79
90
  #>
@@ -86,32 +97,40 @@ function Get-CommandLineGlobalOption {
86
97
  return $script:CommandLineGlobalOptions[$key]
87
98
  }
88
99
 
89
- return [pscustomobject]@{ WithArgument = @(); Standalone = @() }
100
+ return [pscustomobject]@{ WithArgument = @(); Standalone = @(); Terminal = @() }
90
101
  }
91
102
 
92
- function Test-CommandLineRawContainment {
103
+ function Test-CommandLineTokenLiteral {
93
104
  <#
94
105
  .SYNOPSIS
95
- Report whether a raw text contains the command word and every subcommand element.
96
- .DESCRIPTION
97
- The "in any arrangement" test D12 specifies for a wrapper-led or live-substitution
98
- segment. Comparison is ordinal case-insensitive containment, which is deliberately
99
- loose: a false positive only forces a checkpoint check, a false negative is a bypass.
106
+ Report whether a token is literal: no '$' or backtick, and not led by '(', '@', or '{'.
107
+ .OUTPUTS
108
+ System.Boolean
109
+ #>
110
+ [CmdletBinding()]
111
+ [OutputType([bool])]
112
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Token)
113
+
114
+ if ($Token.Contains('$') -or $Token.Contains('`')) { return $false }
115
+ return -not ($Token.StartsWith('(') -or $Token.StartsWith('@') -or $Token.StartsWith('{'))
116
+ }
117
+
118
+ function Test-CommandLineAllWordPresent {
119
+ <#
120
+ .SYNOPSIS
121
+ Report whether every word passes Test-CommandLineWordPresent on one text.
100
122
  .OUTPUTS
101
123
  System.Boolean
102
124
  #>
103
125
  [CmdletBinding()]
104
126
  [OutputType([bool])]
105
127
  param(
106
- [Parameter(Mandatory)][AllowEmptyString()][string] $RawText,
107
- [Parameter(Mandatory)][string] $CommandWord,
108
- [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]] $SubcommandPath
128
+ [Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $Text,
129
+ [Parameter(Mandatory)][string[]] $Word
109
130
  )
110
131
 
111
- $comparison = [System.StringComparison]::OrdinalIgnoreCase
112
- if ($RawText.IndexOf($CommandWord, $comparison) -lt 0) { return $false }
113
- foreach ($element in $SubcommandPath) {
114
- if ($RawText.IndexOf($element, $comparison) -lt 0) { return $false }
132
+ foreach ($current in $Word) {
133
+ if (-not (Test-CommandLineWordPresent -RawText $Text -Word $current)) { return $false }
115
134
  }
116
135
  return $true
117
136
  }
@@ -121,9 +140,10 @@ function Skip-CommandLineOption {
121
140
  .SYNOPSIS
122
141
  Absorb modeled options from a token list and report where they end.
123
142
  .DESCRIPTION
124
- Returns Index, the position of the first token that is not a modeled option, and
125
- Unmodeled, set when a dash-leading token appears in neither list of the supplied
126
- table. A bare '-' and a bare '--' are not options and stop the absorption.
143
+ Returns Index, the position of the first token that is not a modeled option; Terminal,
144
+ set when a bare token (no '=') is in the Terminal list, which is tested before any
145
+ other list; Unmodeled, set when a dash-leading token appears in no list; and Options,
146
+ the ordered {Name, Value} pairs absorbed. A bare '-' and a bare '--' stop absorption.
127
147
  .OUTPUTS
128
148
  System.Management.Automation.PSCustomObject
129
149
  #>
@@ -136,300 +156,290 @@ function Skip-CommandLineOption {
136
156
  )
137
157
 
138
158
  $index = $StartIndex
159
+ $options = [System.Collections.Generic.List[pscustomobject]]::new()
139
160
  while ($index -lt $Token.Count) {
140
161
  $current = $Token[$index]
141
162
  if (-not $current.StartsWith('-') -or $current -eq '-' -or $current -eq '--') { break }
142
163
 
143
164
  $name = $current
144
- $hasInlineValue = $false
165
+ $value = $null
145
166
  $split = $current.IndexOf('=')
146
167
  if ($split -gt 0) {
147
168
  $name = $current.Substring(0, $split)
148
- $hasInlineValue = $true
169
+ $value = $current.Substring($split + 1)
170
+ } elseif (@($Option.Terminal) -contains $current) {
171
+ return [pscustomobject]@{ Index = $index; Unmodeled = $false; Terminal = $true; Options = $options.ToArray() }
149
172
  }
150
173
 
151
174
  if ($Option.WithArgument -contains $name) {
152
- $index += if ($hasInlineValue) { 1 } else { 2 }
175
+ if ($null -eq $value) {
176
+ $value = if ($index + 1 -lt $Token.Count) { $Token[$index + 1] } else { $null }
177
+ $index++
178
+ }
179
+ $options.Add([pscustomobject]@{ Name = $name; Value = $value })
180
+ $index++
153
181
  continue
154
182
  }
155
183
  if ($Option.Standalone -contains $name) {
184
+ $options.Add([pscustomobject]@{ Name = $name; Value = $value })
156
185
  $index++
157
186
  continue
158
187
  }
159
188
 
160
- return [pscustomobject]@{ Index = $index; Unmodeled = $true }
189
+ return [pscustomobject]@{ Index = $index; Unmodeled = $true; Terminal = $false; Options = $options.ToArray() }
161
190
  }
162
191
 
163
- return [pscustomobject]@{ Index = $index; Unmodeled = $false }
192
+ return [pscustomobject]@{ Index = $index; Unmodeled = $false; Terminal = $false; Options = $options.ToArray() }
164
193
  }
165
194
 
166
- function Resolve-CommandLineInvocation {
195
+ function Get-CommandLineStructuralWalk {
167
196
  <#
168
197
  .SYNOPSIS
169
- Locate the first segment that invokes a command word with a subcommand path.
198
+ Walk one record's tokens for a command word and subcommand path (DC-10 rule 3).
170
199
  .DESCRIPTION
171
- Implements the four mandatory fail-closed rules of D12 in order, per segment:
172
- an Unbalanced segment classifies because its structure could not be resolved; a
173
- wrapper-led or live-substitution segment classifies when its RawText contains the
174
- command word and every subcommand element in any arrangement; an unmodeled
175
- dash-leading token between the command word and the subcommand classifies; and a
176
- non-dash token that is not the next expected subcommand element terminates that
177
- segment's scan without a match, so 'git log --grep add' does not classify.
178
-
179
- Returns $null when no segment matched. Otherwise returns Segment, the matched record,
180
- and OperandIndex, the token position just past the subcommand path - or -1 when the
181
- match was reached by a non-structural rule, in which case there is no operand
182
- position to report.
200
+ Skips VAR=value prefixes and transparent wrappers, leaf-compares the command word, and
201
+ absorbs modeled options before each subcommand element. Outcome is 'Match' (with
202
+ OperandIndex and GlobalOptions), 'Terminal', 'Unmodeled', or 'NoMatch'. A NoMatch
203
+ reports whether the command-word token or the subcommand-position token where the walk
204
+ stopped was non-literal, and whether a transparent wrapper was skipped.
183
205
  .OUTPUTS
184
206
  System.Management.Automation.PSCustomObject
185
207
  #>
186
208
  [CmdletBinding()]
187
209
  [OutputType([pscustomobject])]
188
210
  param(
189
- [Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $CommandText,
211
+ [Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Token,
190
212
  [Parameter(Mandatory)][string] $CommandWord,
191
- [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]] $SubcommandPath
213
+ [Parameter(Mandatory)][string[]] $SubcommandPath,
214
+ [Parameter(Mandatory)][pscustomobject] $Option
192
215
  )
193
216
 
194
- $option = Get-CommandLineGlobalOption -CommandWord $CommandWord
195
- $transparent = $script:CommandLineTransparentWrapperNames
196
-
197
- foreach ($segment in @(Read-CommandLineSegment -CommandText $CommandText)) {
198
- if ($segment.Unbalanced) {
199
- return [pscustomobject]@{ Segment = $segment; OperandIndex = -1 }
200
- }
201
-
202
- if (($segment.IsWrapperLed -or $segment.HasLiveSubstitution) -and
203
- (Test-CommandLineRawContainment -RawText $segment.RawText -CommandWord $CommandWord -SubcommandPath $SubcommandPath)) {
204
- return [pscustomobject]@{ Segment = $segment; OperandIndex = -1 }
205
- }
217
+ $result = [pscustomobject]@{
218
+ Outcome = 'NoMatch'; OperandIndex = -1; GlobalOptions = [pscustomobject[]]@()
219
+ SkippedWrapper = $false; CommandWordNonLiteral = $false; SubcommandNonLiteral = $false
220
+ }
221
+ $skip = Skip-CommandLineTransparentWrapper -Token $Token -Except $CommandWord
222
+ $result.SkippedWrapper = $skip.SkippedWrapper
223
+ $index = $skip.Index
224
+ if ($index -ge $Token.Count) { return $result }
225
+ if (-not (Test-CommandLineTokenLiteral -Token $Token[$index])) { $result.CommandWordNonLiteral = $true; return $result }
226
+ if ((ConvertTo-CommandLineLeafWord -Word $Token[$index]) -ne $CommandWord) { return $result }
227
+ $index++
228
+
229
+ $globalOptions = [System.Collections.Generic.List[pscustomobject]]::new()
230
+ foreach ($element in $SubcommandPath) {
231
+ $skipOption = Skip-CommandLineOption -Token $Token -StartIndex $index -Option $Option
232
+ foreach ($absorbed in @($skipOption.Options)) { $globalOptions.Add($absorbed) }
233
+ if ($skipOption.Terminal) { $result.Outcome = 'Terminal'; return $result }
234
+ if ($skipOption.Unmodeled) { $result.Outcome = 'Unmodeled'; return $result }
235
+ $index = $skipOption.Index
236
+ if ($index -ge $Token.Count) { return $result }
237
+ if (-not (Test-CommandLineTokenLiteral -Token $Token[$index])) { $result.SubcommandNonLiteral = $true; return $result }
238
+ if ($Token[$index] -ne $element) { return $result }
239
+ $index++
240
+ }
206
241
 
207
- $tokens = @($segment.Tokens)
208
- $index = 0
209
- while ($index -lt $tokens.Count -and (
210
- $tokens[$index] -match '^[A-Za-z_][A-Za-z0-9_]*=' -or
211
- ($transparent -contains $tokens[$index] -and $tokens[$index] -ne $CommandWord))) {
212
- $index++
213
- }
242
+ $result.Outcome = 'Match'
243
+ $result.OperandIndex = $index
244
+ $result.GlobalOptions = $globalOptions.ToArray()
245
+ return $result
246
+ }
214
247
 
215
- if ($index -ge $tokens.Count -or $tokens[$index] -ne $CommandWord) { continue }
216
- $index++
248
+ function Get-CommandLineInvocationOperand {
249
+ <#
250
+ .SYNOPSIS
251
+ Collect the operands after a structural match and report whether they are complete (DC-12).
252
+ .DESCRIPTION
253
+ OperandsComplete is $false for an argument-injected record, for any non-literal operand,
254
+ for a redirection after the subcommand (a bare operator also consumes its target), and
255
+ for an unmodeled dash token followed by another token.
256
+ .OUTPUTS
257
+ System.Management.Automation.PSCustomObject with Operands and Complete.
258
+ #>
259
+ [CmdletBinding()]
260
+ [OutputType([pscustomobject])]
261
+ param(
262
+ [Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Token,
263
+ [Parameter(Mandatory)][int] $StartIndex,
264
+ [Parameter(Mandatory)][pscustomobject] $Option,
265
+ [Parameter(Mandatory)][bool] $ArgumentInjected
266
+ )
217
267
 
218
- $matched = $true
219
- foreach ($element in $SubcommandPath) {
220
- $skip = Skip-CommandLineOption -Token $tokens -StartIndex $index -Option $option
221
- if ($skip.Unmodeled) {
222
- return [pscustomobject]@{ Segment = $segment; OperandIndex = -1 }
223
- }
224
- $index = $skip.Index
225
- if ($index -ge $tokens.Count -or $tokens[$index] -ne $element) {
226
- $matched = $false
227
- break
228
- }
229
- $index++
268
+ $operands = [System.Collections.Generic.List[string]]::new()
269
+ $complete = -not $ArgumentInjected
270
+ $afterSeparator = $false
271
+ $index = $StartIndex
272
+ while ($index -lt $Token.Count) {
273
+ $current = $Token[$index]
274
+ if (-not $afterSeparator -and $current -match '^(<|>|\d*>|&>)') {
275
+ $complete = $false
276
+ $index += if ($current -match '^(\d*[<>]{1,2}&?|&>{1,2})$') { 2 } else { 1 }
277
+ continue
230
278
  }
231
-
232
- if ($matched) {
233
- return [pscustomobject]@{ Segment = $segment; OperandIndex = $index }
279
+ if ($afterSeparator) { $operands.Add($current); $index++; continue }
280
+ if ($current -eq '--') { $afterSeparator = $true; $index++; continue }
281
+ if ($current.StartsWith('-') -and $current.Length -gt 1) {
282
+ $name = $current
283
+ $hasInlineValue = $false
284
+ $split = $current.IndexOf('=')
285
+ if ($split -gt 0) { $name = $current.Substring(0, $split); $hasInlineValue = $true }
286
+ if ($Option.WithArgument -contains $name) { $index += if ($hasInlineValue) { 1 } else { 2 }; continue }
287
+ if ($Option.Standalone -contains $name -or $script:CommandLineStandaloneFlagNames -contains $name) { $index++; continue }
288
+ if ($index + 1 -lt $Token.Count) { $complete = $false }
289
+ break
234
290
  }
291
+ $operands.Add($current)
292
+ $index++
235
293
  }
236
-
237
- return $null
294
+ foreach ($operand in $operands) {
295
+ if (-not (Test-CommandLineTokenLiteral -Token $operand)) { $complete = $false }
296
+ }
297
+ return [pscustomobject]@{ Operands = [string[]]$operands.ToArray(); Complete = $complete }
238
298
  }
239
299
 
240
- function Test-CommandLineInvocation {
300
+ function ConvertTo-CommandLineIndeterminateMatch {
241
301
  <#
242
302
  .SYNOPSIS
243
- Report whether a command line invokes a named command word with a named
244
- subcommand path, structurally rather than by raw-text adjacency.
245
- .DESCRIPTION
246
- Scans every segment produced by Read-CommandLineSegment and applies the four
247
- mandatory fail-closed rules of D12, which are stated in full on
248
- Resolve-CommandLineInvocation. This predicate is the replacement for every
249
- raw-text trigger regex that asked whether a governed command runs.
250
- .PARAMETER CommandText
251
- The raw Bash command text.
252
- .PARAMETER CommandWord
253
- The command name, e.g. 'git' or 'gh'. Compared case-insensitively.
254
- .PARAMETER SubcommandPath
255
- One or more ordered subcommand tokens, e.g. @('worktree','remove'), @('pr','merge'),
256
- @('issue','create'), @('add'). Compared case-insensitively.
303
+ Build an Indeterminate match record: no operand position, no operands, incomplete.
257
304
  .OUTPUTS
258
- System.Boolean
305
+ System.Management.Automation.PSCustomObject
259
306
  #>
260
307
  [CmdletBinding()]
261
- [OutputType([bool])]
308
+ [OutputType([pscustomobject])]
262
309
  param(
263
- [Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $CommandText,
264
- [Parameter(Mandatory)][string] $CommandWord,
265
- [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]] $SubcommandPath
310
+ [Parameter(Mandatory)][pscustomobject] $Record,
311
+ [Parameter(Mandatory)][string] $Reason
266
312
  )
267
313
 
268
- return $null -ne (Resolve-CommandLineInvocation -CommandText $CommandText -CommandWord $CommandWord -SubcommandPath $SubcommandPath)
314
+ return [pscustomobject]@{
315
+ Status = 'Indeterminate'; Reason = $Reason; Segment = $Record; OperandIndex = -1
316
+ GlobalOptions = [pscustomobject[]]@(); Operands = [string[]]@(); OperandsComplete = $false
317
+ }
269
318
  }
270
319
 
271
- function Get-CommandLineOperand {
320
+ function Get-CommandLineInvocation {
272
321
  <#
273
322
  .SYNOPSIS
274
- Return the positional operands of a matched invocation, in source order.
323
+ Return every match of a command word and subcommand path, in source order.
275
324
  .DESCRIPTION
276
- Locates the first segment in which CommandWord + SubcommandPath match STRUCTURALLY,
277
- then returns the non-option tokens that follow the subcommand path in that segment
278
- only. Modeled option-with-argument pairs are consumed, so '-C <arg>' consumes both
279
- tokens; known zero-argument flags such as '--force' contribute nothing; a token after
280
- a bare '--' separator is always an operand; and a dash-leading token that is not
281
- modeled terminates collection, because its arity is unknown and a wrong guess would
282
- silently return a flag as a path.
283
-
284
- The operand list comes from the MATCHED segment only, so a 'cd <path>' segment chained
285
- before the invocation contributes nothing. That is the direct fix for issue #591. A
286
- match reached by a non-structural fail-closed rule reports no operand position, and
287
- yields no operands.
288
-
289
- Callers receive the result through the @(...) array form used at every call site, so
290
- the no-operand case arrives as an empty array rather than as $null.
291
- .PARAMETER CommandText
292
- The raw Bash command text.
293
- .PARAMETER CommandWord
294
- The command name.
295
- .PARAMETER SubcommandPath
296
- The ordered subcommand tokens.
325
+ For each iterator record the first applicable rule wins (DC-10): Unbalanced, decode
326
+ failure, and depth limit are Indeterminate; a structural walk that reaches a Terminal
327
+ option is no match, one that meets an unmodeled option is Indeterminate (Opaque), and
328
+ one that matches the full path is Structural. Otherwise a wrapper with no payload or a
329
+ transparent-wrapper-led record whose own RawText carries every word as a whole token
330
+ is Indeterminate (Opaque); a non-literal command word or subcommand token is
331
+ Indeterminate (DynamicPosition) when the whole command text carries every word; and a
332
+ payload, substitution, or injected record is Indeterminate (NotProvenInert) when its
333
+ PresenceText carries every word and its root's payload is not proven inert, at most
334
+ once per root.
297
335
  .OUTPUTS
298
- System.String[] - operands with balanced quotes already stripped, in source order.
336
+ System.Management.Automation.PSCustomObject[] - each carries Status ('Structural' or
337
+ 'Indeterminate'), Reason ('', 'Unbalanced', 'Opaque', 'DynamicPosition',
338
+ 'NotProvenInert', 'DepthLimit'), Segment (the iterator record), OperandIndex (-1 when
339
+ Indeterminate), GlobalOptions, Operands, and OperandsComplete.
299
340
  #>
300
341
  [CmdletBinding()]
301
- [OutputType([string[]])]
342
+ [OutputType([pscustomobject[]])]
302
343
  param(
303
344
  [Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $CommandText,
304
345
  [Parameter(Mandatory)][string] $CommandWord,
305
346
  [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]] $SubcommandPath
306
347
  )
307
348
 
308
- $operands = [System.Collections.Generic.List[string]]::new()
309
- $resolved = Resolve-CommandLineInvocation -CommandText $CommandText -CommandWord $CommandWord -SubcommandPath $SubcommandPath
310
- if ($null -eq $resolved -or $resolved.OperandIndex -lt 0) {
311
- return [string[]]$operands.ToArray()
312
- }
313
-
349
+ $matchList = [System.Collections.Generic.List[pscustomobject]]::new()
350
+ $records = @(Read-CommandLineInvocationSegment -CommandText $CommandText)
351
+ if ($records.Count -eq 0) { return [pscustomobject[]]$matchList.ToArray() }
314
352
  $option = Get-CommandLineGlobalOption -CommandWord $CommandWord
315
- $tokens = @($resolved.Segment.Tokens)
316
- $index = $resolved.OperandIndex
317
- $afterSeparator = $false
318
-
319
- while ($index -lt $tokens.Count) {
320
- $current = $tokens[$index]
353
+ $words = [string[]](@($CommandWord) + $SubcommandPath)
354
+ $normalized = ConvertTo-CommandLineNormalizedText -CommandText $CommandText
355
+ $inertByRoot = @{}
356
+ $presenceRoots = [System.Collections.Generic.HashSet[int]]::new()
357
+
358
+ foreach ($record in $records) {
359
+ if ($record.Unbalanced) { $matchList.Add((ConvertTo-CommandLineIndeterminateMatch -Record $record -Reason 'Unbalanced')); continue }
360
+ if ($record.Opaque -and $record.OpaqueReason -eq 'DecodeFailure') { $matchList.Add((ConvertTo-CommandLineIndeterminateMatch -Record $record -Reason 'Opaque')); continue }
361
+ if ($record.Opaque -and $record.OpaqueReason -eq 'DepthLimit') { $matchList.Add((ConvertTo-CommandLineIndeterminateMatch -Record $record -Reason 'DepthLimit')); continue }
362
+
363
+ $tokens = [string[]]@($record.Tokens)
364
+ $walk = Get-CommandLineStructuralWalk -Token $tokens -CommandWord $CommandWord -SubcommandPath $SubcommandPath -Option $option
365
+ if ($walk.Outcome -eq 'Terminal') { continue }
366
+ if ($walk.Outcome -eq 'Unmodeled') { $matchList.Add((ConvertTo-CommandLineIndeterminateMatch -Record $record -Reason 'Opaque')); continue }
367
+ if ($walk.Outcome -eq 'Match') {
368
+ $collected = Get-CommandLineInvocationOperand -Token $tokens -StartIndex $walk.OperandIndex -Option $option -ArgumentInjected ([bool]$record.ArgumentInjected)
369
+ $matchList.Add([pscustomobject]@{
370
+ Status = 'Structural'; Reason = ''; Segment = $record; OperandIndex = $walk.OperandIndex
371
+ GlobalOptions = $walk.GlobalOptions; Operands = $collected.Operands; OperandsComplete = $collected.Complete
372
+ })
373
+ continue
374
+ }
321
375
 
322
- if ($afterSeparator) {
323
- $operands.Add($current)
324
- $index++
376
+ # A transparent-wrapper-led record that expanded a payload is judged by its payload records.
377
+ $hasChild = $record.Index + 1 -lt $records.Count -and $records[$record.Index + 1].Depth -gt $record.Depth
378
+ $opaqueLed = ($record.Opaque -and $record.OpaqueReason -eq 'NoPayload') -or ($walk.SkippedWrapper -and -not $hasChild)
379
+ if ($opaqueLed -and (Test-CommandLineAllWordPresent -Text $record.RawText -Word $words)) {
380
+ $matchList.Add((ConvertTo-CommandLineIndeterminateMatch -Record $record -Reason 'Opaque'))
325
381
  continue
326
382
  }
327
- if ($current -eq '--') {
328
- $afterSeparator = $true
329
- $index++
383
+ if (($walk.CommandWordNonLiteral -or $walk.SubcommandNonLiteral) -and (Test-CommandLineAllWordPresent -Text $normalized -Word $words)) {
384
+ $matchList.Add((ConvertTo-CommandLineIndeterminateMatch -Record $record -Reason 'DynamicPosition'))
330
385
  continue
331
386
  }
332
-
333
- if ($current.StartsWith('-') -and $current.Length -gt 1) {
334
- $name = $current
335
- $hasInlineValue = $false
336
- $split = $current.IndexOf('=')
337
- if ($split -gt 0) {
338
- $name = $current.Substring(0, $split)
339
- $hasInlineValue = $true
340
- }
341
-
342
- if ($option.WithArgument -contains $name) {
343
- $index += if ($hasInlineValue) { 1 } else { 2 }
344
- continue
345
- }
346
- if ($option.Standalone -contains $name -or $script:CommandLineStandaloneFlagNames -contains $name) {
347
- $index++
348
- continue
349
- }
350
- break
387
+ if ($record.Origin -eq 'TopLevel' -or $presenceRoots.Contains([int]$record.RootIndex)) { continue }
388
+ if (-not (Test-CommandLineAllWordPresent -Text $record.PresenceText -Word $words)) { continue }
389
+ if (-not $inertByRoot.ContainsKey($record.RootIndex)) {
390
+ $inertByRoot[$record.RootIndex] = Test-CommandLinePayloadInert -Record $records -RootIndex $record.RootIndex
391
+ }
392
+ if (-not $inertByRoot[$record.RootIndex]) {
393
+ [void]$presenceRoots.Add([int]$record.RootIndex)
394
+ $matchList.Add((ConvertTo-CommandLineIndeterminateMatch -Record $record -Reason 'NotProvenInert'))
351
395
  }
352
-
353
- $operands.Add($current)
354
- $index++
355
396
  }
356
397
 
357
- return [string[]]$operands.ToArray()
398
+ return [pscustomobject[]]$matchList.ToArray()
358
399
  }
359
400
 
360
- function Get-CommandLineFlagValue {
401
+ function Resolve-CommandLineInvocation {
361
402
  <#
362
403
  .SYNOPSIS
363
- Return the value of a named flag belonging to a matched invocation.
404
+ Return the first match of a command word and subcommand path, or $null.
364
405
  .DESCRIPTION
365
- Locates the matched segment as Test-CommandLineInvocation does, then searches that
366
- segment's tokens for FlagName. Recognizes both the separated form ('--merge 688') and
367
- the equals form ('--merge=688'). Returns $null when the flag is absent, when the flag
368
- is present with no following value token, and when the following token is itself
369
- dash-leading.
370
-
371
- The $null-on-missing-value contract is mandatory and is pinned by issue #591's first
372
- constraint: downstream logic treats a missing explicit pull-request number as a
373
- fail-closed condition, so a bare 'gh pr merge --merge' must yield $null, never 0 and
374
- never ''.
375
- .PARAMETER CommandText
376
- The raw Bash command text.
377
- .PARAMETER CommandWord
378
- The command name.
379
- .PARAMETER SubcommandPath
380
- The ordered subcommand tokens.
381
- .PARAMETER FlagName
382
- The flag as written, including leading dashes, e.g. '--merge' or '--body-file'.
383
- Compared case-insensitively.
406
+ Returns Segment (the matched iterator record), OperandIndex (the token position just
407
+ past the subcommand path, or -1 when Status is 'Indeterminate'), and Status
408
+ ('Structural' or 'Indeterminate') for the first record Get-CommandLineInvocation
409
+ matched. An Indeterminate match means the record may invoke the command and its
410
+ structure could not be resolved; every consumer treats it fail-closed.
384
411
  .OUTPUTS
385
- System.String or $null. Quotes are already stripped. Callers that need an integer cast
386
- the result themselves after a $null check.
412
+ System.Management.Automation.PSCustomObject
387
413
  #>
388
414
  [CmdletBinding()]
389
- [OutputType([string])]
415
+ [OutputType([pscustomobject])]
390
416
  param(
391
417
  [Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $CommandText,
392
418
  [Parameter(Mandatory)][string] $CommandWord,
393
- [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]] $SubcommandPath,
394
- [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $FlagName
419
+ [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]] $SubcommandPath
395
420
  )
396
421
 
397
- $resolved = Resolve-CommandLineInvocation -CommandText $CommandText -CommandWord $CommandWord -SubcommandPath $SubcommandPath
398
- if ($null -eq $resolved) { return $null }
399
-
400
- $tokens = @($resolved.Segment.Tokens)
401
- $prefix = $FlagName + '='
402
- for ($index = 0; $index -lt $tokens.Count; $index++) {
403
- $current = $tokens[$index]
404
-
405
- if ($current -eq $FlagName) {
406
- if ($index + 1 -ge $tokens.Count) { return $null }
407
- $value = $tokens[$index + 1]
408
- if ($value.StartsWith('-') -and $value.Length -gt 1) { return $null }
409
- return $value
410
- }
411
-
412
- if ($current.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
413
- $value = $current.Substring($prefix.Length)
414
- if ([string]::IsNullOrEmpty($value)) { return $null }
415
- return $value
416
- }
417
- }
418
-
419
- return $null
422
+ $first = @(Get-CommandLineInvocation -CommandText $CommandText -CommandWord $CommandWord -SubcommandPath $SubcommandPath) | Select-Object -First 1
423
+ if ($null -eq $first) { return $null }
424
+ return [pscustomobject]@{ Segment = $first.Segment; OperandIndex = $first.OperandIndex; Status = $first.Status }
420
425
  }
421
426
 
422
- function Test-CommandLineFlag {
427
+ function Test-CommandLineInvocation {
423
428
  <#
424
429
  .SYNOPSIS
425
- Report whether a named flag is present on a matched invocation, regardless of whether
426
- it carries a value.
430
+ Report whether a command line invokes a named command word with a named
431
+ subcommand path, structurally rather than by raw-text adjacency.
427
432
  .DESCRIPTION
428
- Presence-only companion to Get-CommandLineFlagValue, for boolean flags such as
429
- '--merge', '--force', and '--body'. Matches both '--flag' and '--flag=value'. Exact
430
- token comparison after quote stripping: '--body' does not match '--body-file', which
431
- is the distinction the pr-author hook's negative lookahead currently expresses as
432
- '--body(?!-file)\b'.
433
+ True for any match, Structural or Indeterminate, as Get-CommandLineInvocation
434
+ defines them. This predicate is the replacement for every raw-text trigger regex
435
+ that asked whether a governed command runs.
436
+ .PARAMETER CommandText
437
+ The raw Bash command text.
438
+ .PARAMETER CommandWord
439
+ The command name, e.g. 'git' or 'gh'. Compared case-insensitively.
440
+ .PARAMETER SubcommandPath
441
+ One or more ordered subcommand tokens, e.g. @('worktree','remove'), @('pr','merge'),
442
+ @('issue','create'), @('add'). Compared case-insensitively.
433
443
  .OUTPUTS
434
444
  System.Boolean
435
445
  #>
@@ -438,20 +448,10 @@ function Test-CommandLineFlag {
438
448
  param(
439
449
  [Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $CommandText,
440
450
  [Parameter(Mandatory)][string] $CommandWord,
441
- [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]] $SubcommandPath,
442
- [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $FlagName
451
+ [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string[]] $SubcommandPath
443
452
  )
444
453
 
445
- $resolved = Resolve-CommandLineInvocation -CommandText $CommandText -CommandWord $CommandWord -SubcommandPath $SubcommandPath
446
- if ($null -eq $resolved) { return $false }
447
-
448
- $prefix = $FlagName + '='
449
- foreach ($current in @($resolved.Segment.Tokens)) {
450
- if ($current -eq $FlagName) { return $true }
451
- if ($current.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { return $true }
452
- }
453
-
454
- return $false
454
+ return @(Get-CommandLineInvocation -CommandText $CommandText -CommandWord $CommandWord -SubcommandPath $SubcommandPath).Count -gt 0
455
455
  }
456
456
 
457
457
  function Test-CommandLineMention {
@@ -460,10 +460,9 @@ function Test-CommandLineMention {
460
460
  Report whether a command line merely MENTIONS a command word and subcommand path
461
461
  without invoking it.
462
462
  .DESCRIPTION
463
- Convenience inverse used by hooks that want to log or explain an allow. True when the
464
- raw text contains the command word and every subcommand element but
465
- Test-CommandLineInvocation returns false. Purely informational; no hook makes a deny
466
- decision from this predicate.
463
+ True when Test-CommandLineInvocation returns false and every word occurs in the
464
+ command text as a whole token (Test-CommandLineWordPresent). Purely informational;
465
+ no hook makes a deny decision from this predicate.
467
466
  .OUTPUTS
468
467
  System.Boolean
469
468
  #>
@@ -479,5 +478,5 @@ function Test-CommandLineMention {
479
478
  return $false
480
479
  }
481
480
 
482
- return (Test-CommandLineRawContainment -RawText ([string]$CommandText) -CommandWord $CommandWord -SubcommandPath $SubcommandPath)
481
+ return (Test-CommandLineAllWordPresent -Text ([string]$CommandText) -Word ([string[]](@($CommandWord) + $SubcommandPath)))
483
482
  }