@danmoisan/drm-copilot-mcp 1.1.11 → 1.1.12
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.
- package/out/mcp-server.js +366 -159
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +33 -37
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/pack-manifests/core.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +18 -1
- package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +26 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quota-throttling
|
|
3
|
+
description: 'Quota throttling and account-switching policy for multi-agent orchestration. Use when pacing agent work against five-hour and seven-day usage limits, deciding whether and when to switch accounts, setting concurrency caps, forecasting a rate-limit wall, handling total weekly exhaustion, or designing a usage monitor. Covers account viability, failover-dependent pacing targets, the measured cost model, commit-before-the-wall, and the coordinator/agent division of labour.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Quota Throttling and Account-Switching Policy
|
|
7
|
+
|
|
8
|
+
Policy for keeping long-running, multi-agent orchestration inside account usage limits without
|
|
9
|
+
losing work. Derived from about five days of continuous multi-agent orchestration (TaskMaster runs
|
|
10
|
+
`bugs-2026-09-11` and `bugs-2026-09-17`).
|
|
11
|
+
|
|
12
|
+
Every figure marked **MEASURED** was observed; figures marked **ESTIMATE** are extrapolations.
|
|
13
|
+
Cost figures are machine- and repository-specific. Re-measure before relying on them elsewhere.
|
|
14
|
+
|
|
15
|
+
## When to Use This Skill
|
|
16
|
+
|
|
17
|
+
Use this skill when:
|
|
18
|
+
|
|
19
|
+
- a coordinating session runs orchestrators, epic runs, or parallel runs that spawn subagents;
|
|
20
|
+
- deciding whether to start, stagger, hold, or stop agent work because of usage limits;
|
|
21
|
+
- choosing whether and when to switch between accounts;
|
|
22
|
+
- a rate-limit wall is forecast, or every account is exhausted;
|
|
23
|
+
- designing a monitor that watches usage or CI state.
|
|
24
|
+
|
|
25
|
+
## Reading Quota
|
|
26
|
+
|
|
27
|
+
The coordinator reads usage from the account switcher, for example `cswap list --json`. The
|
|
28
|
+
fields each rule below depends on:
|
|
29
|
+
|
|
30
|
+
| Field | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `accounts[].active` | Whether the account currently supplies credentials. |
|
|
33
|
+
| `accounts[].usageStatus` | Must be `ok`; any other value makes the account non-viable. |
|
|
34
|
+
| `usage.fiveHour.pct` | Five-hour window utilization, an integer percentage. |
|
|
35
|
+
| `usage.fiveHour.resetsAt` | Authoritative ISO-8601 UTC reset time. `null` means the window has not started. |
|
|
36
|
+
| `usage.sevenDay.pct` | Seven-day utilization, an integer percentage. |
|
|
37
|
+
| `usage.sevenDay.resetsAt` | Authoritative ISO-8601 UTC weekly reset time. |
|
|
38
|
+
| `usage.sevenDay.willLastToReset` | The switcher's own projection of whether the weekly budget lasts. |
|
|
39
|
+
| `usage.*.clock`, `usage.*.countdown` | Human-readable display values. `clock` is LOCAL time; do not use it for arithmetic. |
|
|
40
|
+
|
|
41
|
+
Some switchers switch automatically on the five-hour threshold alone and do not consult the
|
|
42
|
+
seven-day window. Such a switcher can hand over to an account whose five-hour window has reset
|
|
43
|
+
but whose weekly budget is spent. Apply section 1 yourself rather than relying on the switcher.
|
|
44
|
+
|
|
45
|
+
## 1. Account Viability
|
|
46
|
+
|
|
47
|
+
An account is a viable target only if BOTH hold:
|
|
48
|
+
|
|
49
|
+
- seven-day usage < 95%
|
|
50
|
+
- five-hour usage < 90%
|
|
51
|
+
|
|
52
|
+
The weekly gate dominates. An account at >= 95% weekly is NOT a viable target regardless of its
|
|
53
|
+
five-hour state, and must be excluded from all pacing math.
|
|
54
|
+
|
|
55
|
+
The binding budget is `min(five-hour headroom to 90%, weekly headroom to 95%)`. Compute both every
|
|
56
|
+
time. Late in a week the weekly headroom is usually the smaller number, and the five-hour window
|
|
57
|
+
is then irrelevant.
|
|
58
|
+
|
|
59
|
+
## 2. Pacing Target Depends on Whether a Failover Exists
|
|
60
|
+
|
|
61
|
+
**Two or more viable accounts: front-load.** Target about 90% of the five-hour window in about
|
|
62
|
+
2 hours (about 45% per hour). Reaching 90% triggers a switch, so the unused remainder of the
|
|
63
|
+
window costs nothing.
|
|
64
|
+
|
|
65
|
+
**Exactly one viable account: spread.** Target about 90% across the WHOLE five-hour window (about
|
|
66
|
+
18% per hour). Front-loading here is wrong: reaching 90% at the 2-hour mark produces a stall of
|
|
67
|
+
about 3 hours with no account to switch to.
|
|
68
|
+
|
|
69
|
+
These are different policies, not different settings of one policy. Applying the two-account
|
|
70
|
+
target to a one-account situation is the most expensive mistake observed (see section 12,
|
|
71
|
+
item 1).
|
|
72
|
+
|
|
73
|
+
## 3. Cost Model (MEASURED, Serialized Single Items)
|
|
74
|
+
|
|
75
|
+
| Activity | Five-hour points | Weekly points | Wall clock |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| Coordination only (API calls, git, checkpoint writes, no agents) | ~3 total | negligible | — |
|
|
78
|
+
| One item, full cycle (child + executor + toolchain + coverage run) | ~17 | ~3 | ~43 min |
|
|
79
|
+
| One plan revision, or a low-cost item | ~4-5 | — | — |
|
|
80
|
+
| Three items entering Phase 0 simultaneously | 96-108 per hour | — | a full five-hour window in 62 min |
|
|
81
|
+
|
|
82
|
+
Builds and test runs consume wall-clock time but zero quota. A long quiet period at 0% per hour
|
|
83
|
+
with agents alive usually means they are inside a build, not stalled.
|
|
84
|
+
|
|
85
|
+
Ratio (ESTIMATE, two data points): one full five-hour window is about 14 weekly points.
|
|
86
|
+
|
|
87
|
+
## 4. Concurrency Control
|
|
88
|
+
|
|
89
|
+
A concurrency cap governs an orchestrator's DIRECT children. Each child spawns its own
|
|
90
|
+
grandchildren (executors, planners, researchers), and those are the dominant consumers. Always
|
|
91
|
+
express a cap as a budget on TOTAL AGENTS IN THE SUBTREE, and require that it be propagated
|
|
92
|
+
verbatim to every level.
|
|
93
|
+
|
|
94
|
+
A cap change only restricts NEW starts. It does nothing to work already running. Cutting a cap
|
|
95
|
+
from 6 to 2 mid-flight changed the burn rate by zero, because the cost was in agents already
|
|
96
|
+
alive.
|
|
97
|
+
|
|
98
|
+
For in-flight burn there are exactly two controls:
|
|
99
|
+
|
|
100
|
+
- **Commit-and-hold** — finish the current task, commit, start nothing new.
|
|
101
|
+
- **Stopping agents outright** — destructive; use only as a last resort.
|
|
102
|
+
|
|
103
|
+
**Stagger starts.** The dominant cost driver is not item count; it is simultaneous Phase 0
|
|
104
|
+
cycles. Bring items in one at a time and let each settle before starting the next.
|
|
105
|
+
|
|
106
|
+
## 5. Measurement Discipline
|
|
107
|
+
|
|
108
|
+
- The usage percentage is an INTEGER. At 5-minute sampling the computed rate quantizes to
|
|
109
|
+
multiples of about 12% per hour. Do not react to a single sample; measure over spans of at
|
|
110
|
+
least 15 minutes.
|
|
111
|
+
- The FIRST sample after an account switch is inflated by batched accounting. Take a second
|
|
112
|
+
reading before acting on it, but do not dismiss it either; it has been partly real more than
|
|
113
|
+
once.
|
|
114
|
+
- Reset times: the human-readable `clock` field is LOCAL time. The ISO `resetsAt` field is
|
|
115
|
+
authoritative. Convert before doing arithmetic; a four-hour error from this changed a plan.
|
|
116
|
+
- Five-hour window elapsed time = 300 − (minutes until reset). The window starts on first use,
|
|
117
|
+
not on a fixed clock boundary.
|
|
118
|
+
- VERIFY a reset directly before resuming. Never infer it from the clock. A reset timestamp can
|
|
119
|
+
pass while the counter still reads the old value for a minute or two.
|
|
120
|
+
|
|
121
|
+
## 6. Approaching a Wall
|
|
122
|
+
|
|
123
|
+
Commit BEFORE the wall, not at it. A rate-limit error terminates an agent immediately; the agent
|
|
124
|
+
does not wait out the gap and resume. The duration of the outage does not change this risk: even
|
|
125
|
+
a 60-second gap terminates in-flight work.
|
|
126
|
+
|
|
127
|
+
Sequence when a wall is forecast:
|
|
128
|
+
|
|
129
|
+
1. Order all in-flight children to COMMIT NOW, in whatever state, without finishing the current
|
|
130
|
+
task. A committed partial result is recoverable; an uncommitted complete one is not.
|
|
131
|
+
2. Let them keep working until the wall actually arrives.
|
|
132
|
+
3. Set the commit checkpoint with real margin, and recompute it as the rate moves. A checkpoint
|
|
133
|
+
that lands ON the forecast wall protects nothing.
|
|
134
|
+
|
|
135
|
+
Git operations are LOCAL and consume no model quota; they succeed on a fully exhausted account.
|
|
136
|
+
If errors start before committing finishes, keep retrying the COMMIT specifically and nothing
|
|
137
|
+
else.
|
|
138
|
+
|
|
139
|
+
## 7. Total Weekly Exhaustion
|
|
140
|
+
|
|
141
|
+
When no account is viable:
|
|
142
|
+
|
|
143
|
+
1. **Serialize** — stop all parallel activity; zero new starts.
|
|
144
|
+
2. **Commit ALL working documents across ALL worktrees** — every item worktree, the plan-home
|
|
145
|
+
worktree, and every in-flight child, not only the current item.
|
|
146
|
+
3. **Report and hold.**
|
|
147
|
+
|
|
148
|
+
This differs from a routine pause: the outage may last days, so nothing may be left uncommitted
|
|
149
|
+
on the assumption of a quick return.
|
|
150
|
+
|
|
151
|
+
## 8. Switching
|
|
152
|
+
|
|
153
|
+
- Switch at 90% of the five-hour window IF a viable target exists.
|
|
154
|
+
- If no viable target exists, do NOT switch; hold until the earliest reset. Switching onto an
|
|
155
|
+
exhausted account produces immediate failures.
|
|
156
|
+
- Verify the switch landed (active account, both windows) before releasing work.
|
|
157
|
+
- A mid-flight credential change is transparent to running agents. Instruct them explicitly not
|
|
158
|
+
to treat the change, or a brief error around it, as a failure signal.
|
|
159
|
+
|
|
160
|
+
## 9. Usage Credits and Overage
|
|
161
|
+
|
|
162
|
+
Never spend credits on work. The ONLY sanctioned use is completing a non-destructive
|
|
163
|
+
serialization and commit so that work is not lost. If any path offers to continue past a limit
|
|
164
|
+
by consuming credits, stop and ask the user.
|
|
165
|
+
|
|
166
|
+
## 10. Reserve Policy
|
|
167
|
+
|
|
168
|
+
Holding an account's last few points as an emergency commit reserve is correct ONLY while
|
|
169
|
+
something is uncommitted. Verify first: if every branch is pushed and every worktree is clean,
|
|
170
|
+
the reserve protects nothing and should be spent on useful work instead. Check before assuming.
|
|
171
|
+
|
|
172
|
+
## 11. Monitor Design
|
|
173
|
+
|
|
174
|
+
Report on thresholds, not on every sample. An idle monitor that fires every ten minutes costs a
|
|
175
|
+
turn each time and hides the signal among routine readings.
|
|
176
|
+
|
|
177
|
+
**A check for the ABSENCE of a bad state is satisfied by the absence of ALL state.** A CI waiter
|
|
178
|
+
polling for "no pending checks" reported SETTLED during the window between one run being
|
|
179
|
+
superseded and the next registering, when no checks existed at all. Require a POSITIVE assertion
|
|
180
|
+
instead: a conjunction that an empty set cannot satisfy (for example, "at least one required check
|
|
181
|
+
exists AND none is pending").
|
|
182
|
+
|
|
183
|
+
Alert on:
|
|
184
|
+
|
|
185
|
+
- threshold crossings;
|
|
186
|
+
- a sustained rate above target;
|
|
187
|
+
- a non-`ok` status;
|
|
188
|
+
- account-viability changes;
|
|
189
|
+
- window rollovers.
|
|
190
|
+
|
|
191
|
+
Stay silent otherwise.
|
|
192
|
+
|
|
193
|
+
## 12. Observed Failure Modes
|
|
194
|
+
|
|
195
|
+
1. Applying the two-account front-load target with only one viable account.
|
|
196
|
+
Cost: about 95 minutes of idle time.
|
|
197
|
+
2. Cutting a concurrency cap to control burn from work already running.
|
|
198
|
+
Cost: none directly, but the effective control went unused while the window drained.
|
|
199
|
+
3. Launching multiple items into simultaneous Phase 0.
|
|
200
|
+
Cost: a full five-hour window in 62 minutes.
|
|
201
|
+
4. Assuming a fallback account was fresh without checking.
|
|
202
|
+
Cost: about 1 hour, and seven items' preparation output stranded uncommitted when agents were
|
|
203
|
+
terminated.
|
|
204
|
+
5. Setting a commit checkpoint that landed on the forecast wall instead of before it.
|
|
205
|
+
6. Reacting to single 5-minute samples inside the quantization band, producing cap oscillation
|
|
206
|
+
that achieved nothing.
|
|
207
|
+
|
|
208
|
+
## 13. Division of Labour
|
|
209
|
+
|
|
210
|
+
An orchestrating agent generally CANNOT see quota. Do not ask it to estimate usage. The workable
|
|
211
|
+
split is:
|
|
212
|
+
|
|
213
|
+
- the **agent** reports its COST PROFILE: how many agents are alive, what kind of work they are
|
|
214
|
+
doing, and what it is about to start;
|
|
215
|
+
- the **coordinator** supplies the MEASUREMENT and the decision.
|
|
216
|
+
|
|
217
|
+
Have the agent announce before starting each item, and reply with a live reading.
|
|
218
|
+
|
|
219
|
+
Corollary: if the coordinator is slow to respond, the agent should proceed on its own rather than
|
|
220
|
+
stall. A four-hour idle period occurred because an agent correctly held for a ruling that was
|
|
221
|
+
never sent. Absorbing a small pacing error is preferable to repeating that.
|
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
".claude/hooks/enforce-discovery-artifact-gate.ps1",
|
|
28
28
|
".claude/hooks/enforce-epic-invocation-origin.ps1",
|
|
29
29
|
".claude/hooks/enforce-epic-merge-gate.ps1",
|
|
30
|
+
".claude/hooks/enforce-epic-merge-gate-authorization.ps1",
|
|
30
31
|
".claude/hooks/enforce-epic-wave-barrier.ps1",
|
|
31
32
|
".claude/hooks/enforce-epic-worktree-removal-gate.ps1",
|
|
32
33
|
".claude/hooks/enforce-evidence-locations.ps1",
|
|
33
34
|
".claude/hooks/enforce-feature-folder-order.ps1",
|
|
34
35
|
".claude/hooks/enforce-model-routing-receipt.ps1",
|
|
35
36
|
".claude/hooks/enforce-orchestration-preimplementation-gate.ps1",
|
|
37
|
+
".claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1",
|
|
36
38
|
".claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1",
|
|
37
39
|
".claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1",
|
|
38
40
|
".claude/hooks/enforce-parallel-abandon-gate.ps1",
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
".claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1",
|
|
45
47
|
".claude/hooks/enforce-pr-author-skill.ps1",
|
|
46
48
|
".claude/hooks/enforce-pr-author-skill-helpers.ps1",
|
|
49
|
+
".claude/hooks/enforce-prd-feature-before-planner-helpers.ps1",
|
|
47
50
|
".claude/hooks/enforce-prd-feature-before-planner.ps1",
|
|
48
51
|
".claude/hooks/enforce-promotion-mcp-only.ps1",
|
|
49
52
|
".claude/hooks/hook-command-invocation.ps1",
|
|
@@ -103,6 +106,7 @@
|
|
|
103
106
|
".claude/skills/pr-author/SKILL.md",
|
|
104
107
|
".claude/skills/pr-base-branch-merge-base/SKILL.md",
|
|
105
108
|
".claude/skills/pr-context-artifacts/SKILL.md",
|
|
109
|
+
".claude/skills/quota-throttling/SKILL.md",
|
|
106
110
|
".claude/skills/remediation-handoff-atomic-planner/SKILL.md",
|
|
107
111
|
".claude/skills/research-issue/SKILL.md",
|
|
108
112
|
".claude/skills/review-epic/SKILL.md",
|
|
@@ -163,6 +167,11 @@
|
|
|
163
167
|
".claude/lib/mermaid/MermaidLineScanner.psm1",
|
|
164
168
|
".claude/lib/mermaid/MermaidMarkdownFences.psm1",
|
|
165
169
|
".claude/lib/mermaid/MermaidValidation.psm1",
|
|
170
|
+
".claude/lib/worktree-resolution/WorktreeResolution.psm1",
|
|
171
|
+
".claude/lib/worktree-resolution/WorktreeTargetResolution.psm1",
|
|
172
|
+
".claude/lib/worktree-resolution/WorktreeItemResolution.psm1",
|
|
173
|
+
".claude/lib/worktree-resolution/EpicScopeResolution.psm1",
|
|
174
|
+
".claude/lib/worktree-resolution/EpicScopeReadiness.psm1",
|
|
166
175
|
".claude/skills/mermaid-diagram/references/flowchart.md",
|
|
167
176
|
".claude/skills/mermaid-diagram/references/sequence.md",
|
|
168
177
|
".claude/skills/mermaid-diagram/references/class.md",
|
package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md
CHANGED
|
@@ -16,7 +16,7 @@ agent profile.
|
|
|
16
16
|
2. Run the canonical topology resolver before choosing a logical agent:
|
|
17
17
|
|
|
18
18
|
```powershell
|
|
19
|
-
|
|
19
|
+
pwsh -NoProfile -File .codex/scripts/Resolve-CodexTopology.ps1 `
|
|
20
20
|
--language <python|powershell|csharp|typescript> `
|
|
21
21
|
--production-file-count <count> `
|
|
22
22
|
--test-file-count <count> `
|
|
@@ -53,7 +53,7 @@ epic root personas are forced independently of file count.
|
|
|
53
53
|
5. Run the canonical resolver:
|
|
54
54
|
|
|
55
55
|
```powershell
|
|
56
|
-
|
|
56
|
+
pwsh -NoProfile -File .codex/scripts/Resolve-CodexDeployment.ps1 `
|
|
57
57
|
--logical-agent <agent> `
|
|
58
58
|
--complexity-band <C1|C2|C3|C4> `
|
|
59
59
|
--execution-context <context> `
|
|
@@ -96,15 +96,14 @@ incomplete, and request a policy change. Do not silently fall back.
|
|
|
96
96
|
## Validation
|
|
97
97
|
|
|
98
98
|
Before accepting delegated results or reporting completion, validate the
|
|
99
|
-
checkpoint with the Codex routing gate
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
when available.
|
|
99
|
+
checkpoint with the Codex routing gate by calling the MCP tool
|
|
100
|
+
`validate_orchestration_artifacts` with these arguments:
|
|
101
|
+
|
|
102
|
+
- `artifact_type`: `orchestrator-state` (use `epic-orchestrator-state` for epic
|
|
103
|
+
execution)
|
|
104
|
+
- `artifact_path`: `artifacts/orchestration/orchestrator-state.json`
|
|
105
|
+
- `workspace_root`: the repository root
|
|
106
|
+
- `require_codex_topology`: `true`
|
|
107
|
+
- `require_codex_model_routing`: `true`
|
|
108
|
+
|
|
109
|
+
The MCP tool is the authoritative completion gate and needs no Python toolchain.
|
|
@@ -11,6 +11,14 @@ param()
|
|
|
11
11
|
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
12
12
|
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
13
13
|
|
|
14
|
+
# Reason codes for the standalone-merge authorization branch (issue #670), spelled
|
|
15
|
+
# byte-identically to the Claude helpers file. Each of the four assignments below is the only
|
|
16
|
+
# place its code literal appears in this file.
|
|
17
|
+
$script:CodexStandaloneAbsentReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_ABSENT'
|
|
18
|
+
$script:CodexStandalonePrMismatchReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_PR_MISMATCH'
|
|
19
|
+
$script:CodexStandaloneMalformedReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_MALFORMED'
|
|
20
|
+
$script:CodexStandaloneNotPrSpecificReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_NOT_PR_SPECIFIC'
|
|
21
|
+
|
|
14
22
|
function ConvertFrom-CodexMergeJson {
|
|
15
23
|
[CmdletBinding()]
|
|
16
24
|
param([AllowNull()][AllowEmptyString()][string] $Raw, [Parameter(Mandatory)][string] $Name, [switch] $Optional)
|
|
@@ -155,6 +163,24 @@ function Invoke-CodexEpicMergeDecision {
|
|
|
155
163
|
return $null
|
|
156
164
|
}
|
|
157
165
|
|
|
166
|
+
# Standalone branch (issue #670), evaluated last and only for an explicit PR number: a
|
|
167
|
+
# PR-specific authorization record bound to the payload session. Its deny keeps the gate
|
|
168
|
+
# token first.
|
|
169
|
+
if ($null -ne $prNumber) {
|
|
170
|
+
$sessionId = [string]$payload.session_id
|
|
171
|
+
$verdict = Test-CodexStandaloneMergeAuthorization -Checkpoints @($child, $epic) -CommandPrNumber $prNumber -SessionId $sessionId
|
|
172
|
+
if ($verdict.Allowed) {
|
|
173
|
+
return $null
|
|
174
|
+
}
|
|
175
|
+
return [ordered]@{
|
|
176
|
+
hookSpecificOutput = [ordered]@{
|
|
177
|
+
hookEventName = 'PreToolUse'
|
|
178
|
+
permissionDecision = 'deny'
|
|
179
|
+
permissionDecisionReason = 'EPIC_MERGE_GATE_BLOCKED: ' + $verdict.ReasonCode + ': ' + $verdict.Message
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
158
184
|
return [ordered]@{
|
|
159
185
|
hookSpecificOutput = [ordered]@{
|
|
160
186
|
hookEventName = 'PreToolUse'
|
|
@@ -164,6 +190,173 @@ function Invoke-CodexEpicMergeDecision {
|
|
|
164
190
|
}
|
|
165
191
|
}
|
|
166
192
|
|
|
193
|
+
function Test-CodexStandalonePositiveInteger {
|
|
194
|
+
<#
|
|
195
|
+
.SYNOPSIS
|
|
196
|
+
Report whether a parsed JSON value is an integer greater than zero; a digit string fails.
|
|
197
|
+
#>
|
|
198
|
+
[CmdletBinding()]
|
|
199
|
+
[OutputType([bool])]
|
|
200
|
+
param([AllowNull()] $Value)
|
|
201
|
+
|
|
202
|
+
return (($Value -is [int] -or $Value -is [long]) -and $Value -gt 0)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function Test-CodexStandaloneText {
|
|
206
|
+
<#
|
|
207
|
+
.SYNOPSIS
|
|
208
|
+
Report whether a value is a string whose trimmed length reaches a minimum.
|
|
209
|
+
#>
|
|
210
|
+
[CmdletBinding()]
|
|
211
|
+
[OutputType([bool])]
|
|
212
|
+
param([AllowNull()] $Value, [int] $MinimumLength = 1)
|
|
213
|
+
|
|
214
|
+
return ($Value -is [string] -and $Value.Trim().Length -ge $MinimumLength)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Get-CodexStandaloneField {
|
|
218
|
+
<#
|
|
219
|
+
.SYNOPSIS
|
|
220
|
+
Read a named field off a parsed record, or $null; the unary comma keeps a
|
|
221
|
+
one-element array from unrolling into its scalar.
|
|
222
|
+
#>
|
|
223
|
+
[CmdletBinding()]
|
|
224
|
+
[OutputType([object], [object[]])]
|
|
225
|
+
param([Parameter(Mandatory)] $Record, [Parameter(Mandatory)][string] $Name)
|
|
226
|
+
|
|
227
|
+
$property = $Record.PSObject.Properties[$Name]
|
|
228
|
+
if ($null -eq $property) {
|
|
229
|
+
return $null
|
|
230
|
+
}
|
|
231
|
+
return , $property.Value
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function Get-CodexStandaloneRecordFailure {
|
|
235
|
+
<#
|
|
236
|
+
.SYNOPSIS
|
|
237
|
+
Return the first failing field of a matched record, or $null when every check passes.
|
|
238
|
+
.DESCRIPTION
|
|
239
|
+
Uses the same fixed order as the Claude helpers file: pr_url, issue_num, branch_name,
|
|
240
|
+
authorized_by, authorized_at, basis, run_slug, session_id. branch_name is checked for
|
|
241
|
+
presence only. A blank payload session fails closed.
|
|
242
|
+
#>
|
|
243
|
+
[CmdletBinding()]
|
|
244
|
+
[OutputType([string])]
|
|
245
|
+
param([Parameter(Mandatory)] $Record, [AllowNull()][AllowEmptyString()][string] $SessionId)
|
|
246
|
+
|
|
247
|
+
$prUrl = Get-CodexStandaloneField -Record $Record -Name 'pr_url'
|
|
248
|
+
if (-not (Test-CodexStandaloneText -Value $prUrl) -or
|
|
249
|
+
-not $prUrl.EndsWith('/pull/' + [string]$Record.pr_number, [System.StringComparison]::Ordinal)) {
|
|
250
|
+
return 'pr_url'
|
|
251
|
+
}
|
|
252
|
+
if (-not (Test-CodexStandalonePositiveInteger -Value (Get-CodexStandaloneField -Record $Record -Name 'issue_num'))) {
|
|
253
|
+
return 'issue_num'
|
|
254
|
+
}
|
|
255
|
+
foreach ($name in @('branch_name', 'authorized_by')) {
|
|
256
|
+
if (-not (Test-CodexStandaloneText -Value (Get-CodexStandaloneField -Record $Record -Name $name))) {
|
|
257
|
+
return $name
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
$authorizedAt = Get-CodexStandaloneField -Record $Record -Name 'authorized_at'
|
|
261
|
+
$parsedAt = [DateTime]::MinValue
|
|
262
|
+
$timestampValid = $authorizedAt -is [DateTime] -or $authorizedAt -is [DateTimeOffset]
|
|
263
|
+
if (-not $timestampValid -and (Test-CodexStandaloneText -Value $authorizedAt)) {
|
|
264
|
+
$timestampValid = [DateTime]::TryParse(
|
|
265
|
+
$authorizedAt,
|
|
266
|
+
[System.Globalization.CultureInfo]::InvariantCulture,
|
|
267
|
+
[System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
|
|
268
|
+
[ref] $parsedAt)
|
|
269
|
+
}
|
|
270
|
+
if (-not $timestampValid) {
|
|
271
|
+
return 'authorized_at'
|
|
272
|
+
}
|
|
273
|
+
if (-not (Test-CodexStandaloneText -Value (Get-CodexStandaloneField -Record $Record -Name 'basis') -MinimumLength 20)) {
|
|
274
|
+
return 'basis'
|
|
275
|
+
}
|
|
276
|
+
if ($null -ne $Record.PSObject.Properties['run_slug'] -and
|
|
277
|
+
-not (Test-CodexStandaloneText -Value (Get-CodexStandaloneField -Record $Record -Name 'run_slug'))) {
|
|
278
|
+
return 'run_slug'
|
|
279
|
+
}
|
|
280
|
+
$recordSession = Get-CodexStandaloneField -Record $Record -Name 'session_id'
|
|
281
|
+
if (-not (Test-CodexStandaloneText -Value $recordSession) -or
|
|
282
|
+
[string]::IsNullOrWhiteSpace($SessionId) -or
|
|
283
|
+
-not [string]::Equals($recordSession, $SessionId, [System.StringComparison]::Ordinal)) {
|
|
284
|
+
return 'session_id'
|
|
285
|
+
}
|
|
286
|
+
return $null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function Test-CodexStandaloneMergeAuthorization {
|
|
290
|
+
<#
|
|
291
|
+
.SYNOPSIS
|
|
292
|
+
Codex counterpart of the Claude standalone-merge authorization predicate.
|
|
293
|
+
.DESCRIPTION
|
|
294
|
+
Evaluates the parsed child and epic checkpoints with the same activation condition
|
|
295
|
+
as the Claude side: a PR-specific record naming CommandPrNumber, with well-formed
|
|
296
|
+
fields and a session_id equal to the payload session. Returns an object whose
|
|
297
|
+
Allowed member is $true on success, or $false with ReasonCode and Message.
|
|
298
|
+
#>
|
|
299
|
+
[CmdletBinding()]
|
|
300
|
+
[OutputType([pscustomobject])]
|
|
301
|
+
param(
|
|
302
|
+
[AllowNull()][AllowEmptyCollection()][object[]] $Checkpoints,
|
|
303
|
+
[Parameter(Mandatory)][int] $CommandPrNumber,
|
|
304
|
+
[AllowNull()][AllowEmptyString()][string] $SessionId
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
$key = 'standalone_merge_authorizations'
|
|
308
|
+
$present = $false
|
|
309
|
+
$specific = $true
|
|
310
|
+
$match = $null
|
|
311
|
+
foreach ($checkpoint in @($Checkpoints)) {
|
|
312
|
+
if ($checkpoint -isnot [System.Management.Automation.PSCustomObject] -or
|
|
313
|
+
$null -eq $checkpoint.PSObject.Properties[$key]) {
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
$present = $true
|
|
317
|
+
$block = $checkpoint.PSObject.Properties[$key].Value
|
|
318
|
+
if ($block -isnot [System.Array] -or $block.Count -eq 0) {
|
|
319
|
+
$specific = $false
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
foreach ($entry in $block) {
|
|
323
|
+
$number = $null
|
|
324
|
+
if ($entry -is [System.Management.Automation.PSCustomObject]) {
|
|
325
|
+
$number = Get-CodexStandaloneField -Record $entry -Name 'pr_number'
|
|
326
|
+
}
|
|
327
|
+
if (-not (Test-CodexStandalonePositiveInteger -Value $number)) {
|
|
328
|
+
$specific = $false
|
|
329
|
+
} elseif ($null -eq $match -and $number -eq $CommandPrNumber) {
|
|
330
|
+
$match = $entry
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (-not $present) {
|
|
336
|
+
$code = $script:CodexStandaloneAbsentReasonCode
|
|
337
|
+
$message = 'no orchestrator checkpoint carries an authorization record, but an authorized standalone merge path exists.'
|
|
338
|
+
} elseif (-not $specific) {
|
|
339
|
+
$code = $script:CodexStandaloneNotPrSpecificReasonCode
|
|
340
|
+
$message = 'an orchestrator checkpoint carries the authorization key but names no specific pull request; a blanket flag is not an authorization record.'
|
|
341
|
+
} elseif ($null -eq $match) {
|
|
342
|
+
$code = $script:CodexStandalonePrMismatchReasonCode
|
|
343
|
+
$message = "authorization records exist, but none names pull request $CommandPrNumber."
|
|
344
|
+
} else {
|
|
345
|
+
$field = Get-CodexStandaloneRecordFailure -Record $match -SessionId $SessionId
|
|
346
|
+
if ($null -eq $field) {
|
|
347
|
+
return [pscustomobject]@{ Allowed = $true; ReasonCode = $null; Message = $null }
|
|
348
|
+
}
|
|
349
|
+
$code = $script:CodexStandaloneMalformedReasonCode
|
|
350
|
+
$message = "the matched authorization record has an invalid '$field'."
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return [pscustomobject]@{
|
|
354
|
+
Allowed = $false
|
|
355
|
+
ReasonCode = $code
|
|
356
|
+
Message = "$message To authorize a standalone merge, write a $key record naming this pull request and the current session_id into an orchestrator checkpoint, as described in .claude/rules/orchestrator-state.md."
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
167
360
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
168
361
|
return
|
|
169
362
|
}
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1
CHANGED
|
@@ -83,9 +83,6 @@ function Get-EpicPlanningRegisteredMcpTool {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
$script:EpicPlanningRepositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
86
|
-
$script:AllowedPreparationSemanticMcpTools = @(Get-EpicPlanningRegisteredMcpTool `
|
|
87
|
-
-RegistryPath (Join-Path $script:EpicPlanningRepositoryRoot 'config/orchestration-handoff-registry.json') `
|
|
88
|
-
-SemanticIds $script:PreparationSemanticMcpIds)
|
|
89
86
|
|
|
90
87
|
function Get-EpicPlanningDenyDecision {
|
|
91
88
|
[CmdletBinding()]
|
|
@@ -209,7 +206,8 @@ function Invoke-EpicPlanningOnlyDecision {
|
|
|
209
206
|
[AllowNull()][AllowEmptyString()][string] $CheckpointRaw,
|
|
210
207
|
[AllowNull()][AllowEmptyString()][string] $EpicExecutionContext,
|
|
211
208
|
[AllowNull()][string[]] $StagedPaths,
|
|
212
|
-
[AllowNull()][AllowEmptyString()][string] $CurrentBranch
|
|
209
|
+
[AllowNull()][AllowEmptyString()][string] $CurrentBranch,
|
|
210
|
+
[string] $RegistryPath = (Join-Path $script:EpicPlanningRepositoryRoot 'config/orchestration-handoff-registry.json')
|
|
213
211
|
)
|
|
214
212
|
|
|
215
213
|
$payload = ConvertFrom-EpicPlanningJson -Raw $PayloadRaw -Name 'PreToolUse input'
|
|
@@ -269,8 +267,20 @@ function Invoke-EpicPlanningOnlyDecision {
|
|
|
269
267
|
return Get-EpicPlanningDenyDecision -Reason 'the Bash command is outside the preparation read, branch, commit, push, or validator allowlist.'
|
|
270
268
|
}
|
|
271
269
|
|
|
270
|
+
# Lifecycle tools are decided first and never need the registry. Only a
|
|
271
|
+
# registry-dependent mcp__ call reads it, so a destination without the
|
|
272
|
+
# registry keeps every other decision and fails closed for semantic MCP.
|
|
272
273
|
$allowedRepositoryMcp = $script:AllowedPreparationMcpTools -contains $toolName
|
|
273
|
-
$allowedSemanticMcp = $
|
|
274
|
+
$allowedSemanticMcp = $false
|
|
275
|
+
if (-not $allowedRepositoryMcp -and $toolName -like 'mcp__*') {
|
|
276
|
+
if (-not (Test-Path -LiteralPath $RegistryPath -PathType Leaf)) {
|
|
277
|
+
return Get-EpicPlanningDenyDecision -Reason "semantic MCP registry '$RegistryPath' does not exist; registry-dependent MCP tool '$toolName' is denied in preparation mode."
|
|
278
|
+
}
|
|
279
|
+
$registeredSemanticMcp = @(Get-EpicPlanningRegisteredMcpTool `
|
|
280
|
+
-RegistryPath $RegistryPath `
|
|
281
|
+
-SemanticIds $script:PreparationSemanticMcpIds)
|
|
282
|
+
$allowedSemanticMcp = $registeredSemanticMcp -contains $toolName
|
|
283
|
+
}
|
|
274
284
|
if ($allowedRepositoryMcp -or $allowedSemanticMcp) {
|
|
275
285
|
if ($attestedPreparation) {
|
|
276
286
|
$workspaceRoot = [string]$payload.tool_input.workspace_root
|