@danmoisan/drm-copilot-mcp 1.0.22 → 1.0.24
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 +164 -7
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +27 -8
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +44 -10
- 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/rules/shell.md +7 -2
- package/resources/claude-customizations/.claude/settings.json +3 -0
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +9 -5
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +24 -13
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +62 -21
- package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +4 -4
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +5 -3
- 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 +14 -1
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +3 -3
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +5 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
package/out/mcp-server.js
CHANGED
|
@@ -18811,9 +18811,161 @@ var ExcludingFileSystem = class {
|
|
|
18811
18811
|
}
|
|
18812
18812
|
};
|
|
18813
18813
|
|
|
18814
|
+
// ../../extensions/drm-copilot/src/lib/push-down/claude-routing-merge.ts
|
|
18815
|
+
var ROUTES_KEY = "routes";
|
|
18816
|
+
var AUTHORITATIVE_ROUTE = "parallel";
|
|
18817
|
+
var RoutingMergeError = class extends Error {
|
|
18818
|
+
/** Destination path whose current content could not be parsed. */
|
|
18819
|
+
path;
|
|
18820
|
+
/**
|
|
18821
|
+
* @param path Destination path that failed to parse.
|
|
18822
|
+
* @param detail Parser detail appended to the message.
|
|
18823
|
+
*/
|
|
18824
|
+
constructor(path11, detail) {
|
|
18825
|
+
super(
|
|
18826
|
+
`Destination routing document is not valid JSON and was not written: ${path11} (${detail})`
|
|
18827
|
+
);
|
|
18828
|
+
this.name = "RoutingMergeError";
|
|
18829
|
+
this.path = path11;
|
|
18830
|
+
}
|
|
18831
|
+
};
|
|
18832
|
+
function normalizePosix4(value) {
|
|
18833
|
+
return value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
18834
|
+
}
|
|
18835
|
+
function relativeToPosix4(path11, root) {
|
|
18836
|
+
const normalizedPath = normalizePosix4(path11);
|
|
18837
|
+
const normalizedRoot = normalizePosix4(root);
|
|
18838
|
+
if (normalizedPath === normalizedRoot) {
|
|
18839
|
+
return "";
|
|
18840
|
+
}
|
|
18841
|
+
const prefix = `${normalizedRoot}/`;
|
|
18842
|
+
return normalizedPath.startsWith(prefix) ? normalizedPath.slice(prefix.length) : null;
|
|
18843
|
+
}
|
|
18844
|
+
function parseRoutingObject(text, path11) {
|
|
18845
|
+
let parsed;
|
|
18846
|
+
try {
|
|
18847
|
+
parsed = JSON.parse(text);
|
|
18848
|
+
} catch (error2) {
|
|
18849
|
+
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
18850
|
+
throw new RoutingMergeError(path11, detail);
|
|
18851
|
+
}
|
|
18852
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
18853
|
+
throw new RoutingMergeError(path11, "document root is not a JSON object");
|
|
18854
|
+
}
|
|
18855
|
+
return parsed;
|
|
18856
|
+
}
|
|
18857
|
+
function asObject(value) {
|
|
18858
|
+
if (value === void 0 || value === null || typeof value !== "object") {
|
|
18859
|
+
return null;
|
|
18860
|
+
}
|
|
18861
|
+
return Array.isArray(value) ? null : value;
|
|
18862
|
+
}
|
|
18863
|
+
function mergeRoutes(destinationRoutes, sourceRoutes) {
|
|
18864
|
+
const merged = {};
|
|
18865
|
+
if (destinationRoutes !== null) {
|
|
18866
|
+
for (const [name, definition] of Object.entries(destinationRoutes)) {
|
|
18867
|
+
const sourceDefinition = sourceRoutes?.[name];
|
|
18868
|
+
merged[name] = name === AUTHORITATIVE_ROUTE && sourceDefinition !== void 0 ? sourceDefinition : definition;
|
|
18869
|
+
}
|
|
18870
|
+
}
|
|
18871
|
+
if (sourceRoutes !== null) {
|
|
18872
|
+
for (const [name, definition] of Object.entries(sourceRoutes)) {
|
|
18873
|
+
if (!(name in merged)) {
|
|
18874
|
+
merged[name] = definition;
|
|
18875
|
+
}
|
|
18876
|
+
}
|
|
18877
|
+
}
|
|
18878
|
+
return merged;
|
|
18879
|
+
}
|
|
18880
|
+
function mergeRoutingDocuments(destinationText, sourceText, path11) {
|
|
18881
|
+
const destination = parseRoutingObject(destinationText, path11);
|
|
18882
|
+
const source = parseRoutingObject(sourceText, path11);
|
|
18883
|
+
const merged = {};
|
|
18884
|
+
for (const [key, value] of Object.entries(destination)) {
|
|
18885
|
+
merged[key] = key === ROUTES_KEY ? mergeRoutes(asObject(value), asObject(source[ROUTES_KEY])) : value;
|
|
18886
|
+
}
|
|
18887
|
+
for (const [key, value] of Object.entries(source)) {
|
|
18888
|
+
if (key in merged) {
|
|
18889
|
+
continue;
|
|
18890
|
+
}
|
|
18891
|
+
merged[key] = key === ROUTES_KEY ? mergeRoutes(null, asObject(value)) : value;
|
|
18892
|
+
}
|
|
18893
|
+
return `${JSON.stringify(merged, null, 2)}
|
|
18894
|
+
`;
|
|
18895
|
+
}
|
|
18896
|
+
var RoutingMergeFileSystem = class {
|
|
18897
|
+
inner;
|
|
18898
|
+
destinationRoot;
|
|
18899
|
+
mergeRelativePath;
|
|
18900
|
+
/**
|
|
18901
|
+
* @param inner The wrapped adapter performing real I/O.
|
|
18902
|
+
* @param destinationRoot Destination workspace root (POSIX path).
|
|
18903
|
+
* @param mergeRelativePath Destination-relative path to merge.
|
|
18904
|
+
*/
|
|
18905
|
+
constructor(inner, destinationRoot, mergeRelativePath) {
|
|
18906
|
+
this.inner = inner;
|
|
18907
|
+
this.destinationRoot = normalizePosix4(destinationRoot);
|
|
18908
|
+
this.mergeRelativePath = mergeRelativePath;
|
|
18909
|
+
}
|
|
18910
|
+
/** @inheritdoc */
|
|
18911
|
+
listFiles(root) {
|
|
18912
|
+
return this.inner.listFiles(root);
|
|
18913
|
+
}
|
|
18914
|
+
/** @inheritdoc */
|
|
18915
|
+
isDir(path11) {
|
|
18916
|
+
return this.inner.isDir(path11);
|
|
18917
|
+
}
|
|
18918
|
+
/** @inheritdoc */
|
|
18919
|
+
isFile(path11) {
|
|
18920
|
+
return this.inner.isFile(path11);
|
|
18921
|
+
}
|
|
18922
|
+
/** @inheritdoc */
|
|
18923
|
+
readTextFile(path11) {
|
|
18924
|
+
return this.inner.readTextFile(path11);
|
|
18925
|
+
}
|
|
18926
|
+
/** @inheritdoc */
|
|
18927
|
+
ensureDir(path11) {
|
|
18928
|
+
this.inner.ensureDir(path11);
|
|
18929
|
+
}
|
|
18930
|
+
/**
|
|
18931
|
+
* Write a file, merging the one configured routing path when it exists.
|
|
18932
|
+
*
|
|
18933
|
+
* @param path Absolute destination POSIX path.
|
|
18934
|
+
* @param content Source content the engine wants to publish.
|
|
18935
|
+
* @throws RoutingMergeError When the destination routing document is not
|
|
18936
|
+
* parseable; the destination bytes are left untouched.
|
|
18937
|
+
*/
|
|
18938
|
+
writeTextFile(path11, content) {
|
|
18939
|
+
if (!this.isMergeTarget(path11)) {
|
|
18940
|
+
this.inner.writeTextFile(path11, content);
|
|
18941
|
+
return;
|
|
18942
|
+
}
|
|
18943
|
+
if (!this.inner.isFile(path11)) {
|
|
18944
|
+
this.inner.writeTextFile(path11, content);
|
|
18945
|
+
return;
|
|
18946
|
+
}
|
|
18947
|
+
const merged = mergeRoutingDocuments(
|
|
18948
|
+
this.inner.readTextFile(path11),
|
|
18949
|
+
content,
|
|
18950
|
+
path11
|
|
18951
|
+
);
|
|
18952
|
+
this.inner.writeTextFile(path11, merged);
|
|
18953
|
+
}
|
|
18954
|
+
/**
|
|
18955
|
+
* Return whether a destination path is the configured merge target.
|
|
18956
|
+
*
|
|
18957
|
+
* @param path Absolute destination POSIX path.
|
|
18958
|
+
* @returns True when the path resolves to the merge-target relative path.
|
|
18959
|
+
*/
|
|
18960
|
+
isMergeTarget(path11) {
|
|
18961
|
+
return relativeToPosix4(path11, this.destinationRoot) === this.mergeRelativePath;
|
|
18962
|
+
}
|
|
18963
|
+
};
|
|
18964
|
+
|
|
18814
18965
|
// ../../extensions/drm-copilot/src/lib/push-down/claude-customizations.ts
|
|
18815
18966
|
var ARTIFACT_DIRECTORY4 = "artifacts/claude-customizations";
|
|
18816
|
-
var ROOT_FOLDERS2 = [".claude"];
|
|
18967
|
+
var ROOT_FOLDERS2 = [".claude", "config"];
|
|
18968
|
+
var ROUTING_MERGE_RELATIVE_PATH = "config/orchestration-routing.json";
|
|
18817
18969
|
var EXCLUDED_RELATIVE_PATHS = [
|
|
18818
18970
|
".claude/settings.local.json"
|
|
18819
18971
|
];
|
|
@@ -18862,8 +19014,13 @@ function pushDownCustomizations4(options) {
|
|
|
18862
19014
|
effectiveBundle,
|
|
18863
19015
|
fs9
|
|
18864
19016
|
);
|
|
18865
|
-
const
|
|
19017
|
+
const mergingFs = new RoutingMergeFileSystem(
|
|
18866
19018
|
fs9,
|
|
19019
|
+
destinationRoot,
|
|
19020
|
+
ROUTING_MERGE_RELATIVE_PATH
|
|
19021
|
+
);
|
|
19022
|
+
const excludingFs = new ExcludingFileSystem(
|
|
19023
|
+
mergingFs,
|
|
18867
19024
|
repoRoot,
|
|
18868
19025
|
EXCLUDED_RELATIVE_PATHS,
|
|
18869
19026
|
{
|
|
@@ -29499,7 +29656,7 @@ function resolveReadinessSignal(fs9, featureDir) {
|
|
|
29499
29656
|
}
|
|
29500
29657
|
return [null, null];
|
|
29501
29658
|
}
|
|
29502
|
-
function
|
|
29659
|
+
function relativeToPosix5(root, path11) {
|
|
29503
29660
|
const normalizedRoot = toPosixPath2(root).replace(/\/+$/u, "");
|
|
29504
29661
|
const normalized = toPosixPath2(path11);
|
|
29505
29662
|
if (normalized.startsWith(`${normalizedRoot}/`)) {
|
|
@@ -29537,7 +29694,7 @@ function discoverCanonicalEvidenceFiles(fs9, root, feature) {
|
|
|
29537
29694
|
for (const pattern of CANONICAL_GLOBS) {
|
|
29538
29695
|
for (const candidate of fs9.glob(featureRoot, pattern)) {
|
|
29539
29696
|
if (fs9.isFile(candidate)) {
|
|
29540
|
-
discovered.add(
|
|
29697
|
+
discovered.add(relativeToPosix6(normalizedRoot, candidate));
|
|
29541
29698
|
}
|
|
29542
29699
|
}
|
|
29543
29700
|
}
|
|
@@ -29608,7 +29765,7 @@ function parseIntegerStrict(value) {
|
|
|
29608
29765
|
}
|
|
29609
29766
|
return Number.parseInt(value, 10);
|
|
29610
29767
|
}
|
|
29611
|
-
function
|
|
29768
|
+
function relativeToPosix6(root, absolute) {
|
|
29612
29769
|
const normalized = toPosixPath2(absolute);
|
|
29613
29770
|
if (normalized.startsWith(`${root}/`)) {
|
|
29614
29771
|
return normalized.slice(root.length + 1);
|
|
@@ -29805,11 +29962,11 @@ function buildContextFiles(params) {
|
|
|
29805
29962
|
const contextFiles = [];
|
|
29806
29963
|
for (const path11 of candidatePaths) {
|
|
29807
29964
|
if (fs9.exists(path11)) {
|
|
29808
|
-
contextFiles.push(
|
|
29965
|
+
contextFiles.push(relativeToPosix5(root, path11));
|
|
29809
29966
|
}
|
|
29810
29967
|
}
|
|
29811
29968
|
if (readinessSource !== null) {
|
|
29812
|
-
contextFiles.push(
|
|
29969
|
+
contextFiles.push(relativeToPosix5(root, readinessSource));
|
|
29813
29970
|
}
|
|
29814
29971
|
const evidenceContextFiles = discoverCanonicalEvidenceFiles(
|
|
29815
29972
|
fs9,
|
package/package.json
CHANGED
|
@@ -15,6 +15,9 @@ tools:
|
|
|
15
15
|
- "Bash(gh *)"
|
|
16
16
|
- "Bash(poetry run python -c *)"
|
|
17
17
|
- "Bash(poetry run python -m *)"
|
|
18
|
+
- "Bash(bash .claude/lib/bash/compute-cohorts.sh*)"
|
|
19
|
+
- "Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"
|
|
20
|
+
- "Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)"
|
|
18
21
|
- "mcp__drm-copilot__collect_pr_context"
|
|
19
22
|
- "mcp__drm-copilot__validate_orchestration_artifacts"
|
|
20
23
|
skills:
|
|
@@ -66,16 +69,32 @@ frames the *who* and *when*; the skill documents the *how* in full. The manifest
|
|
|
66
69
|
checkpoint schema, and the parallel enums are defined once in
|
|
67
70
|
`.claude/rules/parallel-orchestration.md` and are consumed here, never redefined.
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
`
|
|
74
|
-
|
|
72
|
+
The manifest gate is reached through the destination-runtime bash entry point, which needs no
|
|
73
|
+
Python interpreter and is published by push-down alongside `.claude`, so the `tools` allowlist
|
|
74
|
+
grants one entry per command-line entry point —
|
|
75
|
+
`"Bash(bash .claude/lib/bash/compute-cohorts.sh*)"`,
|
|
76
|
+
`"Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"`, and
|
|
77
|
+
`"Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)"` — the last of which covers it:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bash .claude/lib/bash/validate-parallel-manifest.sh docs/features/parallel/<slug>/parallel.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Exit 0 accepts the manifest, exit 1 rejects it with one error per line on stdout, and exit 2 means
|
|
84
|
+
the file is unreadable or uses a YAML construct outside the supported subset. The same entry point's
|
|
85
|
+
`--print-mode` and `--print-max-concurrency` subcommands supply `mode` and `max_concurrency` with
|
|
86
|
+
their documented defaults. `validate_parallel_manifest_text` in
|
|
87
|
+
`scripts/dev_tools/parallel_manifest_contract.py` remains the repository authority and the parity
|
|
88
|
+
reference; it is not invoked on the destination-runtime path. Cohort recoloring and concurrency
|
|
89
|
+
batching use `compute-cohorts.sh` and `compute-concurrency-batches.sh` under the same allowlist
|
|
90
|
+
entry.
|
|
91
|
+
|
|
92
|
+
The two `poetry run` grants remain for the repository-local paths that still need an interpreter:
|
|
93
|
+
the checkpoint-validator CLI fallback the skill names in its `## Parallel-Level Checkpoint` section
|
|
94
|
+
is invoked as `poetry run python -m`, and the drift-detection CLI likewise. Both grants stay scoped
|
|
75
95
|
to those two invocation forms only — not to `poetry run` as a whole — so `pytest`, `black`, `ruff`,
|
|
76
96
|
and every other `poetry run` subcommand remain outside the allowlist. The sibling persona
|
|
77
|
-
`.claude/agents/parallel-planner.md` records the same
|
|
78
|
-
upstream library.
|
|
97
|
+
`.claude/agents/parallel-planner.md` records the same destination-runtime posture.
|
|
79
98
|
|
|
80
99
|
## Startup Protocol
|
|
81
100
|
|
|
@@ -14,6 +14,9 @@ tools:
|
|
|
14
14
|
- "Bash(git *)"
|
|
15
15
|
- "Bash(gh *)"
|
|
16
16
|
- "Bash(poetry run *)"
|
|
17
|
+
- "Bash(bash .claude/lib/bash/compute-cohorts.sh*)"
|
|
18
|
+
- "Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"
|
|
19
|
+
- "Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)"
|
|
17
20
|
- "mcp__drm-copilot__validate_orchestration_artifacts"
|
|
18
21
|
skills:
|
|
19
22
|
- policy-compliance-order
|
|
@@ -133,17 +136,48 @@ Do not report completion until:
|
|
|
133
136
|
|
|
134
137
|
## Upstream Library Invocation
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
Every upstream library this planner needs is reachable from the published customization payload
|
|
140
|
+
alone, with no Python interpreter and no repository checkout. That is the point of the
|
|
141
|
+
destination-portability work in issue #462: a workspace that received `.claude` and `config` can
|
|
142
|
+
plan a parallel run.
|
|
143
|
+
|
|
144
|
+
**Blast radius — PowerShell port.** Radius derivation, V1-V3 validation, and the contention
|
|
145
|
+
relation come from `.claude/lib/blast-radius/BlastRadius.psm1`:
|
|
146
|
+
|
|
147
|
+
```powershell
|
|
148
|
+
Import-Module .claude/lib/blast-radius/BlastRadius.psm1 -Force
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The facade exports `Get-PlanPaths`, `Get-BlastRadius`, `Get-BlastRadiusFromObservedPaths`,
|
|
152
|
+
`Test-BlastRadius`, and `Test-BlastRadiusConflict`. Its truth table is
|
|
153
|
+
`config/blast-radius.json`, which push-down publishes alongside `.claude`.
|
|
154
|
+
|
|
155
|
+
**Cohort seeding and concurrency batching — bash entry points.** The bash library is granted as
|
|
156
|
+
three entry-point-specific allowlist entries — `"Bash(bash .claude/lib/bash/compute-cohorts.sh*)"`,
|
|
157
|
+
`"Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"`, and
|
|
158
|
+
`"Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)"` — one per command-line entry point.
|
|
159
|
+
The six sourceable libraries carry no grant because they are never invoked directly. The two
|
|
160
|
+
commands below require the first two of those entries:
|
|
143
161
|
|
|
144
162
|
```bash
|
|
145
|
-
|
|
163
|
+
bash .claude/lib/bash/compute-cohorts.sh --keys "<k1> <k2> ..." --edges "<a>:<b> ..."
|
|
164
|
+
bash .claude/lib/bash/compute-concurrency-batches.sh --keys "<k1> ..." --max-concurrency <n>
|
|
146
165
|
```
|
|
147
166
|
|
|
148
|
-
|
|
149
|
-
|
|
167
|
+
**Manifest validation — bash entry point.** The
|
|
168
|
+
`"Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)"` allowlist entry covers:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
bash .claude/lib/bash/validate-parallel-manifest.sh <manifest-path>
|
|
172
|
+
bash .claude/lib/bash/validate-parallel-manifest.sh --print-mode <manifest-path>
|
|
173
|
+
bash .claude/lib/bash/validate-parallel-manifest.sh --print-max-concurrency <manifest-path>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Python modules are the repository authority, not the runtime path.**
|
|
177
|
+
`scripts/dev_tools/compute_blast_radius.py`, `scripts/dev_tools/parallel_cohort_computation.py`,
|
|
178
|
+
and `scripts/dev_tools/parallel_manifest_contract.py` remain the reference implementations that the
|
|
179
|
+
ported libraries are asserted against by shared fixture corpora. Do not invoke them on the
|
|
180
|
+
destination-runtime path; cite them for their contract.
|
|
181
|
+
|
|
182
|
+
The `"Bash(poetry run *)"` allowlist entry is retained for the repository-local paths that still
|
|
183
|
+
need it — it is not required by any step above.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# compute-cohorts.sh: destination-portable command-line entry point for the
|
|
3
|
+
# parallel surface's cohort computation. It exists so a workspace that received
|
|
4
|
+
# the Claude customization payload can compute cohorts with nothing but bash --
|
|
5
|
+
# no Python, no Poetry, no repository checkout.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash .claude/lib/bash/compute-cohorts.sh --keys "<k1> <k2> ..." \
|
|
9
|
+
# [--edges "<a>:<b> <a>:<b> ..."]
|
|
10
|
+
#
|
|
11
|
+
# `--edges` is optional; omitting it, or passing an empty string, means the
|
|
12
|
+
# conflict graph has no edges. Item keys and edge endpoints are decimal
|
|
13
|
+
# integers matching `-?(0|[1-9][0-9]*)`; a token with a leading zero is
|
|
14
|
+
# rejected fail-closed with a lexical error, because the Python authority would
|
|
15
|
+
# read such a token differently and a silent disagreement is worse than a
|
|
16
|
+
# refusal.
|
|
17
|
+
#
|
|
18
|
+
# Output contract:
|
|
19
|
+
# stdout compact JSON array of arrays, identical to Python
|
|
20
|
+
# json.dumps(..., separators=(",", ":"))
|
|
21
|
+
# stderr on invalid input, the exact message the Python reference
|
|
22
|
+
# implementation raises
|
|
23
|
+
# exit 0 success
|
|
24
|
+
# exit 1 invalid input (duplicate key, self-loop, unknown endpoint)
|
|
25
|
+
# exit 2 usage error or a token outside the accepted integer lexis
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
# Resolve this script's own directory so the library sources regardless of cwd.
|
|
29
|
+
CC_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
30
|
+
# shellcheck source=.claude/lib/bash/parallel-cohorts.sh
|
|
31
|
+
# shellcheck disable=SC1091
|
|
32
|
+
source "$CC_SCRIPT_DIR/parallel-cohorts.sh"
|
|
33
|
+
|
|
34
|
+
pc_enforce_c_locale
|
|
35
|
+
|
|
36
|
+
cc_usage() {
|
|
37
|
+
# Print the entry point's usage text.
|
|
38
|
+
cat <<'EOF'
|
|
39
|
+
Usage: compute-cohorts.sh --keys "<k1> <k2> ..." [--edges "<a>:<b> ..."]
|
|
40
|
+
|
|
41
|
+
Computes parallel execution cohorts from an undirected conflict graph by
|
|
42
|
+
deterministic greedy graph coloring in Welsh-Powell order.
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
--keys Space-separated item keys (required; may be an empty string).
|
|
46
|
+
--edges Space-separated conflict edges as <a>:<b> (optional).
|
|
47
|
+
|
|
48
|
+
Prints a compact JSON array of arrays on stdout. On invalid input, prints the
|
|
49
|
+
reference implementation's exact message on stderr and exits 1.
|
|
50
|
+
EOF
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cc_require_integer() {
|
|
54
|
+
# Validate one token against the accepted decimal-integer lexis.
|
|
55
|
+
#
|
|
56
|
+
# Args: $1 = the token, $2 = a label naming where the token came from.
|
|
57
|
+
# Exits 2 with a lexical error when the token is outside the lexis.
|
|
58
|
+
local token="$1" label="$2"
|
|
59
|
+
if [[ ! $token =~ ^-?(0|[1-9][0-9]*)$ ]]; then
|
|
60
|
+
printf 'compute-cohorts.sh: %s must be a decimal integer matching -?(0|[1-9][0-9]*); found: %s\n' \
|
|
61
|
+
"$label" "$token" >&2
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
cc_validate_tokens() {
|
|
67
|
+
# Validate every key token and every edge endpoint.
|
|
68
|
+
#
|
|
69
|
+
# Args: $1 = space-separated keys, $2 = space-separated `a:b` edges.
|
|
70
|
+
local keys="$1" edges="$2" token
|
|
71
|
+
pcoh_split_words "$keys"
|
|
72
|
+
local -a key_tokens=("${PCOH_WORDS[@]}")
|
|
73
|
+
for token in "${key_tokens[@]}"; do
|
|
74
|
+
cc_require_integer "$token" "item key"
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
pcoh_split_words "$edges"
|
|
78
|
+
local -a edge_tokens=("${PCOH_WORDS[@]}")
|
|
79
|
+
# Each edge must be exactly two integer endpoints joined by a single colon;
|
|
80
|
+
# anything else is a malformed edge token rather than a graph error.
|
|
81
|
+
for token in "${edge_tokens[@]}"; do
|
|
82
|
+
if [[ $token != *:* || $token == *:*:* ]]; then
|
|
83
|
+
printf 'compute-cohorts.sh: edge must be <a>:<b>; found: %s\n' "$token" >&2
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
cc_require_integer "${token%%:*}" "edge endpoint"
|
|
87
|
+
cc_require_integer "${token#*:}" "edge endpoint"
|
|
88
|
+
done
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cc_main() {
|
|
92
|
+
# Parse arguments, compute the cohorts, and print the result.
|
|
93
|
+
local keys="" edges="" keys_seen=0
|
|
94
|
+
while (($# > 0)); do
|
|
95
|
+
case "$1" in
|
|
96
|
+
--keys)
|
|
97
|
+
(($# >= 2)) || {
|
|
98
|
+
cc_usage >&2
|
|
99
|
+
return 2
|
|
100
|
+
}
|
|
101
|
+
keys="$2"
|
|
102
|
+
keys_seen=1
|
|
103
|
+
shift 2
|
|
104
|
+
;;
|
|
105
|
+
--edges)
|
|
106
|
+
(($# >= 2)) || {
|
|
107
|
+
cc_usage >&2
|
|
108
|
+
return 2
|
|
109
|
+
}
|
|
110
|
+
edges="$2"
|
|
111
|
+
shift 2
|
|
112
|
+
;;
|
|
113
|
+
--help | -h)
|
|
114
|
+
cc_usage
|
|
115
|
+
return 0
|
|
116
|
+
;;
|
|
117
|
+
*)
|
|
118
|
+
cc_usage >&2
|
|
119
|
+
return 2
|
|
120
|
+
;;
|
|
121
|
+
esac
|
|
122
|
+
done
|
|
123
|
+
((keys_seen == 1)) || {
|
|
124
|
+
cc_usage >&2
|
|
125
|
+
return 2
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
cc_validate_tokens "$keys" "$edges"
|
|
129
|
+
if ! pcoh_compute_cohorts "$keys" "$edges"; then
|
|
130
|
+
printf '%s\n' "$PCOH_ERROR" >&2
|
|
131
|
+
return 1
|
|
132
|
+
fi
|
|
133
|
+
printf '%s\n' "$PCOH_RESULT"
|
|
134
|
+
return 0
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Guard so the file can be sourced without executing main. main's return code
|
|
138
|
+
# is captured and re-exited explicitly as the final statement.
|
|
139
|
+
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
|
|
140
|
+
cc_rc=0
|
|
141
|
+
cc_main "$@" || cc_rc=$?
|
|
142
|
+
exit "$cc_rc"
|
|
143
|
+
fi
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# compute-concurrency-batches.sh: destination-portable command-line entry point
|
|
3
|
+
# for the parallel surface's concurrency batching. It exists so a workspace that
|
|
4
|
+
# received the Claude customization payload can cap fan-out with nothing but
|
|
5
|
+
# bash -- no Python, no Poetry, no repository checkout.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash .claude/lib/bash/compute-concurrency-batches.sh \
|
|
9
|
+
# --keys "<k1> <k2> ..." --max-concurrency <n>
|
|
10
|
+
#
|
|
11
|
+
# Item keys are decimal integers matching `-?(0|[1-9][0-9]*)`; a token with a
|
|
12
|
+
# leading zero is rejected fail-closed with a lexical error, because the Python
|
|
13
|
+
# authority would read such a token differently and a silent disagreement is
|
|
14
|
+
# worse than a refusal.
|
|
15
|
+
#
|
|
16
|
+
# Output contract:
|
|
17
|
+
# stdout compact JSON array of arrays, identical to Python
|
|
18
|
+
# json.dumps(..., separators=(",", ":"))
|
|
19
|
+
# stderr on invalid input, the exact message the Python reference
|
|
20
|
+
# implementation raises
|
|
21
|
+
# exit 0 success
|
|
22
|
+
# exit 1 invalid input (max_concurrency below 1)
|
|
23
|
+
# exit 2 usage error or a token outside the accepted integer lexis
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# Resolve this script's own directory so the library sources regardless of cwd.
|
|
27
|
+
CB_SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
28
|
+
# shellcheck source=.claude/lib/bash/parallel-cohorts.sh
|
|
29
|
+
# shellcheck disable=SC1091
|
|
30
|
+
source "$CB_SCRIPT_DIR/parallel-cohorts.sh"
|
|
31
|
+
|
|
32
|
+
pc_enforce_c_locale
|
|
33
|
+
|
|
34
|
+
cb_usage() {
|
|
35
|
+
# Print the entry point's usage text.
|
|
36
|
+
cat <<'EOF'
|
|
37
|
+
Usage: compute-concurrency-batches.sh --keys "<k1> <k2> ..." --max-concurrency <n>
|
|
38
|
+
|
|
39
|
+
Chunks one cohort's item keys into concurrency-capped batches in ascending key
|
|
40
|
+
order. Every batch holds exactly <n> keys except a possibly smaller final batch.
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--keys Space-separated cohort item keys (required; may be empty).
|
|
44
|
+
--max-concurrency The fan-out cap (required).
|
|
45
|
+
|
|
46
|
+
Prints a compact JSON array of arrays on stdout. When the cap is below 1, prints
|
|
47
|
+
the reference implementation's exact message on stderr and exits 1.
|
|
48
|
+
EOF
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
cb_require_integer() {
|
|
52
|
+
# Validate one token against the accepted decimal-integer lexis.
|
|
53
|
+
#
|
|
54
|
+
# Args: $1 = the token, $2 = a label naming where the token came from.
|
|
55
|
+
# Exits 2 with a lexical error when the token is outside the lexis.
|
|
56
|
+
local token="$1" label="$2"
|
|
57
|
+
if [[ ! $token =~ ^-?(0|[1-9][0-9]*)$ ]]; then
|
|
58
|
+
printf 'compute-concurrency-batches.sh: %s must be a decimal integer matching -?(0|[1-9][0-9]*); found: %s\n' \
|
|
59
|
+
"$label" "$token" >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cb_main() {
|
|
65
|
+
# Parse arguments, compute the batches, and print the result.
|
|
66
|
+
local keys="" cap="" keys_seen=0 token
|
|
67
|
+
while (($# > 0)); do
|
|
68
|
+
case "$1" in
|
|
69
|
+
--keys)
|
|
70
|
+
(($# >= 2)) || {
|
|
71
|
+
cb_usage >&2
|
|
72
|
+
return 2
|
|
73
|
+
}
|
|
74
|
+
keys="$2"
|
|
75
|
+
keys_seen=1
|
|
76
|
+
shift 2
|
|
77
|
+
;;
|
|
78
|
+
--max-concurrency)
|
|
79
|
+
(($# >= 2)) || {
|
|
80
|
+
cb_usage >&2
|
|
81
|
+
return 2
|
|
82
|
+
}
|
|
83
|
+
cap="$2"
|
|
84
|
+
shift 2
|
|
85
|
+
;;
|
|
86
|
+
--help | -h)
|
|
87
|
+
cb_usage
|
|
88
|
+
return 0
|
|
89
|
+
;;
|
|
90
|
+
*)
|
|
91
|
+
cb_usage >&2
|
|
92
|
+
return 2
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
done
|
|
96
|
+
if ((keys_seen == 0)) || [[ -z $cap ]]; then
|
|
97
|
+
cb_usage >&2
|
|
98
|
+
return 2
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
cb_require_integer "$cap" "max_concurrency"
|
|
102
|
+
pcoh_split_words "$keys"
|
|
103
|
+
local -a key_tokens=("${PCOH_WORDS[@]}")
|
|
104
|
+
for token in "${key_tokens[@]}"; do
|
|
105
|
+
cb_require_integer "$token" "item key"
|
|
106
|
+
done
|
|
107
|
+
|
|
108
|
+
if ! pcoh_compute_concurrency_batches "$keys" "$cap"; then
|
|
109
|
+
printf '%s\n' "$PCOH_ERROR" >&2
|
|
110
|
+
return 1
|
|
111
|
+
fi
|
|
112
|
+
printf '%s\n' "$PCOH_RESULT"
|
|
113
|
+
return 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Guard so the file can be sourced without executing main. main's return code
|
|
117
|
+
# is captured and re-exited explicitly as the final statement.
|
|
118
|
+
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
|
|
119
|
+
cb_rc=0
|
|
120
|
+
cb_main "$@" || cb_rc=$?
|
|
121
|
+
exit "$cb_rc"
|
|
122
|
+
fi
|