@danmoisan/drm-copilot-mcp 1.0.21 → 1.0.23
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 +1730 -139
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +257 -0
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +183 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
- package/resources/claude-customizations/.claude/lib/bash/compute-cohorts.sh +143 -0
- package/resources/claude-customizations/.claude/lib/bash/compute-concurrency-batches.sh +122 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +330 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-common.sh +238 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-items-validate.sh +244 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +187 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-emit.sh +340 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-scan.sh +335 -0
- package/resources/claude-customizations/.claude/lib/bash/validate-parallel-manifest.sh +134 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
- package/resources/claude-customizations/.claude/rules/shell.md +7 -2
- package/resources/claude-customizations/.claude/settings.json +28 -0
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +152 -0
- package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +971 -0
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +461 -0
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
- package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
- package/resources/claude-customizations/config/blast-radius.json +16 -0
- package/resources/claude-customizations/config/orchestration-routing.json +355 -0
- package/resources/claude-customizations/pack-manifests/core.json +32 -1
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/config/orchestration-routing.json +22 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# parallel-cohorts.sh: sourceable bash port of
|
|
3
|
+
# scripts/dev_tools/parallel_cohort_computation.py. Partitions an item-key set
|
|
4
|
+
# into execution cohorts by deterministic greedy graph coloring in Welsh-Powell
|
|
5
|
+
# order, and chunks one cohort into concurrency-capped batches.
|
|
6
|
+
#
|
|
7
|
+
# Determinism is the objective, not optimality. Every ordering decision is made
|
|
8
|
+
# by an explicit numeric sort under LC_ALL=C: the Welsh-Powell visit order comes
|
|
9
|
+
# from `sort -k1,1nr -k2,2n` over `<degree> <item_key>` lines, which is the
|
|
10
|
+
# composite key (-degree, item_key) ascending; cohort membership and batch
|
|
11
|
+
# contents come from `sort -n`. Associative arrays are used for adjacency
|
|
12
|
+
# membership, degree counting, and index assignment only, and their iteration
|
|
13
|
+
# order never reaches output.
|
|
14
|
+
#
|
|
15
|
+
# Error messages reproduce the Python module's four literal failure messages
|
|
16
|
+
# byte for byte, including the tuple repr `(a, b)` with its comma and space.
|
|
17
|
+
# The caller reads PCOH_ERROR after a non-zero return.
|
|
18
|
+
#
|
|
19
|
+
# The Python module remains the repository authority.
|
|
20
|
+
#
|
|
21
|
+
# shellcheck disable=SC2034
|
|
22
|
+
# SC2034 is disabled file-wide because PCOH_ERROR and PCOH_RESULT are
|
|
23
|
+
# written here and read by the entry points compute-cohorts.sh and
|
|
24
|
+
# compute-concurrency-batches.sh, which shellcheck analyses separately.
|
|
25
|
+
|
|
26
|
+
# Resolve this file's own directory so its dependencies source regardless of
|
|
27
|
+
# the caller's working directory.
|
|
28
|
+
PCOH_LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
29
|
+
# shellcheck source=.claude/lib/bash/parallel-common.sh
|
|
30
|
+
# shellcheck disable=SC1091
|
|
31
|
+
source "$PCOH_LIB_DIR/parallel-common.sh"
|
|
32
|
+
|
|
33
|
+
# Adjacency membership flags keyed by `a,b`; never iterated into output.
|
|
34
|
+
declare -gA PCOH_ADJACENT=()
|
|
35
|
+
|
|
36
|
+
# Distinct-neighbor degree per item key; never iterated into output.
|
|
37
|
+
declare -gA PCOH_DEGREE=()
|
|
38
|
+
|
|
39
|
+
# Space-separated neighbor list per item key; never iterated into output.
|
|
40
|
+
declare -gA PCOH_NEIGHBORS=()
|
|
41
|
+
|
|
42
|
+
# Assigned cohort index per item key; never iterated into output.
|
|
43
|
+
declare -gA PCOH_INDEX_OF=()
|
|
44
|
+
|
|
45
|
+
# The failure message set by the most recent non-zero return.
|
|
46
|
+
PCOH_ERROR=""
|
|
47
|
+
|
|
48
|
+
# The compact JSON array-of-arrays produced by the most recent success.
|
|
49
|
+
PCOH_RESULT=""
|
|
50
|
+
|
|
51
|
+
# Scratch array populated by pcoh_split_words.
|
|
52
|
+
PCOH_WORDS=()
|
|
53
|
+
|
|
54
|
+
pcoh_split_words() {
|
|
55
|
+
# Populate PCOH_WORDS by splitting a space-separated string.
|
|
56
|
+
#
|
|
57
|
+
# Args: $1 = the string to split. An empty or whitespace-only string yields
|
|
58
|
+
# an empty array, which is the empty-graph and empty-cohort case. `read -ra`
|
|
59
|
+
# is used rather than an unquoted expansion so a token is never subjected to
|
|
60
|
+
# pathname expansion.
|
|
61
|
+
PCOH_WORDS=()
|
|
62
|
+
read -ra PCOH_WORDS <<<"${1-}"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pcoh_fail() {
|
|
66
|
+
# Record a failure message and return 1.
|
|
67
|
+
#
|
|
68
|
+
# Args: $1 = the complete, literal failure message.
|
|
69
|
+
PCOH_ERROR="$1"
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pcoh_validate_item_keys() {
|
|
74
|
+
# Reject duplicate item keys, walking the supplied order.
|
|
75
|
+
#
|
|
76
|
+
# Duplicates break the uniqueness assumption behind the total-order sort
|
|
77
|
+
# key (-degree, item_key), so they are rejected rather than deduplicated.
|
|
78
|
+
#
|
|
79
|
+
# Args: $1 = space-separated item keys.
|
|
80
|
+
# Returns 0 when every key is distinct, 1 with PCOH_ERROR otherwise.
|
|
81
|
+
local key seen=""
|
|
82
|
+
pcoh_split_words "$1"
|
|
83
|
+
local -a key_items=("${PCOH_WORDS[@]}")
|
|
84
|
+
# Walk in the supplied order so the first repeat encountered is the key
|
|
85
|
+
# reported, which keeps the message stable for a given input.
|
|
86
|
+
for key in "${key_items[@]}"; do
|
|
87
|
+
if pc_contains_word "$seen" "$key"; then
|
|
88
|
+
pcoh_fail "Duplicate item key $key in item_keys; item keys must be unique because cohort ordering relies on key uniqueness."
|
|
89
|
+
return 1
|
|
90
|
+
fi
|
|
91
|
+
seen="$seen $key"
|
|
92
|
+
done
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pcoh_record_edge() {
|
|
97
|
+
# Record one validated edge on both endpoints, ignoring repeats.
|
|
98
|
+
#
|
|
99
|
+
# Storing a membership flag per ordered pair normalizes the edge list by
|
|
100
|
+
# construction: an edge supplied as (a, b), as (b, a), or supplied
|
|
101
|
+
# repeatedly collapses to the same single neighbor entry on both sides.
|
|
102
|
+
#
|
|
103
|
+
# Args: $1 = first endpoint, $2 = second endpoint.
|
|
104
|
+
local first="$1" second="$2"
|
|
105
|
+
[[ -z ${PCOH_ADJACENT["$first,$second"]-} ]] || return 0
|
|
106
|
+
PCOH_ADJACENT["$first,$second"]=1
|
|
107
|
+
PCOH_ADJACENT["$second,$first"]=1
|
|
108
|
+
PCOH_NEIGHBORS["$first"]="${PCOH_NEIGHBORS["$first"]} $second"
|
|
109
|
+
PCOH_NEIGHBORS["$second"]="${PCOH_NEIGHBORS["$second"]} $first"
|
|
110
|
+
PCOH_DEGREE["$first"]=$((PCOH_DEGREE["$first"] + 1))
|
|
111
|
+
PCOH_DEGREE["$second"]=$((PCOH_DEGREE["$second"] + 1))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pcoh_build_adjacency() {
|
|
115
|
+
# Build symmetric adjacency, normalizing edge direction and duplicates.
|
|
116
|
+
#
|
|
117
|
+
# Args: $1 = space-separated item keys, $2 = space-separated `a:b` edges.
|
|
118
|
+
# Populates PCOH_DEGREE and PCOH_NEIGHBORS. Returns 0 on success, 1 with
|
|
119
|
+
# PCOH_ERROR when an edge is a self-loop or names an undeclared endpoint.
|
|
120
|
+
local keys="$1" key edge first second
|
|
121
|
+
PCOH_DEGREE=()
|
|
122
|
+
PCOH_NEIGHBORS=()
|
|
123
|
+
PCOH_ADJACENT=()
|
|
124
|
+
pcoh_split_words "$keys"
|
|
125
|
+
local -a key_list=("${PCOH_WORDS[@]}")
|
|
126
|
+
# Seed every declared key so an isolated vertex survives into the coloring
|
|
127
|
+
# step; inferring vertices from the edge list alone would drop it.
|
|
128
|
+
for key in "${key_list[@]}"; do
|
|
129
|
+
PCOH_DEGREE["$key"]=0
|
|
130
|
+
PCOH_NEIGHBORS["$key"]=""
|
|
131
|
+
done
|
|
132
|
+
|
|
133
|
+
pcoh_split_words "${2-}"
|
|
134
|
+
local -a edge_list=("${PCOH_WORDS[@]}")
|
|
135
|
+
# Validate then record each conflict. Validation order is part of the
|
|
136
|
+
# contract: the self-loop check precedes the endpoint-membership check, and
|
|
137
|
+
# the first endpoint is checked before the second.
|
|
138
|
+
for edge in "${edge_list[@]}"; do
|
|
139
|
+
first="${edge%%:*}"
|
|
140
|
+
second="${edge#*:}"
|
|
141
|
+
if [[ $first == "$second" ]]; then
|
|
142
|
+
pcoh_fail "Self-loop edge on item key $first; the conflict relation is defined over distinct items, so an item cannot conflict with itself."
|
|
143
|
+
return 1
|
|
144
|
+
fi
|
|
145
|
+
if ! pc_contains_word "$keys" "$first"; then
|
|
146
|
+
pcoh_fail "Conflict edge ($first, $second) names item key $first, which is not a member of item_keys; every edge endpoint must be a declared item key."
|
|
147
|
+
return 1
|
|
148
|
+
fi
|
|
149
|
+
if ! pc_contains_word "$keys" "$second"; then
|
|
150
|
+
pcoh_fail "Conflict edge ($first, $second) names item key $second, which is not a member of item_keys; every edge endpoint must be a declared item key."
|
|
151
|
+
return 1
|
|
152
|
+
fi
|
|
153
|
+
pcoh_record_edge "$first" "$second"
|
|
154
|
+
done
|
|
155
|
+
return 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pcoh_welsh_powell_order() {
|
|
159
|
+
# Echo the item keys in Welsh-Powell visit order, one per line.
|
|
160
|
+
#
|
|
161
|
+
# The composite key (-degree, item_key) is a total order because item keys
|
|
162
|
+
# are unique. That is the single load-bearing determinism guard: the visit
|
|
163
|
+
# order depends on the graph alone, never on the caller's input order. The
|
|
164
|
+
# ordering is delegated to sort so no bash-side comparison logic can drift
|
|
165
|
+
# from the Python sort key.
|
|
166
|
+
#
|
|
167
|
+
# Args: $1 = space-separated item keys.
|
|
168
|
+
local key
|
|
169
|
+
pcoh_split_words "$1"
|
|
170
|
+
local -a key_items=("${PCOH_WORDS[@]}")
|
|
171
|
+
for key in "${key_items[@]}"; do
|
|
172
|
+
printf '%s %s\n' "${PCOH_DEGREE["$key"]}" "$key"
|
|
173
|
+
done | LC_ALL=C sort -k1,1nr -k2,2n | cut -d' ' -f2
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pcoh_lowest_free_index() {
|
|
177
|
+
# Echo the smallest cohort index not held by any assigned neighbor.
|
|
178
|
+
#
|
|
179
|
+
# Args: $1 = the item key being placed.
|
|
180
|
+
local key="$1" neighbor taken="" candidate=0
|
|
181
|
+
pcoh_split_words "${PCOH_NEIGHBORS["$key"]}"
|
|
182
|
+
local -a neighbors=("${PCOH_WORDS[@]}")
|
|
183
|
+
# Collecting neighbor indices into a membership string is order
|
|
184
|
+
# insensitive, so reading the neighbor list here cannot affect the outcome.
|
|
185
|
+
for neighbor in "${neighbors[@]}"; do
|
|
186
|
+
if [[ -n ${PCOH_INDEX_OF["$neighbor"]-} ]]; then
|
|
187
|
+
taken="$taken ${PCOH_INDEX_OF["$neighbor"]}"
|
|
188
|
+
fi
|
|
189
|
+
done
|
|
190
|
+
# Scan upward from zero for the first free index. An isolated vertex and
|
|
191
|
+
# the first-visited vertex both land in cohort 0.
|
|
192
|
+
while pc_contains_word "$taken" "$candidate"; do
|
|
193
|
+
candidate=$((candidate + 1))
|
|
194
|
+
done
|
|
195
|
+
printf '%s' "$candidate"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pcoh_assign_cohort_indices() {
|
|
199
|
+
# Assign each vertex the lowest cohort index free among its neighbors.
|
|
200
|
+
#
|
|
201
|
+
# Args: $1 = space-separated item keys. Populates PCOH_INDEX_OF. A vertex
|
|
202
|
+
# never takes an index already held by a neighbor, so each index class is
|
|
203
|
+
# an independent set of the conflict graph.
|
|
204
|
+
local ordered key
|
|
205
|
+
PCOH_INDEX_OF=()
|
|
206
|
+
ordered=$(pcoh_welsh_powell_order "$1")
|
|
207
|
+
[[ -n $ordered ]] || return 0
|
|
208
|
+
# Visit vertices in Welsh-Powell order; each takes the smallest index its
|
|
209
|
+
# already-assigned neighbors have not taken.
|
|
210
|
+
while IFS= read -r key; do
|
|
211
|
+
[[ -n $key ]] || continue
|
|
212
|
+
PCOH_INDEX_OF["$key"]=$(pcoh_lowest_free_index "$key")
|
|
213
|
+
done <<<"$ordered"
|
|
214
|
+
return 0
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
pcoh_render_cohorts() {
|
|
218
|
+
# Echo the compact JSON array-of-arrays for the current assignment.
|
|
219
|
+
#
|
|
220
|
+
# Args: $1 = space-separated item keys. List position is the cohort index
|
|
221
|
+
# and each inner list holds that cohort's keys sorted ascending.
|
|
222
|
+
local key index highest=-1 ordered
|
|
223
|
+
pcoh_split_words "$1"
|
|
224
|
+
local -a key_items=("${PCOH_WORDS[@]}")
|
|
225
|
+
for key in "${key_items[@]}"; do
|
|
226
|
+
index="${PCOH_INDEX_OF["$key"]}"
|
|
227
|
+
((index > highest)) && highest=$index
|
|
228
|
+
done
|
|
229
|
+
if ((highest < 0)); then
|
|
230
|
+
printf '[]'
|
|
231
|
+
return 0
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
local -a buckets=()
|
|
235
|
+
for ((index = 0; index <= highest; index++)); do
|
|
236
|
+
buckets+=("")
|
|
237
|
+
done
|
|
238
|
+
ordered=$(printf '%s\n' "${key_items[@]}" | LC_ALL=C sort -n)
|
|
239
|
+
# Walk the ascending key order rather than any hash order, so each cohort's
|
|
240
|
+
# keys come out ascending regardless of how the caller ordered its input.
|
|
241
|
+
while IFS= read -r key; do
|
|
242
|
+
[[ -n $key ]] || continue
|
|
243
|
+
index="${PCOH_INDEX_OF["$key"]}"
|
|
244
|
+
if [[ -z ${buckets[index]} ]]; then
|
|
245
|
+
buckets[index]="$key"
|
|
246
|
+
else
|
|
247
|
+
buckets[index]="${buckets[index]},$key"
|
|
248
|
+
fi
|
|
249
|
+
done <<<"$ordered"
|
|
250
|
+
|
|
251
|
+
local rendered=""
|
|
252
|
+
for ((index = 0; index <= highest; index++)); do
|
|
253
|
+
[[ -z $rendered ]] || rendered="$rendered,"
|
|
254
|
+
rendered="${rendered}[${buckets[index]}]"
|
|
255
|
+
done
|
|
256
|
+
printf '[%s]' "$rendered"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
pcoh_compute_cohorts() {
|
|
260
|
+
# Partition item keys into cohorts by deterministic greedy graph coloring.
|
|
261
|
+
#
|
|
262
|
+
# Args: $1 = space-separated item keys, $2 = space-separated `a:b` edges.
|
|
263
|
+
# On success returns 0 with PCOH_RESULT holding the compact JSON output.
|
|
264
|
+
# On malformed input returns 1 with PCOH_ERROR holding the exact Python
|
|
265
|
+
# message. All validation runs before any coloring work.
|
|
266
|
+
local keys="$1" edges="${2-}"
|
|
267
|
+
PCOH_ERROR=""
|
|
268
|
+
PCOH_RESULT=""
|
|
269
|
+
pcoh_validate_item_keys "$keys" || return 1
|
|
270
|
+
pcoh_build_adjacency "$keys" "$edges" || return 1
|
|
271
|
+
pcoh_assign_cohort_indices "$keys"
|
|
272
|
+
PCOH_RESULT=$(pcoh_render_cohorts "$keys")
|
|
273
|
+
return 0
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
pcoh_compute_concurrency_batches() {
|
|
277
|
+
# Chunk one cohort into concurrency-capped batches in ascending key order.
|
|
278
|
+
#
|
|
279
|
+
# The cohort's keys are sorted inside this function rather than trusting
|
|
280
|
+
# the caller's ordering, so determinism does not depend on caller
|
|
281
|
+
# discipline. Every batch is exactly max_concurrency long except a possibly
|
|
282
|
+
# smaller final batch.
|
|
283
|
+
#
|
|
284
|
+
# Args: $1 = space-separated cohort item keys, $2 = the fan-out cap.
|
|
285
|
+
# On success returns 0 with PCOH_RESULT holding the compact JSON output;
|
|
286
|
+
# on a cap below 1 returns 1 with PCOH_ERROR holding the exact message.
|
|
287
|
+
local cap="$2" key sorted
|
|
288
|
+
PCOH_ERROR=""
|
|
289
|
+
PCOH_RESULT=""
|
|
290
|
+
# A cap below 1 would admit no items into any batch, so the cohort could
|
|
291
|
+
# never drain; reject it rather than returning an unusable schedule.
|
|
292
|
+
if ((cap < 1)); then
|
|
293
|
+
pcoh_fail "max_concurrency must be >= 1; received $cap."
|
|
294
|
+
return 1
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
pcoh_split_words "$1"
|
|
298
|
+
local -a supplied=("${PCOH_WORDS[@]}")
|
|
299
|
+
local -a ordered=()
|
|
300
|
+
if ((${#supplied[@]} > 0)); then
|
|
301
|
+
sorted=$(printf '%s\n' "${supplied[@]}" | LC_ALL=C sort -n)
|
|
302
|
+
while IFS= read -r key; do
|
|
303
|
+
[[ -n $key ]] || continue
|
|
304
|
+
ordered+=("$key")
|
|
305
|
+
done <<<"$sorted"
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
local total=${#ordered[@]}
|
|
309
|
+
if ((total == 0)); then
|
|
310
|
+
PCOH_RESULT="[]"
|
|
311
|
+
return 0
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
local start end position rendered="" batch
|
|
315
|
+
# Walk the sorted keys in fixed-size strides so slot filling follows
|
|
316
|
+
# ascending key order and the batch boundaries are reproducible.
|
|
317
|
+
for ((start = 0; start < total; start += cap)); do
|
|
318
|
+
end=$((start + cap))
|
|
319
|
+
((end > total)) && end=$total
|
|
320
|
+
batch=""
|
|
321
|
+
for ((position = start; position < end; position++)); do
|
|
322
|
+
[[ -z $batch ]] || batch="$batch,"
|
|
323
|
+
batch="$batch${ordered[position]}"
|
|
324
|
+
done
|
|
325
|
+
[[ -z $rendered ]] || rendered="$rendered,"
|
|
326
|
+
rendered="${rendered}[$batch]"
|
|
327
|
+
done
|
|
328
|
+
PCOH_RESULT="[$rendered]"
|
|
329
|
+
return 0
|
|
330
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# parallel-common.sh: sourceable bash port of the shared predicates, enum
|
|
3
|
+
# vocabularies, and error-string builders that back the parallel-surface
|
|
4
|
+
# validators. This is the bash counterpart of
|
|
5
|
+
# scripts/dev_tools/_parallel_state_common.py; the Python module remains the
|
|
6
|
+
# repository authority and this file must reproduce its output byte for byte.
|
|
7
|
+
#
|
|
8
|
+
# Scope. Pure string and arithmetic helpers only: nothing here reads a file,
|
|
9
|
+
# starts a process, reads the clock, or mutates global state other than the
|
|
10
|
+
# PC_ERRORS accumulator its own reset/add functions own. The enum tuples are
|
|
11
|
+
# consumed from .claude/rules/parallel-orchestration.md and are never extended
|
|
12
|
+
# here; member order is load-bearing because pc_enum_error renders it.
|
|
13
|
+
#
|
|
14
|
+
# Locale. Every entry point that sources this file calls pc_enforce_c_locale
|
|
15
|
+
# before doing any work, so sorting and character classification are byte
|
|
16
|
+
# ordered and independent of the destination workspace's environment.
|
|
17
|
+
#
|
|
18
|
+
# shellcheck disable=SC2034
|
|
19
|
+
# SC2034 is disabled file-wide because every constant below is consumed by a
|
|
20
|
+
# sibling file of this library -- parallel-items-validate.sh,
|
|
21
|
+
# parallel-manifest-validate.sh, and parallel-cohorts.sh -- which shellcheck
|
|
22
|
+
# analyses separately and therefore cannot see the use.
|
|
23
|
+
|
|
24
|
+
# Item lifecycle states, in canonical order (schema S4).
|
|
25
|
+
PC_VALID_ITEM_STATES="proposed, admitted, prepared, scheduled, in_flight, merged, withdrawn, blocked"
|
|
26
|
+
|
|
27
|
+
# Per-item merge lifecycle, in canonical order (schema S4).
|
|
28
|
+
PC_VALID_MERGE_STATUS="not_started, worktree_created, pr_open, ci_green, merged, worktree_removed, blocked_drift, blocked_ci_loop_limit"
|
|
29
|
+
|
|
30
|
+
# Blast-radius confidence sources, in canonical order (schema S4).
|
|
31
|
+
PC_VALID_SOURCES="derived, declared, observed"
|
|
32
|
+
|
|
33
|
+
# Work-item kinds carried by the manifest (schema S4).
|
|
34
|
+
PC_VALID_KINDS="feature, bug"
|
|
35
|
+
|
|
36
|
+
# Run modes; closed is the documented default (schema S4).
|
|
37
|
+
PC_VALID_MODES="closed, open"
|
|
38
|
+
|
|
39
|
+
# Merge-status values meaning the item reached a terminal merged outcome.
|
|
40
|
+
PC_MERGED_MERGE_STATUSES="merged worktree_removed"
|
|
41
|
+
|
|
42
|
+
# Merge-status values meaning the item is blocked.
|
|
43
|
+
PC_BLOCKED_MERGE_STATUSES="blocked_drift blocked_ci_loop_limit"
|
|
44
|
+
|
|
45
|
+
# The four blast_radius collection fields, in serialization order.
|
|
46
|
+
PC_BLAST_RADIUS_LIST_FIELDS="paths modules shared_surfaces contracts"
|
|
47
|
+
|
|
48
|
+
# Path label for the document root in prohibited-key error strings.
|
|
49
|
+
PC_ROOT_PATH="<root>"
|
|
50
|
+
|
|
51
|
+
# Accumulated validation errors, in emission order.
|
|
52
|
+
PC_ERRORS=()
|
|
53
|
+
|
|
54
|
+
pc_enforce_c_locale() {
|
|
55
|
+
# Pin the byte-ordered C locale for the remainder of the process.
|
|
56
|
+
#
|
|
57
|
+
# Determinism countermeasure R9: every numeric sort, glob expansion, and
|
|
58
|
+
# character class in this library must behave identically regardless of the
|
|
59
|
+
# destination workspace's locale settings.
|
|
60
|
+
export LC_ALL=C
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pc_errors_reset() {
|
|
64
|
+
# Discard any accumulated errors so a fresh validation starts empty.
|
|
65
|
+
PC_ERRORS=()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pc_error_add() {
|
|
69
|
+
# Append one error string to the accumulator.
|
|
70
|
+
#
|
|
71
|
+
# Args: $1 = the complete error string, already fully rendered.
|
|
72
|
+
PC_ERRORS+=("$1")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pc_errors_count() {
|
|
76
|
+
# Echo the number of accumulated errors.
|
|
77
|
+
printf '%s' "${#PC_ERRORS[@]}"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pc_errors_print() {
|
|
81
|
+
# Print every accumulated error, one per line, in emission order.
|
|
82
|
+
#
|
|
83
|
+
# Prints nothing for an empty accumulator, which is the valid-artifact case.
|
|
84
|
+
((${#PC_ERRORS[@]} > 0)) || return 0
|
|
85
|
+
local entry
|
|
86
|
+
# Emit in accumulation order: message sequence is part of the parity
|
|
87
|
+
# contract, so the accumulator is never sorted or de-duplicated.
|
|
88
|
+
for entry in "${PC_ERRORS[@]}"; do
|
|
89
|
+
printf '%s\n' "$entry"
|
|
90
|
+
done
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pc_repr_string() {
|
|
94
|
+
# Echo the Python repr of a string, including Python's quote selection.
|
|
95
|
+
#
|
|
96
|
+
# Python prefers single quotes and switches to double quotes only when the
|
|
97
|
+
# string contains a single quote and no double quote. Backslashes are
|
|
98
|
+
# escaped first so a later escape's own backslash is not doubled, then the
|
|
99
|
+
# three printable control escapes, then the active quote character.
|
|
100
|
+
#
|
|
101
|
+
# Args: $1 = the raw string value.
|
|
102
|
+
local value="$1"
|
|
103
|
+
local quote="'"
|
|
104
|
+
# Quote selection: only a string carrying an apostrophe but no double quote
|
|
105
|
+
# switches Python to double quotes.
|
|
106
|
+
if [[ $value == *"'"* && $value != *'"'* ]]; then
|
|
107
|
+
quote='"'
|
|
108
|
+
fi
|
|
109
|
+
local body=${value//\\/\\\\}
|
|
110
|
+
body=${body//$'\n'/\\n}
|
|
111
|
+
body=${body//$'\r'/\\r}
|
|
112
|
+
body=${body//$'\t'/\\t}
|
|
113
|
+
# Escape whichever quote is active. Under double quoting the escape is a
|
|
114
|
+
# no-op by the selection rule above; under single quoting it fires only for
|
|
115
|
+
# a string that carries both quote characters.
|
|
116
|
+
if [[ $quote == "'" ]]; then
|
|
117
|
+
body=${body//\'/\\\'}
|
|
118
|
+
else
|
|
119
|
+
body=${body//\"/\\\"}
|
|
120
|
+
fi
|
|
121
|
+
printf '%s%s%s' "$quote" "$body" "$quote"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pc_repr() {
|
|
125
|
+
# Echo the Python repr of a lexically typed value.
|
|
126
|
+
#
|
|
127
|
+
# Args: $1 = type tag (absent, null, bool, int, float, str), $2 = raw value.
|
|
128
|
+
# An absent key reads as None, matching Python's mapping.get default.
|
|
129
|
+
local type="$1"
|
|
130
|
+
local value="${2-}"
|
|
131
|
+
# Routing table: the five scalar types the manifest subset can carry, plus
|
|
132
|
+
# the absent-key case that Python renders as None.
|
|
133
|
+
case "$type" in
|
|
134
|
+
absent | null)
|
|
135
|
+
printf 'None'
|
|
136
|
+
;;
|
|
137
|
+
bool)
|
|
138
|
+
if [[ $value == true ]]; then
|
|
139
|
+
printf 'True'
|
|
140
|
+
else
|
|
141
|
+
printf 'False'
|
|
142
|
+
fi
|
|
143
|
+
;;
|
|
144
|
+
int | float)
|
|
145
|
+
printf '%s' "$value"
|
|
146
|
+
;;
|
|
147
|
+
str)
|
|
148
|
+
pc_repr_string "$value"
|
|
149
|
+
;;
|
|
150
|
+
*)
|
|
151
|
+
# A container reaching repr would be a caller defect; render the tag so
|
|
152
|
+
# the failure is visible rather than silently producing a bare value.
|
|
153
|
+
printf '<%s>' "$type"
|
|
154
|
+
;;
|
|
155
|
+
esac
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pc_is_non_empty_string() {
|
|
159
|
+
# Return 0 when a typed value is a string carrying a non-space character.
|
|
160
|
+
#
|
|
161
|
+
# Args: $1 = type tag, $2 = raw value. Mirrors is_non_empty_string.
|
|
162
|
+
[[ $1 == str ]] || return 1
|
|
163
|
+
local stripped="${2-}"
|
|
164
|
+
# A left strip is sufficient: a value that is entirely whitespace becomes
|
|
165
|
+
# empty, which is exactly what Python's two-sided strip would report.
|
|
166
|
+
stripped=${stripped#"${stripped%%[![:space:]]*}"}
|
|
167
|
+
[[ -n $stripped ]]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pc_is_integer() {
|
|
171
|
+
# Return 0 when a typed value is a genuine integer rather than a boolean.
|
|
172
|
+
#
|
|
173
|
+
# Args: $1 = type tag. The parser classifies booleans as `bool`, never as
|
|
174
|
+
# `int`, so the boolean exclusion that Python needs is structural here.
|
|
175
|
+
[[ $1 == int ]]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
pc_is_positive_integer() {
|
|
179
|
+
# Return 0 when a typed value is an integer greater than zero.
|
|
180
|
+
#
|
|
181
|
+
# Args: $1 = type tag, $2 = raw value.
|
|
182
|
+
[[ $1 == int ]] || return 1
|
|
183
|
+
local number="$2"
|
|
184
|
+
((number > 0))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
pc_is_non_negative_integer() {
|
|
188
|
+
# Return 0 when a typed value is an integer of zero or more.
|
|
189
|
+
#
|
|
190
|
+
# Args: $1 = type tag, $2 = raw value.
|
|
191
|
+
[[ $1 == int ]] || return 1
|
|
192
|
+
local number="$2"
|
|
193
|
+
((number >= 0))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
pc_in_bounded_range() {
|
|
197
|
+
# Return 0 when a typed value is an integer inside an inclusive range.
|
|
198
|
+
#
|
|
199
|
+
# Args: $1 = type tag, $2 = raw value, $3 = minimum, $4 = maximum.
|
|
200
|
+
[[ $1 == int ]] || return 1
|
|
201
|
+
local number="$2" minimum="$3" maximum="$4"
|
|
202
|
+
((number >= minimum && number <= maximum))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
pc_enum_error() {
|
|
206
|
+
# Echo the standard out-of-enum error string for a field.
|
|
207
|
+
#
|
|
208
|
+
# Args: $1 = context prefix, $2 = dotted field name, $3 = comma-and-space
|
|
209
|
+
# joined member list in canonical order, $4 = the already-rendered repr of
|
|
210
|
+
# the offending value. Mirrors enum_error so the wording cannot drift.
|
|
211
|
+
printf '%s %s must be one of %s; found: %s.' "$1" "$2" "$3" "$4"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
pc_item_context() {
|
|
215
|
+
# Echo the context prefix for one items[] entry.
|
|
216
|
+
#
|
|
217
|
+
# Args: $1 = surface prefix, $2 = zero-based entry index. The positional
|
|
218
|
+
# index is used rather than issue_num because it exists for every entry,
|
|
219
|
+
# including one whose issue_num is missing.
|
|
220
|
+
printf '%s items[%s]' "$1" "$2"
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pc_contains_word() {
|
|
224
|
+
# Return 0 when a space-separated list contains an exact word.
|
|
225
|
+
#
|
|
226
|
+
# Args: $1 = space-separated haystack, $2 = needle. Used for the enum
|
|
227
|
+
# membership tests whose member lists are stored space separated.
|
|
228
|
+
local haystack=" $1 "
|
|
229
|
+
[[ $haystack == *" $2 "* ]]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
pc_enum_members_contains() {
|
|
233
|
+
# Return 0 when a comma-and-space joined enum list contains an exact member.
|
|
234
|
+
#
|
|
235
|
+
# Args: $1 = the rendered member list, $2 = candidate member.
|
|
236
|
+
local haystack="${1//, / }"
|
|
237
|
+
pc_contains_word "$haystack" "$2"
|
|
238
|
+
}
|