claude-dev-env 2.34.0 → 2.36.0
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/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
- package/bin/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/audit-hooks-cli.mjs +54 -0
- package/scripts/audit-hooks.mjs +397 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +328 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +352 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +66 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +387 -0
- package/scripts/policy_lint/registry.py +162 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- package/scripts/select_pstack_models.mjs +304 -0
- package/scripts/tests/test-audit-hooks.mjs +379 -0
- package/scripts/tests/test_cde_lint.py +374 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +252 -0
- package/scripts/tests/test_policy_lint_selection.py +373 -0
|
@@ -59,8 +59,10 @@ python "$HOME/.claude/skills/_shared/pr-loop/scripts/select_converge_pacer.py" \
|
|
|
59
59
|
[`../_shared/pr-loop/portable-driver.md`](../_shared/pr-loop/portable-driver.md).
|
|
60
60
|
**Do not abort** because the Workflow tool is missing.
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
`
|
|
62
|
+
Set the GitHub transport directive before any GitHub step. Use `transport: "cloud"` in a
|
|
63
|
+
Cloud session. Use `transport: "local"` in a local session. Cloud uses the
|
|
64
|
+
connected GitHub MCP tools and never starts a local GitHub CLI authentication
|
|
65
|
+
flow.
|
|
64
66
|
|
|
65
67
|
## Review-lens boundary
|
|
66
68
|
|
|
@@ -68,18 +70,19 @@ The code-review lens boundary (workflow agent versus the built-in `/code-review`
|
|
|
68
70
|
command) is defined on the **Code-review lens** bullet in
|
|
69
71
|
[`reference/convergence.md`](reference/convergence.md).
|
|
70
72
|
|
|
71
|
-
##
|
|
73
|
+
## Select the GitHub transport directive before any GitHub step
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`pr-loop-cloud-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
In a Cloud session, set `transport: "cloud"` and load the GitHub MCP schemas
|
|
76
|
+
before the first GitHub tool call. Do not run any local GitHub CLI
|
|
77
|
+
authentication, account-switch, or token command. Use the operation in
|
|
78
|
+
[`cloud-pr-loop-compatibility.md`](../../../docs/references/cloud-pr-loop-compatibility.md)
|
|
79
|
+
for each GitHub read or write. Use the Cloud session's credentialed push path
|
|
80
|
+
for local commits. If that path rejects a push, report the exact error instead
|
|
81
|
+
of trying to log in through the local CLI.
|
|
82
|
+
|
|
83
|
+
In a local session, set `transport: "local"` and use the authenticated local
|
|
84
|
+
GitHub CLI. The workflow defaults to this value when older callers omit the
|
|
85
|
+
field.
|
|
83
86
|
|
|
84
87
|
## Pre-flight (main session)
|
|
85
88
|
|
|
@@ -161,7 +164,7 @@ orchestrating session's own steps, and the script's agent-prompt text carries
|
|
|
161
164
|
```
|
|
162
165
|
Workflow({
|
|
163
166
|
scriptPath: "<this skill dir>/workflow/converge.mjs",
|
|
164
|
-
args: { owner: "<O>", repo: "<R>", prNumber: <N>, bugbotDisabled: false, copilotDisabled: false, homeDirectory: "<HOME>" }
|
|
167
|
+
args: { owner: "<O>", repo: "<R>", prNumber: <N>, bugbotDisabled: false, copilotDisabled: false, homeDirectory: "<HOME>", transport: "<cloud|local>" }
|
|
165
168
|
})
|
|
166
169
|
```
|
|
167
170
|
|
|
@@ -173,7 +176,8 @@ the absolute path to the directory that holds `.claude` (resolve it once from
|
|
|
173
176
|
runs in a sandbox with no access to environment variables, so the run reads the
|
|
174
177
|
home directory from this arg to build the path to the codex-review scripts the
|
|
175
178
|
Codex gate calls; leave it out and the Codex gate cannot find those scripts. Set
|
|
176
|
-
`
|
|
179
|
+
`transport: "cloud"` for Cloud and `transport: "local"` for local execution.
|
|
180
|
+
Set `bugbotDisabled: true` only when the user has opted Cursor Bugbot out for the
|
|
177
181
|
run; otherwise the workflow detects an opt-out or an unreachable Bugbot on its
|
|
178
182
|
own. Set `copilotDisabled: true` when the step 5 quota pre-check exits non-zero,
|
|
179
183
|
and `false` when it exits 0; on `true` the workflow skips the Copilot gate with
|
|
@@ -46,9 +46,9 @@ fails in a new way.
|
|
|
46
46
|
not `~/.claude/...`; a tilde resolves to the wrong home and gives a
|
|
47
47
|
file-not-found error that looks like a script failure.
|
|
48
48
|
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
- **Local identity drift across accounts.** A local run that touches more than
|
|
50
|
+
one GitHub account must use the selected local account. Cloud runs use the
|
|
51
|
+
connected GitHub MCP identity and never use local CLI identity commands.
|
|
52
52
|
|
|
53
53
|
- **Commit or push fails after a repair.** Read the hook or command output,
|
|
54
54
|
repair the named issue, and record the rerun. Apply the
|
|
@@ -73,6 +73,7 @@ Workflow({
|
|
|
73
73
|
args: {
|
|
74
74
|
convergeScriptPath: "<this skill dir>/workflow/converge.mjs",
|
|
75
75
|
homeDirectory: "<HOME>",
|
|
76
|
+
transport: "<cloud|local>",
|
|
76
77
|
prs: [
|
|
77
78
|
{ owner: "<O>", repo: "<R>", prNumber: <N1>, repoPath: "<abs worktree 1>", bugbotDisabled: false, copilotDisabled: false },
|
|
78
79
|
{ owner: "<O>", repo: "<R>", prNumber: <N2>, repoPath: "<abs worktree 2>", bugbotDisabled: false, copilotDisabled: false }
|
|
@@ -87,8 +88,11 @@ checked out in. `homeDirectory` is the absolute path to the directory that holds
|
|
|
87
88
|
`.claude` (resolve it once from `$HOME`, or `$env:USERPROFILE` on Windows, with
|
|
88
89
|
forward slashes); the fan-out forwards it to every child run, which needs it to
|
|
89
90
|
build the codex-review scripts path because the workflow sandbox has no access to
|
|
90
|
-
environment variables.
|
|
91
|
-
|
|
91
|
+
environment variables. Set `transport: "cloud"` in a Cloud session and
|
|
92
|
+
`transport: "local"` in a local session. Cloud uses the connected GitHub MCP
|
|
93
|
+
tools and skips local CLI authentication. The workflow runs in the background
|
|
94
|
+
and notifies the session on completion; watch live progress with `/workflows`,
|
|
95
|
+
where each PR's child run
|
|
92
96
|
appears under its own group.
|
|
93
97
|
|
|
94
98
|
The workflow returns
|
|
@@ -32,6 +32,17 @@ test('review tasks use code-quality-agent and keep the working tree read-only',
|
|
|
32
32
|
assert.match(reviewTaskSource, /Make no edit to the tree under verification/)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
+
test('workflow agents receive the selected GitHub transport before role instructions', () => {
|
|
36
|
+
assert.match(
|
|
37
|
+
workflowSource,
|
|
38
|
+
/HEADLESS_EDIT_PREAMBLE\}\$\{githubTransportDirective\(activeGithubTransport\)\}\$\{worktreeDirective/,
|
|
39
|
+
)
|
|
40
|
+
assert.match(
|
|
41
|
+
workflowSource,
|
|
42
|
+
/HEADLESS_READONLY_PREAMBLE\}\$\{githubTransportDirective\(activeGithubTransport\)\}\$\{worktreeDirective/,
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
35
46
|
test('review result parser accepts one standalone terminal disposition only', () => {
|
|
36
47
|
const parseReviewResult = loadReviewResultParser()
|
|
37
48
|
|
|
@@ -91,6 +91,7 @@ const HEADLESS_READONLY_PREAMBLE = HEADLESS_EDIT_PREAMBLE.replace(
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
let activeRepoPath = null
|
|
94
|
+
let activeGithubTransport = 'local'
|
|
94
95
|
|
|
95
96
|
/**
|
|
96
97
|
* Build the per-agent worktree directive for a path-scoped run.
|
|
@@ -111,33 +112,11 @@ const worktreeDirective = (repoPath) =>
|
|
|
111
112
|
? `WORKTREE — this PR is checked out at ${repoPath}. Unless a step explicitly names a different repository directory (for example an environment-hardening repo checkout, which you cd into exactly as that step directs), run every git, gh, diff, edit, commit, push, and test command for this PR in that worktree: cd "${repoPath}" before any such command, and resolve repository roots from there.\n\n`
|
|
112
113
|
: ''
|
|
113
114
|
|
|
114
|
-
/**
|
|
115
|
-
* Spawn a workflow agent that edits code, with the full edit preamble prepended
|
|
116
|
-
* to its prompt. An edit agent may run rm during cleanup, so it inherits the
|
|
117
|
-
* rm-shape rules alongside the no-confirmation-prompt guidance. On a path-scoped
|
|
118
|
-
* run the worktree directive is prepended too, so the agent runs in the PR's own
|
|
119
|
-
* worktree (activeRepoPath); on a single-PR run that directive is empty and the
|
|
120
|
-
* agent keeps its own working directory. A read-only agent (a review, verify, or
|
|
121
|
-
* utility spawn) routes through convergeReadOnlyAgent for the trimmed preamble.
|
|
122
|
-
* @param {string} prompt the agent's role-specific instruction body
|
|
123
|
-
* @param {object} options the agent() options (label, phase, schema, agentType, model)
|
|
124
|
-
* @returns {Promise<*>} the agent() result
|
|
125
|
-
*/
|
|
126
115
|
const convergeAgent = (prompt, options) =>
|
|
127
|
-
agent(`${HEADLESS_EDIT_PREAMBLE}${worktreeDirective(activeRepoPath)}${prompt}`, options)
|
|
116
|
+
agent(`${HEADLESS_EDIT_PREAMBLE}${githubTransportDirective(activeGithubTransport)}${worktreeDirective(activeRepoPath)}${prompt}`, options)
|
|
128
117
|
|
|
129
|
-
/**
|
|
130
|
-
* Spawn a read-only workflow agent with the trimmed read-only preamble prepended
|
|
131
|
-
* to its prompt. A review, verify, or utility agent edits nothing and never runs
|
|
132
|
-
* rm, so it receives the read-only preamble that drops the rm-shape-rules bullet.
|
|
133
|
-
* The worktree directive is prepended on a path-scoped run exactly as for
|
|
134
|
-
* convergeAgent, so a read-only agent still runs in the PR's own worktree.
|
|
135
|
-
* @param {string} prompt the agent's role-specific instruction body
|
|
136
|
-
* @param {object} options the agent() options (label, phase, schema, agentType, model)
|
|
137
|
-
* @returns {Promise<*>} the agent() result
|
|
138
|
-
*/
|
|
139
118
|
const convergeReadOnlyAgent = (prompt, options) =>
|
|
140
|
-
agent(`${HEADLESS_READONLY_PREAMBLE}${worktreeDirective(activeRepoPath)}${prompt}`, options)
|
|
119
|
+
agent(`${HEADLESS_READONLY_PREAMBLE}${githubTransportDirective(activeGithubTransport)}${worktreeDirective(activeRepoPath)}${prompt}`, options)
|
|
141
120
|
|
|
142
121
|
/**
|
|
143
122
|
* Spawn a fresh git-utility Explore agent for a specific task. The one task,
|
|
@@ -1627,19 +1606,32 @@ function normalizeRunInput(rawArgs) {
|
|
|
1627
1606
|
}
|
|
1628
1607
|
}
|
|
1629
1608
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1609
|
+
const DEFAULT_GITHUB_TRANSPORT = 'local'
|
|
1610
|
+
const GITHUB_TRANSPORT_DIRECTIVES = {
|
|
1611
|
+
local:
|
|
1612
|
+
'LOCAL GITHUB TRANSPORT DIRECTIVE. Use the authenticated local GitHub CLI for GitHub operations. Report local authentication or permission failures directly.\n\n',
|
|
1613
|
+
cloud:
|
|
1614
|
+
'CLOUD GITHUB TRANSPORT DIRECTIVE. Use the connected GitHub MCP tools for every GitHub API read and write. Load the GitHub MCP schemas before the first MCP call and resolve the connected identity with mcp__github__get_me. Do not invoke a local GitHub CLI for authentication, account switching, token lookup, pull-request operations, review operations, or push setup. Replace local CLI steps in this prompt with the matching operation in docs/references/cloud-pr-loop-compatibility.md. For a local commit push, use only the credentialed push path supplied by the Cloud session. If no credentialed path is available, return a named blocker instead of attempting a login.\n\n',
|
|
1615
|
+
}
|
|
1616
|
+
const VALID_GITHUB_TRANSPORTS = new Set(Object.keys(GITHUB_TRANSPORT_DIRECTIVES))
|
|
1617
|
+
|
|
1618
|
+
function githubTransportDirective(transport) {
|
|
1619
|
+
return GITHUB_TRANSPORT_DIRECTIVES[transport] || GITHUB_TRANSPORT_DIRECTIVES[DEFAULT_GITHUB_TRANSPORT]
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1639
1622
|
function classifyRunInput(rawArgs) {
|
|
1640
1623
|
const candidate = normalizeRunInput(rawArgs)
|
|
1641
1624
|
const hasUsableCoordinates =
|
|
1642
1625
|
candidate != null && candidate.owner && candidate.repo && candidate.prNumber
|
|
1626
|
+
const hasValidTransport =
|
|
1627
|
+
candidate != null &&
|
|
1628
|
+
(candidate.transport === undefined || VALID_GITHUB_TRANSPORTS.has(candidate.transport))
|
|
1629
|
+
if (hasUsableCoordinates && !hasValidTransport) {
|
|
1630
|
+
return {
|
|
1631
|
+
input: null,
|
|
1632
|
+
blocker: 'invalid run transport: expected "local" or "cloud"',
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1643
1635
|
if (hasUsableCoordinates) return { input: candidate, blocker: null }
|
|
1644
1636
|
return {
|
|
1645
1637
|
input: null,
|
|
@@ -1654,6 +1646,7 @@ if (runInput.blocker) {
|
|
|
1654
1646
|
}
|
|
1655
1647
|
const input = runInput.input
|
|
1656
1648
|
activeRepoPath = typeof input.repoPath === 'string' && input.repoPath ? input.repoPath : null
|
|
1649
|
+
activeGithubTransport = input.transport === undefined ? DEFAULT_GITHUB_TRANSPORT : input.transport
|
|
1657
1650
|
const prCoordinates = `owner=${input.owner} repo=${input.repo} PR #${input.prNumber} (https://github.com/${input.owner}/${input.repo}/pull/${input.prNumber})`
|
|
1658
1651
|
|
|
1659
1652
|
/**
|
|
@@ -17,10 +17,10 @@ function sourceSliceBetween(startNeedle, endNeedle) {
|
|
|
17
17
|
|
|
18
18
|
const productionModule = new Function(
|
|
19
19
|
`${sourceSliceBetween('function normalizeRunInput(', '\nconst runInput =')}\n` +
|
|
20
|
-
'return { normalizeRunInput, classifyRunInput };',
|
|
20
|
+
'return { normalizeRunInput, classifyRunInput, githubTransportDirective };',
|
|
21
21
|
)();
|
|
22
22
|
|
|
23
|
-
const { normalizeRunInput, classifyRunInput } = productionModule;
|
|
23
|
+
const { normalizeRunInput, classifyRunInput, githubTransportDirective } = productionModule;
|
|
24
24
|
|
|
25
25
|
const VALID_COORDINATES = { owner: 'jl-cmd', repo: 'claude-dev-env', prNumber: 543 };
|
|
26
26
|
|
|
@@ -46,6 +46,19 @@ test('classifyRunInput accepts coordinates carrying owner, repo, and prNumber',
|
|
|
46
46
|
assert.equal(classified.blocker, null);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
test('classifyRunInput accepts the cloud transport without changing the input shape', () => {
|
|
50
|
+
const cloudInput = { ...VALID_COORDINATES, transport: 'cloud' };
|
|
51
|
+
const classified = classifyRunInput(cloudInput);
|
|
52
|
+
assert.deepEqual(classified.input, cloudInput);
|
|
53
|
+
assert.equal(classified.blocker, null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('classifyRunInput blocks an unknown transport before any GitHub work starts', () => {
|
|
57
|
+
const classified = classifyRunInput({ ...VALID_COORDINATES, transport: 'proxy' });
|
|
58
|
+
assert.equal(classified.input, null);
|
|
59
|
+
assert.match(classified.blocker, /transport/i);
|
|
60
|
+
});
|
|
61
|
+
|
|
49
62
|
test('classifyRunInput blocks a failed parse with a structured blocker and no input', () => {
|
|
50
63
|
const classified = classifyRunInput('not json at all');
|
|
51
64
|
assert.equal(classified.input, null);
|
|
@@ -90,3 +103,14 @@ test('classifyRunInput leaves copilotDisabled undefined when the input omits it'
|
|
|
90
103
|
const classified = classifyRunInput(VALID_COORDINATES);
|
|
91
104
|
assert.equal(classified.input.copilotDisabled, undefined);
|
|
92
105
|
});
|
|
106
|
+
|
|
107
|
+
test('the cloud transport directive selects MCP and contains no local CLI auth command', () => {
|
|
108
|
+
const cloudDirective = githubTransportDirective('cloud');
|
|
109
|
+
assert.match(cloudDirective, /mcp__github__/);
|
|
110
|
+
assert.match(cloudDirective, /credentialed push path/);
|
|
111
|
+
assert.doesNotMatch(cloudDirective, /gh auth/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('the local transport directive keeps the local CLI path', () => {
|
|
115
|
+
assert.match(githubTransportDirective('local'), /authenticated local GitHub CLI/);
|
|
116
|
+
});
|
|
@@ -57,18 +57,9 @@ function isUsablePrEntry(prEntry) {
|
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* A fan-out run needs the absolute converge.mjs script path and a non-empty list
|
|
64
|
-
* of PR entries that each carry owner, repo, prNumber, and the absolute worktree
|
|
65
|
-
* path the PR is checked out in. A payload that fails to parse, a non-string
|
|
66
|
-
* convergeScriptPath, a missing or empty prs list, or any entry missing a
|
|
67
|
-
* coordinate yields a blocker the top-level run reports as
|
|
68
|
-
* {converged:false, blocker} rather than throwing on a missing field.
|
|
69
|
-
* @param {string|object} rawArgs the workflow args global (JSON string or object)
|
|
70
|
-
* @returns {{input: object|null, blocker: string|null}} usable coordinates or a blocker
|
|
71
|
-
*/
|
|
60
|
+
const DEFAULT_GITHUB_TRANSPORT = 'local'
|
|
61
|
+
const VALID_GITHUB_TRANSPORTS = new Set(['local', 'cloud'])
|
|
62
|
+
|
|
72
63
|
function classifyMultiInput(rawArgs) {
|
|
73
64
|
const candidate = normalizeMultiInput(rawArgs)
|
|
74
65
|
if (candidate == null) {
|
|
@@ -84,6 +75,12 @@ function classifyMultiInput(rawArgs) {
|
|
|
84
75
|
'invalid run coordinates: convergeScriptPath (absolute path to converge.mjs) is required',
|
|
85
76
|
}
|
|
86
77
|
}
|
|
78
|
+
if (candidate.transport !== undefined && !VALID_GITHUB_TRANSPORTS.has(candidate.transport)) {
|
|
79
|
+
return {
|
|
80
|
+
input: null,
|
|
81
|
+
blocker: 'invalid run transport: expected "local" or "cloud"',
|
|
82
|
+
}
|
|
83
|
+
}
|
|
87
84
|
if (!Array.isArray(candidate.prs) || candidate.prs.length === 0) {
|
|
88
85
|
return {
|
|
89
86
|
input: null,
|
|
@@ -102,21 +99,7 @@ function classifyMultiInput(rawArgs) {
|
|
|
102
99
|
return { input: candidate, blocker: null }
|
|
103
100
|
}
|
|
104
101
|
|
|
105
|
-
|
|
106
|
-
* Build the converge.mjs child-run args for one validated PR entry.
|
|
107
|
-
*
|
|
108
|
-
* Every per-run opt-out the child converge.mjs reads is forwarded here so the
|
|
109
|
-
* fan-out honors it: bugbotDisabled skips the Bugbot phase, and copilotDisabled
|
|
110
|
-
* short-circuits the Copilot quota gate when the account is out of premium
|
|
111
|
-
* requests. An entry that omits an opt-out defaults it to false so the child
|
|
112
|
-
* runs that phase. homeDirectory is a run-level value forwarded to every child
|
|
113
|
-
* so the child can build its codex-review scripts path: the workflow sandbox
|
|
114
|
-
* exposes no process global, so the child reads home from its args, not env.
|
|
115
|
-
* @param {object} prEntry one validated element of the args.prs array
|
|
116
|
-
* @param {string} homeDirectory the run-level home directory for every child
|
|
117
|
-
* @returns {object} the args object passed to the converge.mjs child run
|
|
118
|
-
*/
|
|
119
|
-
function childRunInput(prEntry, homeDirectory) {
|
|
102
|
+
function childRunInput(prEntry, homeDirectory, transport = DEFAULT_GITHUB_TRANSPORT) {
|
|
120
103
|
return {
|
|
121
104
|
owner: prEntry.owner,
|
|
122
105
|
repo: prEntry.repo,
|
|
@@ -125,6 +108,7 @@ function childRunInput(prEntry, homeDirectory) {
|
|
|
125
108
|
bugbotDisabled: Boolean(prEntry.bugbotDisabled),
|
|
126
109
|
copilotDisabled: Boolean(prEntry.copilotDisabled),
|
|
127
110
|
homeDirectory,
|
|
111
|
+
transport,
|
|
128
112
|
}
|
|
129
113
|
}
|
|
130
114
|
|
|
@@ -141,7 +125,7 @@ const childResults = await parallel(
|
|
|
141
125
|
input.prs.map((eachPr) => async () => {
|
|
142
126
|
const childOutcome = await workflow(
|
|
143
127
|
{ scriptPath: input.convergeScriptPath },
|
|
144
|
-
childRunInput(eachPr, input.homeDirectory),
|
|
128
|
+
childRunInput(eachPr, input.homeDirectory, input.transport),
|
|
145
129
|
)
|
|
146
130
|
return {
|
|
147
131
|
owner: eachPr.owner,
|
|
@@ -68,6 +68,18 @@ test('valid coordinates classify with no blocker and keep every PR entry', () =>
|
|
|
68
68
|
assert.equal(classified.input.prs.length, 2);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
test('a cloud transport classifies with no blocker', () => {
|
|
72
|
+
const classified = classifyMultiInput({ ...validArgs(), transport: 'cloud' });
|
|
73
|
+
assert.equal(classified.blocker, null);
|
|
74
|
+
assert.equal(classified.input.transport, 'cloud');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('an unknown transport is blocked before child workflows start', () => {
|
|
78
|
+
const classified = classifyMultiInput({ ...validArgs(), transport: 'proxy' });
|
|
79
|
+
assert.equal(classified.input, null);
|
|
80
|
+
assert.match(classified.blocker, /transport/i);
|
|
81
|
+
});
|
|
82
|
+
|
|
71
83
|
test('a missing convergeScriptPath is blocked', () => {
|
|
72
84
|
const classified = classifyMultiInput({ prs: [validEntry(101)] });
|
|
73
85
|
assert.equal(classified.input, null);
|
|
@@ -138,6 +150,10 @@ test('childRunInput defaults bugbotDisabled to false when the entry omits it', (
|
|
|
138
150
|
assert.equal(childRunInput(validEntry(101)).bugbotDisabled, false);
|
|
139
151
|
});
|
|
140
152
|
|
|
153
|
+
test('childRunInput forwards the selected cloud transport', () => {
|
|
154
|
+
assert.equal(childRunInput(validEntry(101), '/home/dev', 'cloud').transport, 'cloud');
|
|
155
|
+
});
|
|
156
|
+
|
|
141
157
|
test('childRunInput forwards the run-level home directory to the child run', () => {
|
|
142
158
|
const childArgs = childRunInput(validEntry(101), '/home/dev');
|
|
143
159
|
assert.equal(childArgs.homeDirectory, '/home/dev');
|
package/bin/cde.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { constants as osConstants } from "node:os";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const lintScriptPath = path.join(packageRoot, "scripts", "cde_lint.py");
|
|
9
|
+
const posixSignalStatusOffset = 128;
|
|
10
|
+
const lintCommandName = "lint";
|
|
11
|
+
const pythonOptionName = "--python";
|
|
12
|
+
const pythonEnvironmentName = "CDE_PYTHON";
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export function createHelpText() {
|
|
16
|
+
return [
|
|
17
|
+
"Usage: cde lint [options]",
|
|
18
|
+
"",
|
|
19
|
+
"Run the policy linter. Source modes are mutually exclusive:",
|
|
20
|
+
" --files PATH [PATH ...] Current worktree files",
|
|
21
|
+
" --staged Staged index content",
|
|
22
|
+
" --base REVISION Changes from a base revision",
|
|
23
|
+
" --repository Tracked worktree files",
|
|
24
|
+
" --text-as PATH One editor buffer from standard input",
|
|
25
|
+
"",
|
|
26
|
+
"Report formats: --format text|json|editor",
|
|
27
|
+
"",
|
|
28
|
+
"Launcher options:",
|
|
29
|
+
" --python <command> Python interpreter override",
|
|
30
|
+
" Exit 0 clean, 1 diagnostics, 2 invalid input or start failure, 3 failed rule",
|
|
31
|
+
"",
|
|
32
|
+
"Python may also be selected with CDE_PYTHON.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export function pythonInterpreterNames(platformName = process.platform) {
|
|
38
|
+
if (platformName === "win32") return ["py", "python"];
|
|
39
|
+
return ["python3", "python"];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export function findPython(
|
|
44
|
+
commandNames = pythonInterpreterNames(),
|
|
45
|
+
canRun = (commandName) => spawnSync(commandName, ["--version"], { stdio: "ignore", shell: false }).status === 0,
|
|
46
|
+
) {
|
|
47
|
+
for (const eachCommand of commandNames) {
|
|
48
|
+
if (canRun(eachCommand)) return eachCommand;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
export function buildLintCommand(interpreter, forwardedArguments) {
|
|
55
|
+
return {
|
|
56
|
+
executable: interpreter,
|
|
57
|
+
arguments: [lintScriptPath, ...forwardedArguments],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
function takePythonOverride(allArguments) {
|
|
63
|
+
const remainingArguments = [];
|
|
64
|
+
let interpreterOverride;
|
|
65
|
+
for (let index = 0; index < allArguments.length; index += 1) {
|
|
66
|
+
if (allArguments[index] === pythonOptionName) {
|
|
67
|
+
const nextArgument = allArguments[index + 1];
|
|
68
|
+
if (nextArgument === undefined || nextArgument.startsWith("--")) {
|
|
69
|
+
return { error: "--python requires an interpreter command" };
|
|
70
|
+
}
|
|
71
|
+
interpreterOverride = nextArgument;
|
|
72
|
+
index += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
remainingArguments.push(allArguments[index]);
|
|
76
|
+
}
|
|
77
|
+
return { interpreterOverride, remainingArguments, error: undefined };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
function attachSignalForwarding(childProcess, processEmitter) {
|
|
82
|
+
const allSignals = ["SIGINT", "SIGTERM"];
|
|
83
|
+
if (process.platform !== "win32") {
|
|
84
|
+
allSignals.push("SIGHUP");
|
|
85
|
+
}
|
|
86
|
+
const listenerBySignal = [];
|
|
87
|
+
for (const eachSignal of allSignals) {
|
|
88
|
+
const listener = () => {
|
|
89
|
+
try {
|
|
90
|
+
childProcess.kill(eachSignal);
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
processEmitter.on(eachSignal, listener);
|
|
96
|
+
listenerBySignal.push([eachSignal, listener]);
|
|
97
|
+
}
|
|
98
|
+
return () => {
|
|
99
|
+
for (const [eachSignal, listener] of listenerBySignal) {
|
|
100
|
+
processEmitter.off(eachSignal, listener);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
function statusCodeForSignal(signalName) {
|
|
107
|
+
const signalNumber = osConstants.signals?.[signalName];
|
|
108
|
+
if (signalNumber === undefined) return 1;
|
|
109
|
+
return posixSignalStatusOffset + signalNumber;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
export function runLintCommand(command, dependencies = {}) {
|
|
114
|
+
const runChild = dependencies.runChild ?? spawn;
|
|
115
|
+
const processEmitter = dependencies.processEmitter ?? process;
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
let childProcess;
|
|
118
|
+
try {
|
|
119
|
+
childProcess = runChild(command.executable, command.arguments, {
|
|
120
|
+
stdio: "inherit",
|
|
121
|
+
shell: false,
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
processEmitter.stderr.write("Unable to start the policy linter.\n");
|
|
126
|
+
resolve(2);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const detachSignals = attachSignalForwarding(childProcess, processEmitter);
|
|
130
|
+
childProcess.on("error", () => {
|
|
131
|
+
detachSignals();
|
|
132
|
+
processEmitter.stderr.write("Unable to start the policy linter.\n");
|
|
133
|
+
resolve(2);
|
|
134
|
+
});
|
|
135
|
+
childProcess.on("close", (exitCode, signalName) => {
|
|
136
|
+
detachSignals();
|
|
137
|
+
if (exitCode !== null) {
|
|
138
|
+
resolve(exitCode);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
resolve(statusCodeForSignal(signalName));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
export async function main(argumentsList = process.argv.slice(2), dependencies = {}) {
|
|
148
|
+
if (argumentsList.length === 0 || argumentsList.includes("--help") || argumentsList.includes("-h")) {
|
|
149
|
+
process.stdout.write(`${createHelpText()}\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (argumentsList[0] !== lintCommandName) {
|
|
153
|
+
process.stderr.write(`${createHelpText()}\n`);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
const { interpreterOverride, remainingArguments, error } = takePythonOverride(argumentsList.slice(1));
|
|
157
|
+
if (error) {
|
|
158
|
+
process.stderr.write(`${error}\n`);
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
const resolvePython = dependencies.findPython ?? findPython;
|
|
162
|
+
const interpreter = interpreterOverride ?? process.env[pythonEnvironmentName] ?? await resolvePython();
|
|
163
|
+
if (!interpreter) {
|
|
164
|
+
process.stderr.write("No usable Python interpreter found. Use --python or CDE_PYTHON.\n");
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
const command = buildLintCommand(interpreter, remainingArguments);
|
|
168
|
+
const runCommand = dependencies.runCommand ?? runLintCommand;
|
|
169
|
+
return await runCommand(command, dependencies);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
const isLaunchedDirectly = process.argv[1] !== undefined
|
|
174
|
+
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
175
|
+
if (isLaunchedDirectly) process.exitCode = await main();
|