@danmoisan/drm-copilot-mcp 1.1.17 → 1.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/out/mcp-server.js +500 -106
  2. package/package.json +4 -3
  3. package/resources/claude-customizations/.claude/agents/feature-review.md +10 -1
  4. package/resources/claude-customizations/.claude/agents/orchestrator.md +16 -3
  5. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +6 -0
  6. package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +4 -2
  7. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
  8. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +375 -0
  9. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
  10. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
  11. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
  12. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +119 -3
  13. package/resources/claude-customizations/.claude/rules/quality-tiers.md +1 -1
  14. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  15. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +2 -0
  16. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
  17. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
  18. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +169 -5
  19. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
  20. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
  21. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +4 -15
  22. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +13 -6
  23. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  24. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
  25. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +3 -1
  26. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +30 -12
  27. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +18 -3
  28. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +5 -1
  29. package/resources/claude-customizations/pack-manifests/core.json +2 -0
  30. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  31. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +5 -3
  32. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  33. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  34. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
  35. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +3 -1
  36. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +30 -7
  37. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
  38. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +18 -3
  39. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +1 -1
  40. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
  41. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
  42. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
  43. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
  44. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
  45. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
  46. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
  47. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +1 -1
  48. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +1 -1
  49. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +1 -1
  50. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +1 -1
  51. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +1 -1
  52. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +1 -1
  53. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  54. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  55. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  56. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -38,11 +38,12 @@
38
38
  "prepack": "node prepack.cjs"
39
39
  },
40
40
  "overrides": {
41
- "fast-uri": "^3.1.4",
41
+ "fast-uri": "^3.1.8",
42
42
  "hono": "^4.12.27",
43
43
  "ip-address": "^10.7.2",
44
44
  "qs": "^6.15.2",
45
- "@hono/node-server": "^2.0.5"
45
+ "@hono/node-server": "^2.0.5",
46
+ "brace-expansion": "^5.0.12"
46
47
  },
47
48
  "devDependencies": {
48
49
  "esbuild": "^0.28.0"
@@ -9,6 +9,7 @@ tools:
9
9
  - "Bash(git diff *)"
10
10
  - "Bash(git log *)"
11
11
  - "Write(/docs/features/active/**)"
12
+ - "mcp__drm-copilot__validate_orchestration_artifacts"
12
13
  skills:
13
14
  - policy-compliance-order
14
15
  - acceptance-criteria-tracking
@@ -32,7 +33,12 @@ When the active review scope is a selected version folder such as `docs/features
32
33
  1. `docs/features/active/<feature-or-selected-version>/policy-audit.<timestamp>.md` — policy compliance audit with PASS/PARTIAL/FAIL verdicts and evidence
33
34
  2. `docs/features/active/<feature-or-selected-version>/code-review.<timestamp>.md` — code quality review covering best practices
34
35
  3. `docs/features/active/<feature-or-selected-version>/feature-audit.<timestamp>.md` — acceptance criteria verification relative to baseline
35
- 4. If remediation is needed: `docs/features/active/<feature-or-selected-version>/remediation-inputs.<timestamp>.md` with explicit remediation-required findings and artifact paths
36
+ 4. For every review with at least one blocking finding, including halt (`HALT_NON_REMEDIABLE`) and wait (`AWAITING_CI`) verdicts: `docs/features/active/<feature-or-selected-version>/remediation-inputs.<timestamp>.md` with explicit remediation-required findings and artifact paths. The file carries these line formats:
37
+ - one line `Review-Verdict: <VERDICT>`, where the verdict is `PASS`, `REMEDIATION_REQUIRED`, `HALT_NON_REMEDIABLE`, or `AWAITING_CI`,
38
+ - inside each blocking finding block, one line `Remediability: <class>`, where the class is `autonomous`, `external_dependency`, `policy_hold`, `awaiting_ci`, or `human_decision_required` (`autonomous` when the line is absent),
39
+ - inside each blocking finding block, one line `Remediability-Evidence: <text>` stating why the class applies.
40
+
41
+ These lines, including the evidence text, never contain the substrings the orchestrator counts when it counts blocking findings, so their presence does not change the blocking count. The verdict follows from the classes: no blocking finding is `PASS`; any `autonomous` finding is `REMEDIATION_REQUIRED`; otherwise any `external_dependency`, `policy_hold`, or `human_decision_required` finding is `HALT_NON_REMEDIABLE`; otherwise `AWAITING_CI`.
36
42
 
37
43
  Timestamp format: `yyyy-MM-ddTHH-mm` (ISO-8601).
38
44
 
@@ -44,6 +50,9 @@ Report the required artifact paths in the final response using these tokens:
44
50
  - `code-review-path: docs/features/active/<feature-or-selected-version>/code-review.<timestamp>.md`
45
51
  - `feature-audit-path: docs/features/active/<feature-or-selected-version>/feature-audit.<timestamp>.md`
46
52
  - When remediation inputs are produced: `remediation-inputs-path: docs/features/active/<feature-or-selected-version>/remediation-inputs.<timestamp>.md`
53
+ - Optional: `review-status: <VERDICT>` with the same verdict as the remediation inputs (`PASS` when none were produced). The orchestrator treats the remediation inputs as authoritative and this token as a cross-check.
54
+
55
+ Validate each review artifact immediately after writing it with `mcp__drm-copilot__validate_orchestration_artifacts`, passing `artifact_path` set to that artifact: use `artifact_type: "policy-audit"` for `policy-audit.<timestamp>.md`, `artifact_type: "code-review"` for `code-review.<timestamp>.md`, and `artifact_type: "feature-audit"` for `feature-audit.<timestamp>.md`. When the validator reports a failure, fix the artifact in the same review and validate it again. Report an artifact path only after its validation passes.
47
56
 
48
57
  ## Context Sources
49
58
 
@@ -96,7 +96,9 @@ PR creation and PR body edits must be delegated to `Agent(pr-author)`. The orche
96
96
 
97
97
  When the orchestrator runs the remediation loop, it records a top-level `remediation_loop` object in `artifacts/orchestration/orchestrator-state.json`:
98
98
 
99
- - `current_cycle` — integer index of the active cycle.
99
+ - `current_cycle` — integer index of the active cycle's record in `cycles[]`; a record position, not an attempt count.
100
+ - `completed_attempts` — non-negative integer equal to the number of cycles whose `candidate_applied` is `true`. This is the shared attempt count for local, CI-failure, merge-conflict, and parallel-drift cycles; the guard halts after three completed attempts.
101
+ - `review_outcomes[]` — one entry per review, including `PASS`, appended in order. Each entry is an object with `verdict` (one of `PASS`, `REMEDIATION_REQUIRED`, `HALT_NON_REMEDIABLE`, `AWAITING_CI`) and `findings[]`, the review's blocking findings only, each an object with `remediability` (one of `autonomous`, `external_dependency`, `policy_hold`, `awaiting_ci`, `human_decision_required`). An optional `inputs_path` names the `remediation-inputs` file.
100
102
  - `cycles[]` — an ordered array of cycle records. Each cycle is an object with:
101
103
  - `entry_timestamp` — ISO-8601 timestamp when the cycle was entered.
102
104
  - `inputs_path` — path to the `remediation-inputs.<entry-ts>.md` that opened the cycle.
@@ -106,14 +108,25 @@ When the orchestrator runs the remediation loop, it records a top-level `remedia
106
108
  - `audit_paths` — the reaudit artifact paths produced at cycle exit (`code-review`, `feature-audit`, `policy-audit`).
107
109
  - `blocking_count` — the total number of blocking findings across the reaudit artifacts.
108
110
  - `exit_condition_met` — boolean; `true` only when the cycle's exit gate is satisfied.
111
+ - `candidate_applied` — boolean; `true` only for a completed attempt (execution `complete` and a non-empty pre-reaudit commit). A cycle without a candidate records `false` and consumes no attempt number.
112
+ - `opened_by_review` — zero-based index of the `review_outcomes[]` entry that opened the cycle.
113
+
114
+ These four fields are recorded by producers. Verdicts follow from the classes of a review's blocking findings: none is `PASS`; any `autonomous` is `REMEDIATION_REQUIRED`; otherwise any `external_dependency`, `policy_hold`, or `human_decision_required` is `HALT_NON_REMEDIABLE`; otherwise `AWAITING_CI`. A `HALT_NON_REMEDIABLE` or `AWAITING_CI` review opens no cycle.
109
115
 
110
116
  Malformed-cycle rules:
111
117
 
112
118
  - `plan_path` must be a non-empty string.
113
119
  - `execution_status` may be in `{in_progress, complete, failed}` only when `preflight.final_status == 'clear'`; any other preflight status with one of those execution statuses is malformed (execution recorded before preflight cleared).
114
120
  - `exit_condition_met == true` requires `blocking_count == 0`; a non-zero `blocking_count` with `exit_condition_met == true` is malformed.
121
+ - R5: `candidate_applied`, when present, must be a boolean.
122
+ - R6: `candidate_applied == true` requires `execution_status == "complete"`.
123
+ - R7: `completed_attempts`, when present, must be a non-negative integer equal to the number of object cycles whose `candidate_applied` is `true`.
124
+ - R8: `review_outcomes`, when present, must be a list, and each entry must be an object.
125
+ - R9: each outcome's `verdict` must be one of the four verdicts, its `findings` must be a list, and each finding must be an object whose `remediability` is one of the five classes (case-sensitive).
126
+ - R10: each well-formed outcome's `verdict` must equal the verdict derived from its findings' classes.
127
+ - R11: `opened_by_review`, when present, must be a non-negative integer that indexes an object entry of `review_outcomes` whose `verdict` is `REMEDIATION_REQUIRED`.
115
128
 
116
- Cycle-aware `next_step` uses the form `remediation.cycle_N.{plan,preflight,execute,reaudit,exit_check}`, where `N` is the cycle index and the sub-step names the current position in the loop.
129
+ Cycle-aware `next_step` uses the form `remediation.cycle_N.{plan,preflight,execute,reaudit,exit_check}`, where `N` is `completed_attempts + 1` (the active cycle number) and the sub-step names the current position in the loop; `current_cycle` remains a record position in `cycles[]`.
117
130
 
118
131
  ### CI Monitoring and Post-PR Remediation
119
132
 
@@ -149,7 +162,7 @@ A new finding discovered during execution triggers a NEW cycle with a follow-up
149
162
 
150
163
  ### Exit Gate
151
164
 
152
- `blocking_count` is the total of FAIL and blocking-PARTIAL findings across the three reaudit artifacts (`code-review`, `feature-audit`, `policy-audit`). Only `blocking_count == 0` sets `exit_condition_met = true`. A non-zero `blocking_count` leaves the gate unmet and opens the next cycle.
165
+ `blocking_count` is the total of FAIL and blocking-PARTIAL findings across the three reaudit artifacts (`code-review`, `feature-audit`, `policy-audit`). Only `blocking_count == 0` sets `exit_condition_met = true`. The exit gate counts only `autonomous` blocking findings when deciding whether to open the next cycle: a non-zero count of `autonomous` blocking findings leaves the gate unmet and opens the next cycle, while remaining blocking findings that are all non-remediable halt (`HALT_NON_REMEDIABLE`) or wait (`AWAITING_CI`) instead of opening a cycle.
153
166
 
154
167
  ### Citations
155
168
 
@@ -275,6 +275,12 @@ In `closed` mode, do not report completion until:
275
275
  `artifact_type: "parallel-orchestrator-state"`).
276
276
  4. Acceptance criteria in the AC source files of each item have been checked off by that item's own
277
277
  run, per the `acceptance-criteria-tracking` skill.
278
+ A CI-dependent criterion, whose verification requires the result of CI on the pull-request head,
279
+ is checked off and pushed by the item's own run after its S9 success and before its DONE, and
280
+ that run re-runs S9 so its reported `ci_gate.head_sha` is the final head. Before `gh pr merge`,
281
+ confirm that the `headRefOid` value equals that reported `ci_gate.head_sha` and that the item
282
+ reported no pending CI-dependent criteria. You never commit acceptance-criteria check-offs from
283
+ the coordinator root.
278
284
 
279
285
  In `open` mode there is no automatic completion. The run is a standing queue and terminates only
280
286
  via `/parallel-close`, which is owned by F6 and is not specified here or shipped by this feature. Do
@@ -82,8 +82,10 @@ pla_parse_edges() {
82
82
  PLA_EDGES=()
83
83
  local -a tokens=()
84
84
  # read -ra rather than an unquoted expansion, so a token carrying a glob
85
- # character is never subjected to pathname expansion.
86
- read -ra tokens <<<"$text"
85
+ # character is never subjected to pathname expansion. Newline, CR, VT, and FF
86
+ # become spaces first: read consumes one newline-terminated record and splits
87
+ # it on IFS only, so the Python str.split() whitespace set would otherwise differ.
88
+ read -ra tokens <<<"${text//[$'\n\r\v\f']/ }"
87
89
  for token in "${tokens[@]}"; do
88
90
  # Partition on the FIRST colon, matching str.partition: a token with two
89
91
  # colons yields a second endpoint that still carries one, which the lexis
@@ -93,17 +93,10 @@ $script:STEP_SPECIFIC_EXTRA_STATUS = @{
93
93
  step9_status = @('passed', 'failed_remediation_required', 'blocked_ci_loop_limit')
94
94
  }
95
95
 
96
- # The allowed blocked_reason vocabulary. Pinned to VALID_BLOCKED_REASONS in the
97
- # primary validator.
98
- $script:VALID_BLOCKED_REASONS = @(
99
- 'none',
100
- 'spawn_agent_unavailable',
101
- 'delegation_launch_failed',
102
- 'delegate_no_receipt',
103
- 'delegate_contract_incomplete',
104
- 'validator_failed',
105
- 'user_requested_stop'
106
- )
96
+ # blocked_reason vocabulary; mirrors scripts/dev_tools/_orchestrator_state_blocked_reason.py.
97
+ $script:MECHANICAL_BLOCKED_REASONS = @('spawn_agent_unavailable', 'delegation_launch_failed', 'delegate_no_receipt', 'delegate_contract_incomplete', 'validator_failed', 'user_requested_stop')
98
+ $script:NON_MECHANICAL_BLOCKED_REASONS = @('premise_falsified', 'external_dependency', 'policy_hold', 'awaiting_ci', 'human_decision_required')
99
+ $script:VALID_BLOCKED_REASONS = @('none') + $script:MECHANICAL_BLOCKED_REASONS + $script:NON_MECHANICAL_BLOCKED_REASONS
107
100
 
108
101
  # The upstream steps that must not be pending/blocked before the first PR creation
109
102
  # of a branch. Pinned to PR_CREATION_READY_STEP_KEYS in
@@ -290,7 +283,7 @@ function Get-OrchestratorStateBasePresenceError {
290
283
  # A present, non-null blocked_reason must be a member of the allowed vocabulary.
291
284
  $blocked = Get-OrchestratorStateField -State $State -Name 'blocked_reason'
292
285
  if ($blocked.Present -and $null -ne $blocked.Value -and
293
- ($script:VALID_BLOCKED_REASONS -notcontains [string]$blocked.Value)) {
286
+ ($script:VALID_BLOCKED_REASONS -cnotcontains [string]$blocked.Value)) {
294
287
  $errors.Add("Checkpoint has invalid blocked_reason: $($blocked.Value)")
295
288
  }
296
289
 
@@ -333,7 +326,7 @@ function Get-OrchestratorStatePrCreationReadinessError {
333
326
  # blocked_reason must read as clear: absent, null, or the literal 'none'. Any
334
327
  # other recorded reason means the branch is not ready for PR creation.
335
328
  $blocked = Get-OrchestratorStateField -State $State -Name 'blocked_reason'
336
- if ($blocked.Present -and $null -ne $blocked.Value -and ([string]$blocked.Value -ne 'none')) {
329
+ if ($blocked.Present -and $null -ne $blocked.Value -and ([string]$blocked.Value -cne 'none')) {
337
330
  $errors.Add('Checkpoint PR-creation readiness validation failed: blocked_reason is not `none`.')
338
331
  }
339
332
 
@@ -0,0 +1,375 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Issue-adoption record validation and receipt waivers (inventory row C6.15).
4
+
5
+ .DESCRIPTION
6
+ Destination-runtime PowerShell port of `resolve_issue_adoption` in
7
+ `scripts/dev_tools/_orchestrator_state_issue_adoption.py`. The optional
8
+ checkpoint object `issue_adoption` records that the orchestration adopted a
9
+ GitHub issue that existed before orchestration started. A valid record
10
+ waives the successful-receipt requirement for `potential_to_issue` and,
11
+ optionally, for the checkpoint's promotion-entry tool.
12
+
13
+ Invariants:
14
+ - When the `issue_adoption` member is absent, no error is produced and no
15
+ tool is waived, so existing checkpoints validate exactly as before.
16
+ - Errors accumulate in the fixed rule order of the feature specification.
17
+ - Fail-closed: WaivedTools is empty whenever Errors is non-empty.
18
+ - Every string comparison is ordinal and case-sensitive.
19
+ - Messages interpolate only non-blank `waived_tools` entries and the route id.
20
+
21
+ Every function is pure: it reads no file, starts no process, and never mutates
22
+ its input.
23
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
24
+ #>
25
+
26
+ Set-StrictMode -Version Latest
27
+ $ErrorActionPreference = 'Stop'
28
+
29
+ # Import the shared checkpoint-value primitives, resolved relative to this
30
+ # module's directory so the import travels with the pushed-down pack.
31
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'OrchestratorStateCheckpointValue.psm1') -Force -ErrorAction Stop
32
+
33
+ $script:ISSUE_ADOPTION_KEY = 'issue_adoption'
34
+ $script:POTENTIAL_TO_ISSUE_TOOL = 'potential_to_issue'
35
+ $script:FEATURE_PROMOTION_ENTRY_TOOL = 'new_potential_entry'
36
+ $script:BUG_PROMOTION_ENTRY_TOOL = 'new_potential_bug_entry'
37
+ $script:ORIGIN_VALUES = [string[]]@('transferred', 'filed_before_orchestration', 'epic_decomposition')
38
+ $script:VERIFIED_VIA_VALUES = [string[]]@('gh_issue_view', 'gh_api_get', 'github_mcp_issue_read')
39
+
40
+ # The closed set of tools an adoption record may waive: the issue-creation tool
41
+ # and the two promotion-type-specific promotion-entry tools.
42
+ $script:WAIVABLE_TOOLS = [string[]]@($script:POTENTIAL_TO_ISSUE_TOOL, $script:FEATURE_PROMOTION_ENTRY_TOOL, $script:BUG_PROMOTION_ENTRY_TOOL)
43
+ $script:PROMOTION_ENTRY_TOOLS = [string[]]@($script:FEATURE_PROMOTION_ENTRY_TOOL, $script:BUG_PROMOTION_ENTRY_TOOL)
44
+ $script:POTENTIAL_RECORD_PREFIX = 'docs/features/potential/'
45
+ $script:POTENTIAL_RECORD_SUFFIX = '.md'
46
+ $script:ISSUE_NUM_PATTERN = '\A[1-9][0-9]*\z'
47
+
48
+ $script:ERROR_NOT_OBJECT = 'Checkpoint issue_adoption must be an object when present.'
49
+ $script:ERROR_ISSUE_NUM_FORMAT = 'Checkpoint issue_adoption.issue_num must be a string of decimal digits without a leading zero.'
50
+ $script:ERROR_ISSUE_NUM_MISMATCH = 'Checkpoint issue_adoption.issue_num must equal the checkpoint issue-num.'
51
+ $script:ERROR_ISSUE_URL = 'Checkpoint issue_adoption.issue_url must end with /issues/ followed by issue_num.'
52
+ $script:ERROR_ORIGIN = 'Checkpoint issue_adoption.origin must be one of transferred, filed_before_orchestration, epic_decomposition.'
53
+ $script:ERROR_VERIFIED_VIA = 'Checkpoint issue_adoption.verified_via must be one of gh_issue_view, gh_api_get, github_mcp_issue_read.'
54
+ $script:ERROR_VERIFIED_AT = 'Checkpoint issue_adoption.verified_at must be present.'
55
+ $script:ERROR_EVIDENCE = 'Checkpoint issue_adoption.evidence must be a non-empty string.'
56
+ $script:ERROR_WAIVED_TOOLS_SHAPE = 'Checkpoint issue_adoption.waived_tools must be a non-empty list of tool names.'
57
+ $script:ERROR_WAIVED_TOOLS_INCLUDE = 'Checkpoint issue_adoption.waived_tools must include potential_to_issue.'
58
+
59
+
60
+ function Test-AdoptionNonBlankString {
61
+ <#
62
+ .SYNOPSIS
63
+ Report whether a value is a string with non-whitespace content.
64
+ .PARAMETER Value
65
+ The deserialized JSON value to inspect. May be $null.
66
+ .OUTPUTS
67
+ System.Boolean
68
+ #>
69
+ [CmdletBinding()]
70
+ [OutputType([bool])]
71
+ param(
72
+ [Parameter(Mandatory = $true)]
73
+ [AllowNull()]
74
+ [object] $Value
75
+ )
76
+
77
+ return (($Value -is [string]) -and -not [string]::IsNullOrWhiteSpace([string]$Value))
78
+ }
79
+
80
+ function Get-AdoptionIssueIdentityError {
81
+ <#
82
+ .SYNOPSIS
83
+ Validate issue_num and issue_url (rules 2 and 3).
84
+ .PARAMETER Adoption
85
+ The issue_adoption object.
86
+ .PARAMETER State
87
+ The parsed checkpoint object, read for its hyphenated issue-num key.
88
+ .OUTPUTS
89
+ System.String[] - zero or more error strings.
90
+ #>
91
+ [CmdletBinding()]
92
+ [OutputType([string[]])]
93
+ param(
94
+ [Parameter(Mandatory = $true)]
95
+ [psobject] $Adoption,
96
+
97
+ [Parameter(Mandatory = $true)]
98
+ [psobject] $State
99
+ )
100
+
101
+ $errors = [System.Collections.Generic.List[string]]::new()
102
+ $rawIssueNum = (Get-CheckpointObjectMember -Owner $Adoption -Name 'issue_num').Value
103
+ $issueNum = $null
104
+ if (($rawIssueNum -is [string]) -and ([string]$rawIssueNum -cmatch $script:ISSUE_NUM_PATTERN)) {
105
+ $issueNum = [string]$rawIssueNum
106
+ }
107
+
108
+ if ($null -ceq $issueNum) {
109
+ $errors.Add($script:ERROR_ISSUE_NUM_FORMAT)
110
+ }
111
+ else {
112
+ # The equality check applies only when the checkpoint records its own
113
+ # issue number as a string; other shapes are validated elsewhere.
114
+ $checkpointIssueNum = (Get-CheckpointObjectMember -Owner $State -Name 'issue-num').Value
115
+ if (($checkpointIssueNum -is [string]) -and -not [string]::Equals([string]$checkpointIssueNum, $issueNum, [System.StringComparison]::Ordinal)) {
116
+ $errors.Add($script:ERROR_ISSUE_NUM_MISMATCH)
117
+ }
118
+ }
119
+
120
+ $issueUrl = (Get-CheckpointObjectMember -Owner $Adoption -Name 'issue_url').Value
121
+ $urlIsValid = ($null -cne $issueNum) -and ($issueUrl -is [string]) -and ([string]$issueUrl).EndsWith("/issues/$issueNum", [System.StringComparison]::Ordinal)
122
+ if (-not $urlIsValid) {
123
+ $errors.Add($script:ERROR_ISSUE_URL)
124
+ }
125
+ return $errors.ToArray()
126
+ }
127
+
128
+ function Get-AdoptionProvenanceError {
129
+ <#
130
+ .SYNOPSIS
131
+ Validate origin, verified_via, verified_at, and evidence (rules 4 to 7).
132
+ .PARAMETER Adoption
133
+ The issue_adoption object.
134
+ .OUTPUTS
135
+ System.String[] - zero or more error strings.
136
+ #>
137
+ [CmdletBinding()]
138
+ [OutputType([string[]])]
139
+ param(
140
+ [Parameter(Mandatory = $true)]
141
+ [psobject] $Adoption
142
+ )
143
+
144
+ $errors = [System.Collections.Generic.List[string]]::new()
145
+
146
+ $origin = (Get-CheckpointObjectMember -Owner $Adoption -Name 'origin').Value
147
+ if (-not (($origin -is [string]) -and ($script:ORIGIN_VALUES -ccontains [string]$origin))) {
148
+ $errors.Add($script:ERROR_ORIGIN)
149
+ }
150
+
151
+ $verifiedVia = (Get-CheckpointObjectMember -Owner $Adoption -Name 'verified_via').Value
152
+ if (-not (($verifiedVia -is [string]) -and ($script:VERIFIED_VIA_VALUES -ccontains [string]$verifiedVia))) {
153
+ $errors.Add($script:ERROR_VERIFIED_VIA)
154
+ }
155
+
156
+ # Presence-only: any non-null value other than a blank string passes,
157
+ # including a System.DateTime produced by ConvertFrom-Json.
158
+ $verifiedAt = (Get-CheckpointObjectMember -Owner $Adoption -Name 'verified_at').Value
159
+ if (($null -ceq $verifiedAt) -or (($verifiedAt -is [string]) -and [string]::IsNullOrWhiteSpace([string]$verifiedAt))) {
160
+ $errors.Add($script:ERROR_VERIFIED_AT)
161
+ }
162
+
163
+ if (-not (Test-AdoptionNonBlankString -Value (Get-CheckpointObjectMember -Owner $Adoption -Name 'evidence').Value)) {
164
+ $errors.Add($script:ERROR_EVIDENCE)
165
+ }
166
+ return $errors.ToArray()
167
+ }
168
+
169
+ function Get-AdoptionWaivedToolList {
170
+ <#
171
+ .SYNOPSIS
172
+ Return the waived tools when they form a non-empty list of non-blank strings.
173
+ .PARAMETER Value
174
+ The deserialized waived_tools value. May be $null.
175
+ .OUTPUTS
176
+ System.Collections.Hashtable with keys Ok (bool) and Value (string[]).
177
+ #>
178
+ [CmdletBinding()]
179
+ [OutputType([hashtable])]
180
+ param(
181
+ [Parameter(Mandatory = $true)]
182
+ [AllowNull()]
183
+ [object] $Value
184
+ )
185
+
186
+ if (-not (Test-CheckpointListValue -Value $Value)) {
187
+ return @{ Ok = $false; Value = [string[]]@() }
188
+ }
189
+ $items = @($Value)
190
+ if ($items.Count -ceq 0) {
191
+ return @{ Ok = $false; Value = [string[]]@() }
192
+ }
193
+ foreach ($item in $items) {
194
+ if (-not (Test-AdoptionNonBlankString -Value $item)) {
195
+ return @{ Ok = $false; Value = [string[]]@() }
196
+ }
197
+ }
198
+ return @{ Ok = $true; Value = [string[]]$items }
199
+ }
200
+
201
+ function Get-AdoptionWaivedToolError {
202
+ <#
203
+ .SYNOPSIS
204
+ Validate each waived tool entry and the mandatory issue-creation waiver (rule 8).
205
+ .PARAMETER WaivedTool
206
+ The well-formed waived tool list, in checkpoint order.
207
+ .PARAMETER RouteId
208
+ The selected route id, interpolated into route errors.
209
+ .PARAMETER RequiredMcpTool
210
+ The route's required MCP tools after promotion-type resolution.
211
+ .PARAMETER SuccessfulTool
212
+ Tools that hold a successful MCP receipt.
213
+ .OUTPUTS
214
+ System.String[] - zero or more error strings.
215
+ #>
216
+ [CmdletBinding()]
217
+ [OutputType([string[]])]
218
+ param(
219
+ [Parameter(Mandatory = $true)]
220
+ [string[]] $WaivedTool,
221
+
222
+ [Parameter(Mandatory = $true)]
223
+ [string] $RouteId,
224
+
225
+ [Parameter(Mandatory = $true)]
226
+ [AllowEmptyCollection()]
227
+ [string[]] $RequiredMcpTool,
228
+
229
+ [Parameter(Mandatory = $true)]
230
+ [AllowEmptyCollection()]
231
+ [string[]] $SuccessfulTool
232
+ )
233
+
234
+ $errors = [System.Collections.Generic.List[string]]::new()
235
+ $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
236
+ foreach ($tool in $WaivedTool) {
237
+ # Each entry receives only the first rule it violates, in rule order.
238
+ if ($seen.Contains($tool)) {
239
+ $errors.Add("Checkpoint issue_adoption.waived_tools lists a tool more than once: $tool.")
240
+ }
241
+ elseif ($script:WAIVABLE_TOOLS -cnotcontains $tool) {
242
+ $errors.Add("Checkpoint issue_adoption.waived_tools names a tool that cannot be waived: $tool.")
243
+ }
244
+ elseif ($RequiredMcpTool -cnotcontains $tool) {
245
+ $errors.Add("Checkpoint issue_adoption.waived_tools names a tool that is not required by route ${RouteId}: $tool.")
246
+ }
247
+ elseif ($SuccessfulTool -ccontains $tool) {
248
+ $errors.Add("Checkpoint issue_adoption.waived_tools names a tool that has a successful MCP receipt: $tool.")
249
+ }
250
+ [void]$seen.Add($tool)
251
+ }
252
+
253
+ if (-not $seen.Contains($script:POTENTIAL_TO_ISSUE_TOOL)) {
254
+ $errors.Add($script:ERROR_WAIVED_TOOLS_INCLUDE)
255
+ }
256
+ return $errors.ToArray()
257
+ }
258
+
259
+ function Get-AdoptionPotentialRecordError {
260
+ <#
261
+ .SYNOPSIS
262
+ Require a potential record when a promotion-entry tool is waived (rule 9).
263
+ .PARAMETER WaivedTool
264
+ The well-formed waived tool list, in checkpoint order.
265
+ .PARAMETER PotentialRecord
266
+ The deserialized potential_record value. May be $null.
267
+ .OUTPUTS
268
+ System.String[] - zero or more error strings.
269
+ #>
270
+ [CmdletBinding()]
271
+ [OutputType([string[]])]
272
+ param(
273
+ [Parameter(Mandatory = $true)]
274
+ [string[]] $WaivedTool,
275
+
276
+ [Parameter(Mandatory = $true)]
277
+ [AllowNull()]
278
+ [object] $PotentialRecord
279
+ )
280
+
281
+ $recordIsValid = $false
282
+ if ($PotentialRecord -is [string]) {
283
+ $record = [string]$PotentialRecord
284
+ $recordIsValid = $record.StartsWith($script:POTENTIAL_RECORD_PREFIX, [System.StringComparison]::Ordinal) -and $record.EndsWith($script:POTENTIAL_RECORD_SUFFIX, [System.StringComparison]::Ordinal)
285
+ }
286
+ if ($recordIsValid) { return [string[]]@() }
287
+
288
+ $errors = [System.Collections.Generic.List[string]]::new()
289
+ $reported = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
290
+ foreach ($tool in $WaivedTool) {
291
+ if (($script:PROMOTION_ENTRY_TOOLS -ccontains $tool) -and -not $reported.Contains($tool)) {
292
+ $errors.Add("Checkpoint issue_adoption.potential_record must name a markdown file under docs/features/potential/ when waiving $tool.")
293
+ [void]$reported.Add($tool)
294
+ }
295
+ }
296
+ return $errors.ToArray()
297
+ }
298
+
299
+ function Get-OrchestratorStateIssueAdoptionResult {
300
+ <#
301
+ .SYNOPSIS
302
+ Validate the checkpoint's issue_adoption record and resolve its waivers.
303
+ .DESCRIPTION
304
+ Public entry mirroring resolve_issue_adoption. Absent key: no errors and
305
+ no waivers. A present value that is not an object (null included) yields
306
+ only the not-an-object error. Otherwise rules 2 to 9 accumulate errors in
307
+ order, and the waived-tool set is populated only when no error was found.
308
+ .PARAMETER State
309
+ The parsed checkpoint object.
310
+ .PARAMETER RouteId
311
+ The selected route id, interpolated into route errors.
312
+ .PARAMETER RequiredMcpTool
313
+ The route's required MCP tools after promotion-type resolution.
314
+ .PARAMETER SuccessfulTool
315
+ Tools that hold a successful MCP receipt in the checkpoint.
316
+ .OUTPUTS
317
+ System.Collections.Hashtable with keys Errors (string[]) and WaivedTools (string[]).
318
+ #>
319
+ [CmdletBinding()]
320
+ [OutputType([hashtable])]
321
+ param(
322
+ [Parameter(Mandatory = $true)]
323
+ [psobject] $State,
324
+
325
+ [Parameter(Mandatory = $true)]
326
+ [string] $RouteId,
327
+
328
+ [Parameter(Mandatory = $true)]
329
+ [AllowEmptyCollection()]
330
+ [string[]] $RequiredMcpTool,
331
+
332
+ [Parameter(Mandatory = $true)]
333
+ [AllowEmptyCollection()]
334
+ [string[]] $SuccessfulTool
335
+ )
336
+
337
+ $member = Get-CheckpointObjectMember -Owner $State -Name $script:ISSUE_ADOPTION_KEY
338
+ if (-not $member.Present) {
339
+ return @{ Errors = [string[]]@(); WaivedTools = [string[]]@() }
340
+ }
341
+
342
+ $adoption = $member.Value
343
+ if (-not (Test-CheckpointObjectValue -Value $adoption)) {
344
+ return @{ Errors = [string[]]@($script:ERROR_NOT_OBJECT); WaivedTools = [string[]]@() }
345
+ }
346
+
347
+ $errors = [System.Collections.Generic.List[string]]::new()
348
+ $errors.AddRange([string[]]@(Get-AdoptionIssueIdentityError -Adoption $adoption -State $State))
349
+ $errors.AddRange([string[]]@(Get-AdoptionProvenanceError -Adoption $adoption))
350
+
351
+ $waived = Get-AdoptionWaivedToolList -Value (Get-CheckpointObjectMember -Owner $adoption -Name 'waived_tools').Value
352
+ if (-not $waived.Ok) {
353
+ # A malformed list stops rule 8 and skips rule 9 entirely.
354
+ $errors.Add($script:ERROR_WAIVED_TOOLS_SHAPE)
355
+ return @{ Errors = $errors.ToArray(); WaivedTools = [string[]]@() }
356
+ }
357
+
358
+ $errors.AddRange([string[]]@(Get-AdoptionWaivedToolError -WaivedTool $waived.Value -RouteId $RouteId -RequiredMcpTool $RequiredMcpTool -SuccessfulTool $SuccessfulTool))
359
+ $potentialRecord = (Get-CheckpointObjectMember -Owner $adoption -Name 'potential_record').Value
360
+ $errors.AddRange([string[]]@(Get-AdoptionPotentialRecordError -WaivedTool $waived.Value -PotentialRecord $potentialRecord))
361
+
362
+ if ($errors.Count -gt 0) {
363
+ return @{ Errors = $errors.ToArray(); WaivedTools = [string[]]@() }
364
+ }
365
+
366
+ # The waived-tool set is the distinct entries in first-occurrence order.
367
+ $distinct = [System.Collections.Generic.List[string]]::new()
368
+ foreach ($tool in $waived.Value) {
369
+ if (-not $distinct.Contains($tool)) { $distinct.Add($tool) }
370
+ }
371
+ return @{ Errors = [string[]]@(); WaivedTools = $distinct.ToArray() }
372
+ }
373
+
374
+ # Only the family entry point is exported; the rule helpers stay private.
375
+ Export-ModuleMember -Function Get-OrchestratorStateIssueAdoptionResult
@@ -37,6 +37,7 @@ $ErrorActionPreference = 'Stop'
37
37
  # module's directory so the import travels with the pushed-down pack regardless
38
38
  # of the consumer repository's working directory.
39
39
  Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'OrchestratorStateCheckpointValue.psm1') -Force -ErrorAction Stop
40
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'OrchestratorStateRemediationAccounting.psm1') -Force -ErrorAction Stop
40
41
 
41
42
  # The eight keys every list-form delegation receipt must carry. Pinned to
42
43
  # REQUIRED_RECEIPT_KEYS in scripts/dev_tools/validate_orchestrator_state.py.
@@ -290,12 +291,15 @@ function Get-RemediationCycleError {
290
291
  function Get-OrchestratorStateRemediationLoopError {
291
292
  <#
292
293
  .SYNOPSIS
293
- Return the remediation_loop errors (inventory rows U6.R1-U6.R4).
294
+ Return the remediation_loop errors (inventory rows U6.R1-U6.R4, R5-R11).
294
295
  .DESCRIPTION
295
296
  Public entry mirroring _validate_remediation_loop. A non-object
296
- remediation_loop, or a cycles value that is not a list, carries no cycles
297
- to validate and deliberately yields ZERO errors, matching the Python
298
- tolerance rather than fabricating a structural error.
297
+ remediation_loop deliberately yields ZERO errors, matching the Python
298
+ tolerance rather than fabricating a structural error. When cycles is a
299
+ list each cycle is validated independently (U6.R1-U6.R4); the issue #484
300
+ review-outcome and accounting invariants R5-R11 then run through
301
+ Get-OrchestratorStateRemediationAccountingError with the cycle list, or
302
+ $null when cycles is absent or not a list.
299
303
  .PARAMETER Value
300
304
  The raw deserialized value of the checkpoint's remediation_loop key.
301
305
  .OUTPUTS
@@ -313,21 +317,24 @@ function Get-OrchestratorStateRemediationLoopError {
313
317
 
314
318
  if (-not (Test-CheckpointObjectValue -Value $Value)) { return $errors.ToArray() }
315
319
  $cycles = (Get-CheckpointObjectMember -Owner $Value -Name $script:REMEDIATION_CYCLES_KEY).Value
316
- if (-not (Test-CheckpointListValue -Value $cycles)) { return $errors.ToArray() }
317
-
318
- # Validate each cycle independently so a malformed entry does not mask the
319
- # errors of the cycles that follow it.
320
- $index = 0
321
- foreach ($cycle in @($cycles)) {
322
- if (-not (Test-CheckpointObjectValue -Value $cycle)) {
323
- $errors.Add("Checkpoint remediation cycle #$index must be an object.")
320
+ $cycleList = $null
321
+ if (Test-CheckpointListValue -Value $cycles) {
322
+ $cycleList = $cycles
323
+ # Validate each cycle independently so a malformed entry does not mask the
324
+ # errors of the cycles that follow it.
325
+ $index = 0
326
+ foreach ($cycle in @($cycles)) {
327
+ if (-not (Test-CheckpointObjectValue -Value $cycle)) {
328
+ $errors.Add("Checkpoint remediation cycle #$index must be an object.")
329
+ $index++
330
+ continue
331
+ }
332
+ $errors.AddRange([string[]]@(Get-RemediationCycleError -Index $index -Cycle $cycle))
324
333
  $index++
325
- continue
326
334
  }
327
- $errors.AddRange([string[]]@(Get-RemediationCycleError -Index $index -Cycle $cycle))
328
- $index++
329
335
  }
330
336
 
337
+ $errors.AddRange([string[]]@(Get-OrchestratorStateRemediationAccountingError -RemediationLoop $Value -Cycle $cycleList))
331
338
  return $errors.ToArray()
332
339
  }
333
340