@danmoisan/drm-copilot-mcp 1.1.25 → 1.1.27

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 (185) hide show
  1. package/out/mcp-server.js +170 -52
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +3 -3
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +2 -2
  5. package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +7 -7
  6. package/resources/claude-customizations/.claude/agents/feature-review.md +6 -3
  7. package/resources/claude-customizations/.claude/agents/orchestrator.md +11 -7
  8. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +7 -3
  9. package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
  10. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +7 -7
  11. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +6 -6
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-review.ps1 +699 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +16 -1
  14. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
  15. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
  18. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
  21. package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
  22. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +50 -16
  23. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -1
  24. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +4 -2
  25. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +51 -2
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +5 -2
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +116 -4
  28. package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
  29. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
  30. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
  31. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
  32. package/resources/claude-customizations/.claude/rules/csharp.md +8 -8
  33. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -2
  34. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
  35. package/resources/claude-customizations/.claude/rules/powershell.md +2 -2
  36. package/resources/claude-customizations/.claude/settings.json +1 -0
  37. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +9 -13
  38. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
  39. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
  40. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
  41. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
  42. package/resources/claude-customizations/.claude/skills/csharp-qa-gate/SKILL.md +14 -15
  43. package/resources/claude-customizations/.claude/skills/epic-progress-report/SKILL.md +338 -0
  44. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Logic.ps1 +248 -0
  45. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Metrics.ps1 +232 -0
  46. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.Render.ps1 +196 -0
  47. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.State.ps1 +286 -0
  48. package/resources/claude-customizations/.claude/skills/epic-progress-report/scripts/Get-EpicProgressReport.ps1 +475 -0
  49. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +31 -16
  50. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +21 -9
  51. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +5 -5
  52. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -4
  53. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +4 -4
  54. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +24 -2
  55. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +34 -2
  57. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
  58. package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +16 -17
  59. package/resources/claude-customizations/.claude/skills/python-qa-gate/SKILL.md +15 -16
  60. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
  61. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +6 -6
  62. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +13 -14
  63. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +4 -4
  64. package/resources/claude-customizations/pack-manifests/core.json +15 -0
  65. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +8 -12
  66. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +1 -1
  67. package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +13 -14
  68. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +31 -16
  69. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +5 -1
  70. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +18 -5
  71. package/resources/codex-and-agents-customizations/.agents/skills/invoke-csharp-engineer/SKILL.md +4 -4
  72. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +4 -4
  73. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +4 -4
  74. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +24 -2
  75. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +16 -1
  76. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +3 -3
  77. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +2 -2
  78. package/resources/codex-and-agents-customizations/.agents/skills/powershell-qa-gate/SKILL.md +15 -16
  79. package/resources/codex-and-agents-customizations/.agents/skills/python-qa-gate/SKILL.md +14 -15
  80. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +2 -0
  81. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +13 -14
  82. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +4 -4
  83. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +3 -3
  84. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +3 -3
  85. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +3 -3
  86. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +3 -3
  87. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +3 -3
  88. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +3 -3
  89. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +2 -2
  90. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +2 -2
  91. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +2 -2
  92. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +2 -2
  93. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +2 -2
  94. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +2 -2
  95. package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-executor.toml +2 -2
  96. package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-planning.toml +1 -1
  97. package/resources/codex-and-agents-customizations/.codex/agents/csharp-orchestrator.toml +1 -1
  98. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +6 -6
  99. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +6 -6
  100. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +6 -6
  101. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +6 -6
  102. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +6 -6
  103. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +6 -6
  104. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +2 -2
  105. package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-executor.toml +2 -2
  106. package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-planning.toml +1 -1
  107. package/resources/codex-and-agents-customizations/.codex/agents/powershell-di-unit-test-engineer.toml +1 -1
  108. package/resources/codex-and-agents-customizations/.codex/agents/powershell-orchestrator.toml +1 -1
  109. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +7 -7
  110. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +7 -7
  111. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +7 -7
  112. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +7 -7
  113. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +7 -7
  114. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +7 -7
  115. package/resources/codex-and-agents-customizations/.codex/agents/pytest-unit-test-coding.toml +1 -1
  116. package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-executor.toml +2 -2
  117. package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-planning.toml +1 -1
  118. package/resources/codex-and-agents-customizations/.codex/agents/python-execution-only-typed.toml +1 -1
  119. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +1 -1
  120. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +6 -6
  121. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +6 -6
  122. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +6 -6
  123. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +6 -6
  124. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +6 -6
  125. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +6 -6
  126. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  127. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate-review.ps1 +699 -0
  128. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +29 -2
  129. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
  130. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
  131. package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
  132. package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +51 -4
  133. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +6 -6
  134. package/resources/codex-and-agents-customizations/pack-manifests/core.json +3 -0
  135. package/resources/customizations/.github/agents/5.1-Beast-adjusted.agent.md +1 -1
  136. package/resources/customizations/.github/agents/Powershell DI Unit Test Engineer.agent.md +20 -26
  137. package/resources/customizations/.github/agents/atomic_executor.agent.md +4 -4
  138. package/resources/customizations/.github/agents/atomic_planning.agent.md +4 -4
  139. package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
  140. package/resources/customizations/.github/agents/commit-steward.agent.md +1 -1
  141. package/resources/customizations/.github/agents/csharp-atomic-executor.agent.md +14 -14
  142. package/resources/customizations/.github/agents/csharp-atomic-planning.agent.md +2 -2
  143. package/resources/customizations/.github/agents/csharp-orchestrator.agent.md +4 -4
  144. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +19 -22
  145. package/resources/customizations/.github/agents/epic-review.agent.md +1 -1
  146. package/resources/customizations/.github/agents/gpt-5-beast-mode.agent.md +3 -3
  147. package/resources/customizations/.github/agents/orchestrator.agent.md +8 -8
  148. package/resources/customizations/.github/agents/powershell-atomic-executor.agent.md +12 -12
  149. package/resources/customizations/.github/agents/powershell-atomic-planning.agent.md +4 -4
  150. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +6 -6
  151. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +21 -24
  152. package/resources/customizations/.github/agents/pytest-unit-test-coding.agent.md +13 -17
  153. package/resources/customizations/.github/agents/python-atomic-executor.agent.md +11 -11
  154. package/resources/customizations/.github/agents/python-atomic-planning.agent.md +2 -2
  155. package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +14 -17
  156. package/resources/customizations/.github/agents/python-orchestrator.agent.md +7 -7
  157. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +17 -20
  158. package/resources/customizations/.github/agents/staged-review.agent.md +2 -2
  159. package/resources/customizations/.github/agents/tdd-green.agent.md +1 -1
  160. package/resources/customizations/.github/agents/tdd-red.agent.md +3 -3
  161. package/resources/customizations/.github/agents/tdd-refactor.agent.md +2 -2
  162. package/resources/customizations/.github/agents/typescript-engineer.agent.md +3 -3
  163. package/resources/customizations/.github/codex/execute-hard-lock.prompt.md +3 -3
  164. package/resources/customizations/.github/instructions/general-code-change.instructions.md +1 -1
  165. package/resources/customizations/.github/instructions/powershell-unit-test.instructions.md +1 -1
  166. package/resources/customizations/.github/instructions/python-code-change.instructions.md +5 -5
  167. package/resources/customizations/.github/instructions/python-suppressions.instructions.md +3 -3
  168. package/resources/customizations/.github/instructions/python-unit-test.instructions.md +1 -1
  169. package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +2 -2
  170. package/resources/customizations/.github/prompts/generate-commit-message-repo.prompt.md +2 -2
  171. package/resources/customizations/.github/prompts/remediate-comments.prompt.md +2 -2
  172. package/resources/customizations/.github/prompts/review-feature.prompt.md +1 -1
  173. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +8 -12
  174. package/resources/customizations/.github/skills/execution-environment/SKILL.md +31 -16
  175. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +15 -2
  176. package/resources/feature-templates/bug/plan.yyyy-MM-ddTHH-mm.md +4 -4
  177. package/resources/feature-templates/feature/plan.yyyy-MM-ddTHH-mm.md +3 -3
  178. package/resources/feature-templates/refactor/plan.yyyy-MM-ddTHH-mm.md +2 -2
  179. package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
  180. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +14 -12
  181. package/resources/powershell/PoshQC/PoshQC.psm1 +3 -3
  182. package/resources/powershell/PoshQC/README.md +8 -8
  183. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -2
  184. package/resources/templates/policy_audit/AGENTS.md +9 -10
  185. package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +19 -20
@@ -0,0 +1,248 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pure logic for Get-EpicProgressReport.ps1.
4
+
5
+ .DESCRIPTION
6
+ The hub of the epic progress report logic. It holds the parsing and counting functions
7
+ and dot-sources the State (settings, state file, baseline, samples), Metrics (velocity,
8
+ deviation, estimates, formatting), and Render (summary and line composition) files, so a
9
+ consumer that dot-sources this file receives every logic function. Every function takes
10
+ data in and returns data: none reads a file, starts a process, calls the network, or
11
+ reads the clock. The main script dot-sources this file and supplies the data through its
12
+ I/O wrapper seams. Numeric and date text uses the invariant culture so the printed lines
13
+ do not depend on the host culture.
14
+ #>
15
+
16
+ . (Join-Path -Path $PSScriptRoot -ChildPath 'Get-EpicProgressReport.State.ps1')
17
+ . (Join-Path -Path $PSScriptRoot -ChildPath 'Get-EpicProgressReport.Metrics.ps1')
18
+ . (Join-Path -Path $PSScriptRoot -ChildPath 'Get-EpicProgressReport.Render.ps1')
19
+
20
+ function Test-EpicProgressJsonText {
21
+ <#
22
+ .SYNOPSIS
23
+ Return whether a text is non-blank, well-formed JSON; guards every JSON conversion.
24
+ .PARAMETER Json
25
+ The text to test. Blank and null text return $false.
26
+ #>
27
+ [CmdletBinding()]
28
+ [OutputType([bool])]
29
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [AllowNull()] [string] $Json)
30
+
31
+ if ([string]::IsNullOrWhiteSpace($Json)) { return $false }
32
+ return [bool] (Test-Json -Json $Json -ErrorAction SilentlyContinue)
33
+ }
34
+
35
+ function ConvertFrom-EpicProgressKickoffRow {
36
+ <#
37
+ .SYNOPSIS
38
+ Return one record (Issue, Wave, Band, PlanPath) per row of the kickoff feature table.
39
+ .DESCRIPTION
40
+ Only the feature-summary table is read: the table whose header row's first cell is
41
+ 'issue_num' or 'Issue' (case-insensitive). Lines before that header are skipped, and
42
+ the first line after it that is not a table row ends the table, so every other table
43
+ (for example the wave summary) is ignored. Each line is trimmed before it is matched
44
+ and split, so leading whitespace before the first '|' is allowed on every table line,
45
+ and a blank line ends the table. Inside the table, a feature row is a row
46
+ whose first cell is a numeric issue number of any length; any whitespace, including
47
+ none, may surround each cell, and the separator row is ignored. Cells 1, 3, 4, and 5
48
+ hold the issue, the wave, the band, and the repository-relative plan path. A feature
49
+ row whose wave cell is not all digits raises a terminating error naming the issue and
50
+ the cell, because a skipped row would understate the epic's task total. For the same
51
+ reason, a feature row with fewer than five cells, and a feature row whose plan cell is
52
+ blank, each raise a terminating error naming the issue. Kickoff text
53
+ that holds a non-whitespace character but no such header raises a terminating error;
54
+ empty or whitespace-only text returns no rows.
55
+ .PARAMETER KickoffText
56
+ The full text of the kickoff file.
57
+ #>
58
+ [CmdletBinding()]
59
+ [OutputType([pscustomobject])]
60
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $KickoffText)
61
+
62
+ $lines = $KickoffText -split '\r?\n'
63
+ $inFeatureTable = $false
64
+ foreach ($line in $lines) {
65
+ $trimmed = $line.Trim()
66
+ if (-not $inFeatureTable) {
67
+ $inFeatureTable = $trimmed -match '^\|\s*(issue_num|issue)\s*\|'
68
+ continue
69
+ }
70
+ if ($trimmed -notmatch '^\|') { break }
71
+ if ($trimmed -notmatch '^\|\s*(\d+)\s*\|') { continue }
72
+ $cells = @($trimmed -split '\|' | ForEach-Object { $_.Trim() })
73
+ $cellCount = $cells.Count - 1
74
+ if ($trimmed.EndsWith('|')) { $cellCount-- }
75
+ if ($cellCount -lt 5) {
76
+ throw "The kickoff row for issue #$($cells[1]) has $cellCount cells; a feature row needs 5 (issue, title, wave, band, plan path)."
77
+ }
78
+ if ([string]::IsNullOrWhiteSpace($cells[5])) {
79
+ throw "The kickoff row for issue #$($cells[1]) has a blank plan cell."
80
+ }
81
+ $wave = 0
82
+ if (-not [int]::TryParse($cells[3], [System.Globalization.NumberStyles]::None, [cultureinfo]::InvariantCulture, [ref] $wave)) {
83
+ throw "The kickoff row for issue #$($cells[1]) has a non-numeric wave cell '$($cells[3])'."
84
+ }
85
+ [pscustomobject]@{ Issue = $cells[1]; Wave = $wave; Band = $cells[4]; PlanPath = $cells[5] }
86
+ }
87
+ if (-not $inFeatureTable -and $KickoffText -match '\S') {
88
+ throw "The kickoff has no feature-summary table: no table header row whose first cell is 'issue_num' or 'Issue' was found."
89
+ }
90
+ }
91
+
92
+ function Get-EpicProgressIssueFromKickoff {
93
+ <#
94
+ .SYNOPSIS
95
+ Return the digits of the first 'Epic issue: #n' marker (mid-line allowed), or $null.
96
+ .PARAMETER KickoffText
97
+ The full text of the kickoff file.
98
+ #>
99
+ [CmdletBinding()]
100
+ [OutputType([string])]
101
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $KickoffText)
102
+
103
+ $match = [regex]::Match($KickoffText, 'Epic issue: #(\d+)')
104
+ if ($match.Success) { return $match.Groups[1].Value }
105
+ return $null
106
+ }
107
+
108
+ function ConvertFrom-EpicProgressWorktreeList {
109
+ <#
110
+ .SYNOPSIS
111
+ Return a map from branch name to worktree path from 'git worktree list --porcelain'.
112
+ .DESCRIPTION
113
+ Each 'worktree' line opens a record and the following 'branch refs/heads/' line
114
+ names its branch. Detached worktrees have no branch line and are omitted.
115
+ .PARAMETER PorcelainLine
116
+ The porcelain output lines.
117
+ #>
118
+ [CmdletBinding()]
119
+ [OutputType([hashtable])]
120
+ param([Parameter(Mandatory = $true)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $PorcelainLine)
121
+
122
+ $map = @{}
123
+ $worktreePath = $null
124
+ foreach ($entry in $PorcelainLine) {
125
+ if ($entry -like 'worktree *') {
126
+ $worktreePath = $entry.Substring(9)
127
+ } elseif ($entry -like 'branch refs/heads/*') {
128
+ $map[$entry.Substring(18)] = $worktreePath
129
+ }
130
+ }
131
+ return $map
132
+ }
133
+
134
+ function ConvertFrom-EpicProgressPrList {
135
+ <#
136
+ .SYNOPSIS
137
+ Return '#number state' for the first entry of a 'gh pr list --json number,state' array.
138
+ .DESCRIPTION
139
+ Returns an empty string for an empty array, blank or unparseable output, or an
140
+ entry without a number.
141
+ .PARAMETER Json
142
+ The JSON text printed by gh.
143
+ #>
144
+ [CmdletBinding()]
145
+ [OutputType([string])]
146
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Json)
147
+
148
+ if (-not (Test-EpicProgressJsonText -Json $Json)) { return '' }
149
+ $items = @(ConvertFrom-Json -InputObject $Json)
150
+ if ($items.Count -eq 0) { return '' }
151
+ $first = $items[0]
152
+ if ($null -eq $first -or -not ($first.PSObject.Properties.Name -contains 'number')) { return '' }
153
+ return "#$($first.number) $($first.state)"
154
+ }
155
+
156
+ function Get-EpicProgressWeeklyQuota {
157
+ <#
158
+ .SYNOPSIS
159
+ Return usage.sevenDay.pct of the first active account in 'cswap list --json' output.
160
+ .DESCRIPTION
161
+ Returns NaN when no account is active, the percentage is missing, or the output is
162
+ blank or unparseable.
163
+ .PARAMETER Json
164
+ The JSON text printed by cswap.
165
+ #>
166
+ [CmdletBinding()]
167
+ [OutputType([double])]
168
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Json)
169
+
170
+ if (-not (Test-EpicProgressJsonText -Json $Json)) { return [double]::NaN }
171
+ $data = ConvertFrom-Json -InputObject $Json
172
+ if ($null -eq $data -or -not ($data.PSObject.Properties.Name -contains 'accounts')) { return [double]::NaN }
173
+ $active = @($data.accounts | Where-Object { $null -ne $_ -and $_.active }) | Select-Object -First 1
174
+ if ($null -eq $active -or $null -eq $active.usage -or $null -eq $active.usage.sevenDay -or $null -eq $active.usage.sevenDay.pct) {
175
+ return [double]::NaN
176
+ }
177
+ return [double] $active.usage.sevenDay.pct
178
+ }
179
+
180
+ function Get-EpicProgressTaskCount {
181
+ <#
182
+ .SYNOPSIS
183
+ Return Total and Done counts of '- [ ] [P#-T#]' and '- [x] [P#-T#]' plan checkboxes.
184
+ .PARAMETER PlanText
185
+ The full plan text. Checkboxes without a task identifier are not counted.
186
+ #>
187
+ [CmdletBinding()]
188
+ [OutputType([pscustomobject])]
189
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $PlanText)
190
+
191
+ $taskMatches = [regex]::Matches($PlanText, '(?m)^\s*- \[( |x)\] \[P\d+-T\d+\]')
192
+ $done = 0
193
+ foreach ($taskMatch in $taskMatches) {
194
+ if ($taskMatch.Groups[1].Value -eq 'x') { $done++ }
195
+ }
196
+ return [pscustomobject]@{ Total = $taskMatches.Count; Done = $done }
197
+ }
198
+
199
+ function Get-EpicProgressCurrentPhase {
200
+ <#
201
+ .SYNOPSIS
202
+ Return the heading text of the phase that holds the first unchecked task.
203
+ .DESCRIPTION
204
+ Returns the first level-2 to level-4 'Phase N' heading without its hash marks,
205
+ 'Phase N' when no heading matches, and an empty string when every task is checked.
206
+ .PARAMETER PlanText
207
+ The full plan text.
208
+ #>
209
+ [CmdletBinding()]
210
+ [OutputType([string])]
211
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $PlanText)
212
+
213
+ $first = [regex]::Match($PlanText, '(?m)^\s*- \[ \] \[P(\d+)-T\d+\]')
214
+ if (-not $first.Success) { return '' }
215
+ $phaseNumber = $first.Groups[1].Value
216
+ $heading = [regex]::Match($PlanText, "(?m)^#{2,4}\s+Phase\s+$phaseNumber\b[^\r\n]*")
217
+ if ($heading.Success) { return ($heading.Value -replace '^#+\s+', '').Trim() }
218
+ return "Phase $phaseNumber"
219
+ }
220
+
221
+ function Get-EpicProgressFeatureState {
222
+ <#
223
+ .SYNOPSIS
224
+ Classify a feature as Completed, Running, or Pending.
225
+ .DESCRIPTION
226
+ Completed when the merge status is 'merged' or 'worktree_removed' or the PR state
227
+ contains MERGED; Running when a task is checked, the merge status is not
228
+ 'not_started', or a PR exists; Pending otherwise.
229
+ .PARAMETER MergeStatus
230
+ The checkpoint merge status ('not_started' when the feature has no entry).
231
+ .PARAMETER PrState
232
+ The pull-request text from ConvertFrom-EpicProgressPrList; empty when none.
233
+ .PARAMETER Done
234
+ The number of checked plan tasks.
235
+ #>
236
+ [CmdletBinding()]
237
+ [OutputType([string])]
238
+ param(
239
+ [Parameter(Mandatory = $true)] [string] $MergeStatus,
240
+ [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $PrState,
241
+ [Parameter(Mandatory = $true)] [int] $Done
242
+ )
243
+
244
+ $isMerged = ($MergeStatus -eq 'merged') -or ($MergeStatus -eq 'worktree_removed') -or ($PrState -match 'MERGED')
245
+ if ($isMerged) { return 'Completed' }
246
+ if ($Done -gt 0 -or $MergeStatus -ne 'not_started' -or $PrState) { return 'Running' }
247
+ return 'Pending'
248
+ }
@@ -0,0 +1,232 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Velocity, standard-deviation, estimate, and formatting functions for Get-EpicProgressReport.ps1.
4
+
5
+ .DESCRIPTION
6
+ Pure functions: each takes data in and returns data, and none reads a file, starts a
7
+ process, calls the network, or reads the clock. Numeric and time text uses the invariant
8
+ culture, and every non-finite value prints as n/a. Get-EpicProgressReport.Logic.ps1
9
+ dot-sources this file, so a consumer dot-sources the Logic hub, not this file.
10
+ #>
11
+
12
+ function Format-EpicProgressHour {
13
+ <#
14
+ .SYNOPSIS
15
+ Format hours as 'H h MM min' from the rounded total minutes; 'n/a' when not finite.
16
+ .PARAMETER Hours
17
+ The hours to format.
18
+ #>
19
+ [CmdletBinding()]
20
+ [OutputType([string])]
21
+ param([Parameter(Mandatory = $true)] [double] $Hours)
22
+
23
+ if ([double]::IsNaN($Hours) -or [double]::IsInfinity($Hours)) { return 'n/a' }
24
+ $minutes = [math]::Round($Hours * 60, [System.MidpointRounding]::AwayFromZero)
25
+ $wholeHours = [math]::Floor($minutes / 60)
26
+ return [string]::Format([cultureinfo]::InvariantCulture, '{0:N0} h {1:00} min', $wholeHours, ($minutes - 60 * $wholeHours))
27
+ }
28
+
29
+ function Format-EpicProgressClock {
30
+ <#
31
+ .SYNOPSIS
32
+ Format hours as 'HH:MM' from the rounded total minutes; 'n/a' when not finite.
33
+ .PARAMETER Hours
34
+ The hours to format.
35
+ #>
36
+ [CmdletBinding()]
37
+ [OutputType([string])]
38
+ param([Parameter(Mandatory = $true)] [double] $Hours)
39
+
40
+ if ([double]::IsNaN($Hours) -or [double]::IsInfinity($Hours)) { return 'n/a' }
41
+ $minutes = [math]::Round($Hours * 60)
42
+ $wholeHours = [math]::Floor($minutes / 60)
43
+ return [string]::Format([cultureinfo]::InvariantCulture, '{0:00}:{1:00}', $wholeHours, ($minutes - 60 * $wholeHours))
44
+ }
45
+
46
+ function Format-EpicProgressNumber {
47
+ <#
48
+ .SYNOPSIS
49
+ Return a number rounded to Digits decimals as invariant text; 'n/a' when not finite.
50
+ .PARAMETER Value
51
+ The number to format.
52
+ .PARAMETER Digits
53
+ The decimal digits kept (0 to 15). The default of 15 prints the value unrounded.
54
+ #>
55
+ [CmdletBinding()]
56
+ [OutputType([string])]
57
+ param(
58
+ [Parameter(Mandatory = $true)] [double] $Value,
59
+ [ValidateRange(0, 15)] [int] $Digits = 15
60
+ )
61
+
62
+ if ([double]::IsNaN($Value) -or [double]::IsInfinity($Value)) { return 'n/a' }
63
+ return ([math]::Round($Value, $Digits)).ToString([cultureinfo]::InvariantCulture)
64
+ }
65
+
66
+ function Get-EpicProgressAverageVelocity {
67
+ <#
68
+ .SYNOPSIS
69
+ Return (Done - BaselineDone) / ElapsedHours, and 0 when ElapsedHours is not above 0.
70
+ .PARAMETER Done
71
+ The checked task count now.
72
+ .PARAMETER BaselineDone
73
+ The checked task count at the start instant.
74
+ .PARAMETER ElapsedHours
75
+ The hours since the start instant.
76
+ #>
77
+ [CmdletBinding()]
78
+ [OutputType([double])]
79
+ param(
80
+ [Parameter(Mandatory = $true)] [int] $Done,
81
+ [Parameter(Mandatory = $true)] [int] $BaselineDone,
82
+ [Parameter(Mandatory = $true)] [double] $ElapsedHours
83
+ )
84
+
85
+ if ($ElapsedHours -gt 0) { return [double] ($Done - $BaselineDone) / $ElapsedHours }
86
+ return [double] 0
87
+ }
88
+
89
+ function Get-EpicProgressRecentVelocityInterval {
90
+ <#
91
+ .SYNOPSIS
92
+ Return the tasks-per-hour rate of each pair of consecutive samples at least 2 minutes apart.
93
+ .DESCRIPTION
94
+ Each rate is clamped to at least 0; pairs closer than 2 minutes are skipped.
95
+ .PARAMETER Sample
96
+ The retained samples, each with a 'ts' (Unix seconds) and a 'done' count.
97
+ #>
98
+ [CmdletBinding()]
99
+ [OutputType([double[]])]
100
+ param([Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Sample)
101
+
102
+ $intervals = [System.Collections.Generic.List[double]]::new()
103
+ for ($index = 1; $index -lt $Sample.Count; $index++) {
104
+ $hours = ([long] $Sample[$index].ts - [long] $Sample[$index - 1].ts) / 3600.0
105
+ if ($hours -ge (2 / 60)) {
106
+ $rate = ([double] $Sample[$index].done - [double] $Sample[$index - 1].done) / $hours
107
+ $intervals.Add([math]::Max(0.0, $rate))
108
+ }
109
+ }
110
+ return $intervals.ToArray()
111
+ }
112
+
113
+ function Get-EpicProgressStandardDeviation {
114
+ <#
115
+ .SYNOPSIS
116
+ Return the sample standard deviation (n - 1 denominator), or NaN below 3 intervals.
117
+ .PARAMETER Interval
118
+ The interval rates from Get-EpicProgressRecentVelocityInterval.
119
+ #>
120
+ [CmdletBinding()]
121
+ [OutputType([double])]
122
+ param([Parameter(Mandatory = $true)] [AllowEmptyCollection()] [double[]] $Interval)
123
+
124
+ if ($Interval.Count -lt 3) { return [double]::NaN }
125
+ $sum = 0.0
126
+ foreach ($value in $Interval) { $sum += $value }
127
+ $mean = $sum / $Interval.Count
128
+ $squares = 0.0
129
+ foreach ($value in $Interval) { $squares += ($value - $mean) * ($value - $mean) }
130
+ return [math]::Sqrt($squares / ($Interval.Count - 1))
131
+ }
132
+
133
+ function Get-EpicProgressCriticalPath {
134
+ <#
135
+ .SYNOPSIS
136
+ Return the wave-parallel critical-path estimate (Tasks, OpenWaves, Hours).
137
+ .DESCRIPTION
138
+ A wave is open when it holds a feature that is not Completed. Tasks sums, over open
139
+ waves, the largest remaining count among the wave's open features. Hours is
140
+ Tasks / Velocity plus WaveOverheadHours per open wave, and NaN when Velocity is
141
+ not above 0.
142
+ .PARAMETER Feature
143
+ The feature records (Wave, Total, Done, State).
144
+ .PARAMETER Velocity
145
+ The average velocity in tasks per hour.
146
+ .PARAMETER WaveOverheadHours
147
+ The review and CI overhead added per open wave.
148
+ #>
149
+ [CmdletBinding()]
150
+ [OutputType([pscustomobject])]
151
+ param(
152
+ [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Feature,
153
+ [Parameter(Mandatory = $true)] [double] $Velocity,
154
+ [Parameter(Mandatory = $true)] [double] $WaveOverheadHours
155
+ )
156
+
157
+ $largestRemaining = @{}
158
+ foreach ($record in $Feature) {
159
+ if ($record.State -eq 'Completed') { continue }
160
+ $remaining = [int] $record.Total - [int] $record.Done
161
+ $wave = [string] $record.Wave
162
+ if (-not $largestRemaining.ContainsKey($wave) -or $remaining -gt $largestRemaining[$wave]) {
163
+ $largestRemaining[$wave] = $remaining
164
+ }
165
+ }
166
+ $tasks = 0
167
+ foreach ($value in $largestRemaining.Values) { $tasks += $value }
168
+ $openWaves = $largestRemaining.Count
169
+ $hours = [double]::NaN
170
+ if ($Velocity -gt 0) { $hours = $tasks / $Velocity + $openWaves * $WaveOverheadHours }
171
+ return [pscustomobject]@{ Tasks = $tasks; OpenWaves = $openWaves; Hours = $hours }
172
+ }
173
+
174
+ function Get-EpicProgressRangeText {
175
+ <#
176
+ .SYNOPSIS
177
+ Return the estimated-time-remaining range text.
178
+ .DESCRIPTION
179
+ Below 3 intervals the text states that the range needs 3 or more check-in
180
+ intervals. Otherwise the fast and slow velocities are the average plus and minus
181
+ 2.05 standard deviations: a slow velocity above 0.5 tasks per hour gives a bounded
182
+ range, and otherwise the text states that there is no upper bound.
183
+ .PARAMETER AverageVelocity
184
+ The average velocity in tasks per hour.
185
+ .PARAMETER StandardDeviation
186
+ The sample standard deviation of the interval rates.
187
+ .PARAMETER IntervalCount
188
+ The number of check-in intervals.
189
+ .PARAMETER CriticalPath
190
+ The record from Get-EpicProgressCriticalPath.
191
+ .PARAMETER WaveOverheadHours
192
+ The review and CI overhead added per open wave.
193
+ #>
194
+ [CmdletBinding()]
195
+ [OutputType([string])]
196
+ param(
197
+ [Parameter(Mandatory = $true)] [double] $AverageVelocity,
198
+ [Parameter(Mandatory = $true)] [double] $StandardDeviation,
199
+ [Parameter(Mandatory = $true)] [int] $IntervalCount,
200
+ [Parameter(Mandatory = $true)] [pscustomobject] $CriticalPath,
201
+ [Parameter(Mandatory = $true)] [double] $WaveOverheadHours
202
+ )
203
+
204
+ if ($IntervalCount -lt 3) {
205
+ return "$(Format-EpicProgressClock -Hours $CriticalPath.Hours) hrs (range needs 3 or more check-in intervals)"
206
+ }
207
+ $overhead = $CriticalPath.OpenWaves * $WaveOverheadHours
208
+ $slowVelocity = $AverageVelocity - 2.05 * $StandardDeviation
209
+ $fastHours = [double] $CriticalPath.Tasks / ($AverageVelocity + 2.05 * $StandardDeviation) + $overhead
210
+ $fastText = Format-EpicProgressClock -Hours $fastHours
211
+ if ($slowVelocity -gt 0.5) {
212
+ $slowText = Format-EpicProgressClock -Hours ([double] $CriticalPath.Tasks / $slowVelocity + $overhead)
213
+ return "$fastText hrs to $slowText hrs"
214
+ }
215
+ return "$fastText hrs to no upper bound (average velocity minus 2.05 SD is at or below 0.5 tasks / hr)"
216
+ }
217
+
218
+ function Get-EpicProgressWeeklyText {
219
+ <#
220
+ .SYNOPSIS
221
+ Return a weekly quota percentage as 'N%', or 'n/a' when it is not finite.
222
+ .PARAMETER Value
223
+ The percentage.
224
+ #>
225
+ [CmdletBinding()]
226
+ [OutputType([string])]
227
+ param([Parameter(Mandatory = $true)] [double] $Value)
228
+
229
+ $text = Format-EpicProgressNumber -Value $Value
230
+ if ($text -eq 'n/a') { return $text }
231
+ return "$text%"
232
+ }
@@ -0,0 +1,196 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Summary and line-composition functions for Get-EpicProgressReport.ps1.
4
+
5
+ .DESCRIPTION
6
+ Pure functions: Get-EpicProgressSummary computes every value the report prints, and
7
+ Get-EpicProgressTldrLine and Get-EpicProgressFullLine compose the TL;DR and Full lines from
8
+ that summary. None reads a file, starts a process, calls the network, or reads the clock.
9
+ Get-EpicProgressReport.Logic.ps1 dot-sources this file, so a consumer dot-sources the Logic
10
+ hub, not this file.
11
+ #>
12
+
13
+ function Get-EpicProgressSummary {
14
+ <#
15
+ .SYNOPSIS
16
+ Return one record with every value the progress report prints.
17
+ .PARAMETER Feature
18
+ The feature records (Issue, Wave, Band, Total, Done, State, Phase, Pr).
19
+ .PARAMETER BaselineDone
20
+ The checked task count at the start instant.
21
+ .PARAMETER StartUtc
22
+ The start instant (UTC). A start after NowUtc counts as zero elapsed time, so the
23
+ report shows 0 h 00 min elapsed, zero average velocity, and n/a estimates.
24
+ .PARAMETER NowUtc
25
+ The current instant (UTC).
26
+ .PARAMETER Sample
27
+ The retained samples (ts, done).
28
+ .PARAMETER WaveOverheadHours
29
+ The review and CI overhead added per open wave.
30
+ .PARAMETER WeeklyPct
31
+ The weekly quota percentage now; NaN when unavailable.
32
+ .PARAMETER BaselineWeeklyPct
33
+ The weekly quota percentage at the start instant.
34
+ .PARAMETER EpicIssue
35
+ The epic issue number printed in the report.
36
+ .PARAMETER EpicName
37
+ The epic name printed in the status heading.
38
+ #>
39
+ [CmdletBinding()]
40
+ [OutputType([pscustomobject])]
41
+ param(
42
+ [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Feature,
43
+ [Parameter(Mandatory = $true)] [int] $BaselineDone,
44
+ [Parameter(Mandatory = $true)] [datetime] $StartUtc,
45
+ [Parameter(Mandatory = $true)] [datetime] $NowUtc,
46
+ [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]] $Sample,
47
+ [Parameter(Mandatory = $true)] [double] $WaveOverheadHours,
48
+ [Parameter(Mandatory = $true)] [double] $WeeklyPct,
49
+ [Parameter(Mandatory = $true)] [double] $BaselineWeeklyPct,
50
+ [Parameter(Mandatory = $true)] [string] $EpicIssue,
51
+ [Parameter(Mandatory = $true)] [string] $EpicName
52
+ )
53
+
54
+ $total = 0
55
+ $done = 0
56
+ foreach ($record in $Feature) {
57
+ $total += [int] $record.Total
58
+ $done += [int] $record.Done
59
+ }
60
+ $elapsedHours = [math]::Max(0.0, ($NowUtc - $StartUtc).TotalHours)
61
+ $velocity = Get-EpicProgressAverageVelocity -Done $done -BaselineDone $BaselineDone -ElapsedHours $elapsedHours
62
+ $straightHours = [double]::NaN
63
+ if ($velocity -gt 0) { $straightHours = ($total - $done) / $velocity }
64
+ $criticalPath = Get-EpicProgressCriticalPath -Feature $Feature -Velocity $velocity -WaveOverheadHours $WaveOverheadHours
65
+ [double[]] $intervals = @(Get-EpicProgressRecentVelocityInterval -Sample $Sample)
66
+ $recent = [double]::NaN
67
+ if ($intervals.Count -gt 0) { $recent = $intervals[$intervals.Count - 1] }
68
+ $deviation = Get-EpicProgressStandardDeviation -Interval $intervals
69
+
70
+ return [pscustomobject]@{
71
+ EpicIssue = $EpicIssue
72
+ EpicName = $EpicName
73
+ Feature = $Feature
74
+ Completed = [object[]] @($Feature | Where-Object { $_.State -eq 'Completed' })
75
+ Running = [object[]] @($Feature | Where-Object { $_.State -eq 'Running' })
76
+ Pending = [object[]] @($Feature | Where-Object { $_.State -eq 'Pending' })
77
+ Total = $total
78
+ Done = $done
79
+ Percent = 100.0 * $done / [math]::Max(1, $total)
80
+ StartUtc = $StartUtc
81
+ NowUtc = $NowUtc
82
+ ElapsedHours = $elapsedHours
83
+ Velocity = $velocity
84
+ Remaining = $total - $done
85
+ StraightHours = $straightHours
86
+ CriticalPath = $criticalPath
87
+ WaveOverheadHours = $WaveOverheadHours
88
+ IntervalCount = $intervals.Count
89
+ RecentVelocity = $recent
90
+ StandardDeviation = $deviation
91
+ RangeText = Get-EpicProgressRangeText -AverageVelocity $velocity -StandardDeviation $deviation -IntervalCount $intervals.Count -CriticalPath $criticalPath -WaveOverheadHours $WaveOverheadHours
92
+ WeeklyPct = $WeeklyPct
93
+ WeeklyDelta = $WeeklyPct - $BaselineWeeklyPct
94
+ }
95
+ }
96
+
97
+ function Get-EpicProgressTldrLine {
98
+ <#
99
+ .SYNOPSIS
100
+ Return the four TL;DR lines (TL;DR, Elapsed, velocity, estimated time remaining).
101
+ .PARAMETER Summary
102
+ The record from Get-EpicProgressSummary.
103
+ #>
104
+ [CmdletBinding()]
105
+ [OutputType([string[]])]
106
+ param([Parameter(Mandatory = $true)] [pscustomobject] $Summary)
107
+
108
+ $featureCount = $Summary.Feature.Count
109
+ $running = $Summary.Running
110
+ $runSentence = 'No feature is running.'
111
+ if ($running.Count -eq 1) {
112
+ $runSentence = "#$($running[0].Issue) is running ($($running[0].Done) of $($running[0].Total))."
113
+ } elseif ($running.Count -gt 1) {
114
+ $runSentence = (($running | ForEach-Object { "#$($_.Issue) ($($_.Done) of $($_.Total))" }) -join ', ') + ' are running.'
115
+ }
116
+ $doneSentence = "None of the $featureCount features is done yet."
117
+ if ($Summary.Completed.Count -gt 0) { $doneSentence = "$($Summary.Completed.Count) of $featureCount features are done." }
118
+ $percent = Format-EpicProgressNumber -Value $Summary.Percent -Digits 1
119
+ $hoursLeft = Format-EpicProgressNumber -Value $Summary.CriticalPath.Hours -Digits 0
120
+ $velocity = Format-EpicProgressNumber -Value $Summary.Velocity -Digits 1
121
+ $recent = Format-EpicProgressNumber -Value $Summary.RecentVelocity -Digits 1
122
+ $deviation = Format-EpicProgressNumber -Value $Summary.StandardDeviation -Digits 1
123
+ $weekly = Get-EpicProgressWeeklyText -Value $Summary.WeeklyPct
124
+
125
+ return [string[]] @(
126
+ "**TL;DR:** Epic #$($Summary.EpicIssue) is at $($Summary.Done) of $($Summary.Total) tasks ($percent%). $doneSentence $runSentence"
127
+ "**Elapsed:** $(Format-EpicProgressHour -Hours $Summary.ElapsedHours), with about $hoursLeft hours left on the wave-parallel estimate. **Weekly** quota is at $weekly."
128
+ "**Avg Velocity:** $velocity tasks / hr. **Recent Velocity:** $recent tasks / hr since last check-in. (SD of recent velocity: $deviation, from $($Summary.IntervalCount) intervals)"
129
+ "**Estimated time remaining:** $($Summary.RangeText)"
130
+ )
131
+ }
132
+
133
+ function Get-EpicProgressFullLine {
134
+ <#
135
+ .SYNOPSIS
136
+ Return the Full-mode lines that follow the TL;DR block.
137
+ .DESCRIPTION
138
+ The status heading, the Elapsed and Progress line, the State table, the Wave table
139
+ with its Total row, the Velocity and Remaining line, the straight-line and
140
+ wave-parallel lines, and the weekly-quota line, with invariant-culture times.
141
+ .PARAMETER Summary
142
+ The record from Get-EpicProgressSummary.
143
+ #>
144
+ [CmdletBinding()]
145
+ [OutputType([string[]])]
146
+ param([Parameter(Mandatory = $true)] [pscustomobject] $Summary)
147
+
148
+ $culture = [cultureinfo]::InvariantCulture
149
+ $percent = Format-EpicProgressNumber -Value $Summary.Percent -Digits 1
150
+ $lines = [System.Collections.Generic.List[string]]::new()
151
+ $lines.Add('')
152
+ $lines.Add("## Epic #$($Summary.EpicIssue) $($Summary.EpicName): status at $($Summary.NowUtc.ToString('HH:mm', $culture)) UTC, $($Summary.NowUtc.ToString('MMM d', $culture))")
153
+ $lines.Add('')
154
+ $lines.Add("**Elapsed:** $(Format-EpicProgressHour -Hours $Summary.ElapsedHours) since $($Summary.StartUtc.ToString('HH:mm', $culture)) UTC. **Progress:** $($Summary.Done) of $($Summary.Total) tasks complete, **$percent%**.")
155
+ $lines.Add('')
156
+ $lines.Add('| State | Features |')
157
+ $lines.Add('|---|---|')
158
+ $completedText = 'None yet'
159
+ if ($Summary.Completed.Count -gt 0) { $completedText = ($Summary.Completed | ForEach-Object { "#$($_.Issue)" }) -join ', ' }
160
+ $lines.Add("| Completed | $completedText |")
161
+ foreach ($record in $Summary.Running) {
162
+ $pullRequest = ''
163
+ if ($record.Pr) { $pullRequest = " PR $($record.Pr)." }
164
+ $phase = ''
165
+ if ($record.Phase) { $phase = " Now at: $($record.Phase)." }
166
+ $share = Format-EpicProgressNumber -Value (100.0 * $record.Done / [math]::Max(1, $record.Total)) -Digits 0
167
+ $lines.Add("| Running | #$($record.Issue) (wave $($record.Wave), $($record.Band)): $($record.Done) of $($record.Total) tasks ($share%).$phase$pullRequest |")
168
+ }
169
+ $pendingGroups = @($Summary.Pending | Group-Object -Property Wave | Sort-Object -Property { [int] $_.Name })
170
+ foreach ($group in $pendingGroups) {
171
+ $lines.Add("| Pending, wave $($group.Name) | " + (($group.Group | ForEach-Object { "#$($_.Issue) ($($_.Total))" }) -join ', ') + ' |')
172
+ }
173
+ $lines.Add('')
174
+ $lines.Add('| Wave | Children | Tasks | Done |')
175
+ $lines.Add('|---|---|---|---|')
176
+ $waveGroups = @($Summary.Feature | Group-Object -Property Wave | Sort-Object -Property { [int] $_.Name })
177
+ foreach ($group in $waveGroups) {
178
+ $waveTotal = 0
179
+ $waveDone = 0
180
+ foreach ($record in $group.Group) {
181
+ $waveTotal += [int] $record.Total
182
+ $waveDone += [int] $record.Done
183
+ }
184
+ $lines.Add("| $($group.Name) | $($group.Count) | $waveTotal | $waveDone |")
185
+ }
186
+ $lines.Add("| **Total** | **$($Summary.Feature.Count)** | **$($Summary.Total)** | **$($Summary.Done)** |")
187
+ $lines.Add('')
188
+ $lines.Add("**Velocity:** $(Format-EpicProgressNumber -Value $Summary.Velocity -Digits 1) tasks per hour (one child's pace). **Remaining:** $($Summary.Remaining) tasks.")
189
+ $lines.Add("- Straight line, no parallelism: about $(Format-EpicProgressHour -Hours $Summary.StraightHours) (upper bound).")
190
+ $overhead = Format-EpicProgressNumber -Value $Summary.WaveOverheadHours
191
+ $lines.Add("- Wave-parallel (longest chain $($Summary.CriticalPath.Tasks) tasks plus about $overhead h per open wave for review and CI): about **$(Format-EpicProgressHour -Hours $Summary.CriticalPath.Hours)**.")
192
+ $delta = Format-EpicProgressNumber -Value $Summary.WeeklyDelta
193
+ if ($delta -ne 'n/a' -and $Summary.WeeklyDelta -ge 0) { $delta = "+$delta" }
194
+ $lines.Add("**Weekly quota (active account):** $(Get-EpicProgressWeeklyText -Value $Summary.WeeklyPct) used, $delta points since the epic started. This pool also covers the other runs in this session, so it is not the epic's cost alone.")
195
+ return $lines.ToArray()
196
+ }