Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
9df3852
feat(pr-pipeline): run resolve-issue tokenless and commit its patch w…
kyle-sexton Oct 20, 2026
9480abb
Merge remote-tracking branch 'origin/main' into feat/pr-pipeline-appl…
kyle-sexton Oct 20, 2026
28a02f8
fix(pr-pipeline): hold a resumed issue branch and patched blob sizes …
kyle-sexton Oct 20, 2026
7d372e9
feat(pr-pipeline): admit an issue lane run before any credentialed job
kyle-sexton Oct 20, 2026
f51c6e3
Merge origin/main into feat/pr-pipeline-apply-issue-patch
kyle-sexton Oct 20, 2026
13aa87c
Merge origin/main into feat/pr-pipeline-apply-issue-patch
kyle-sexton Oct 20, 2026
35c8d50
Merge branch 'main' into feat/pr-pipeline-apply-issue-patch
kyle-sexton Oct 20, 2026
d46558f
feat(pr-pipeline): route fix-ci's model through the multi-agent role map
kyle-sexton Oct 20, 2026
6c7aef3
fix(pr-pipeline): never route a role profile to the frontier tier
kyle-sexton Oct 20, 2026
a7f6009
fix(pr-pipeline): treat an unreadable role map as absent
kyle-sexton Oct 20, 2026
253d82b
fix(pr-pipeline): give write lanes the Grep and Glob tools
kyle-sexton Oct 20, 2026
b4f4474
Merge branch 'main' into fix/write-lane-grep-glob
kyle-sexton Oct 20, 2026
2516cae
Merge commit 'a7f60091eeb169a0c45ff42b2f08877065bea2e7' into feat/pr-…
kyle-sexton Oct 20, 2026
38c35bb
Merge commit 'b4f44742c7eaecc1514353f3edb55f332c2b5a6f' into feat/pr-…
kyle-sexton Oct 20, 2026
2134e61
feat(pr-pipeline): hold fix-ci patches to apply-ci-fix's refusals in …
kyle-sexton Oct 20, 2026
a1b2e32
feat(pr-pipeline): stage a read skill for run-claude-cli under .lane/
kyle-sexton Oct 20, 2026
02a262e
feat(pr-pipeline): run fix-ci on run-claude-cli with a subscription f…
kyle-sexton Oct 20, 2026
dc2ee1b
docs(pr-pipeline): spell color in a fix-ci-context comment
kyle-sexton Oct 20, 2026
ea8d817
Merge origin/main into feat/pr-pipeline-fix-ci-run-claude-cli
kyle-sexton Oct 20, 2026
22c9ba4
docs(pr-pipeline): describe fix-ci's two calls on run-claude-cli and …
kyle-sexton Oct 20, 2026
72802a8
test(pr-pipeline): check report-escalation's needs are jobs and drop …
kyle-sexton Oct 20, 2026
59cbce9
fix(pr-pipeline): run fix-ci's patch under the ci-context row
kyle-sexton Oct 20, 2026
c470b95
fix(pr-pipeline): skip fix-ci's patch on a moved head and keep the st…
kyle-sexton Oct 20, 2026
ea4ff50
Merge remote-tracking branch 'origin/main' into feat/pr-pipeline-fix-…
kyle-sexton Oct 21, 2026
4a898e0
fix(pr-pipeline): scan fix-ci's outputs for credentials before any up…
kyle-sexton Oct 21, 2026
2d50d8c
feat(pr-pipeline): run fix-docs on run-claude-cli with an apply-patch…
kyle-sexton Oct 21, 2026
85d66aa
feat(pr-pipeline): let fix-docs read the web from the read runner
kyle-sexton Oct 21, 2026
c246a21
test(pr-pipeline): rename the resolver fixtures' fix-docs activity to…
kyle-sexton Oct 21, 2026
27f3a8a
fix(pr-pipeline): list leak-scan files from a scratch index and gate …
kyle-sexton Oct 21, 2026
c5f9a2f
feat(pr-pipeline): run fix-docs in auto mode, checked against the CLI…
kyle-sexton Oct 21, 2026
5b10ca4
Merge feat/pr-pipeline-fix-ci-run-claude-cli into feat/pr-pipeline-fi…
kyle-sexton Oct 21, 2026
67cd316
docs(pr-pipeline): show fix-docs handing on a patch in the example co…
kyle-sexton Oct 21, 2026
7fcc107
feat(pr-pipeline): refuse a write effect for fix-docs in activity-rul…
kyle-sexton Oct 21, 2026
762cb7a
fix(pr-pipeline): upload the verdict of a PR run that stopped before …
kyle-sexton Oct 21, 2026
cb1b660
Merge feat/pr-pipeline-fix-ci-run-claude-cli into feat/pr-pipeline-fi…
kyle-sexton Oct 21, 2026
03764c5
fix(pr-pipeline): escalate fix-ci's no-change patch as ci-fix-invalid…
kyle-sexton Oct 21, 2026
03d9be8
fix(pr-pipeline): block the leak scan on credential formats only
kyle-sexton Oct 21, 2026
8859f8e
Merge remote-tracking branch 'origin/main' into feat/pr-pipeline-fix-…
kyle-sexton Oct 21, 2026
055dc7a
fix(pr-pipeline): judge each line in the leak scan so an assignment h…
kyle-sexton Oct 21, 2026
8b839a7
Merge feat/pr-pipeline-fix-ci-run-claude-cli into feat/pr-pipeline-fi…
kyle-sexton Oct 21, 2026
afe225a
test(pr-pipeline): check the default row's push reasons against the w…
kyle-sexton Oct 21, 2026
5f89768
Merge origin/main into feat/pr-pipeline-fix-docs-run-claude-cli
kyle-sexton Oct 22, 2026
f58999b
fix(pr-pipeline): stage an empty .mcp.json in fix-docs's research plu…
kyle-sexton Oct 22, 2026
6eb551c
docs(pr-pipeline): say fix-docs's launcher branch adds no deny of its…
kyle-sexton Oct 22, 2026
44c3873
fix(pr-pipeline): keep fix-docs off the web
kyle-sexton Oct 22, 2026
63c51bc
Merge remote-tracking branch 'origin/main' into feat/pr-pipeline-fix-…
kyle-sexton Oct 22, 2026
8eb1bc2
Merge remote-tracking branch 'origin/main' into feat/pr-pipeline-fix-…
kyle-sexton Oct 22, 2026
e4f5161
Merge branch 'main' into feat/pr-pipeline-fix-docs-run-claude-cli
kyle-sexton Oct 22, 2026
1b3ebe7
chore(pr-pipeline): resync the pull request head after the base merge
kyle-sexton Oct 22, 2026
2466471
Merge remote-tracking branch 'origin/main' into feat/pr-pipeline-fix-…
kyle-sexton Oct 22, 2026
0944a8d
feat(pr-pipeline): run fix-docs in pr-unblock, dispatched once per PR…
kyle-sexton Oct 22, 2026
e89dc01
Merge remote-tracking branch 'origin/main' into feat/pr-pipeline-fix-…
kyle-sexton Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
bump: minor
---

### Added

- A skill activity's `permission-mode` (`dontAsk`, the default, or `auto`) sets the read
runner's launcher mode, through `resolve-config`'s new `permission-mode` output. A step after
each launcher leg fails the run unless the CLI's init message reports that mode. fix-docs sets
`auto`
([ADR 0049](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/adr/0049-run-ci-lanes-on-github-hosted-runners-under-trigger-and-token-hardening.md)).
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
bump: minor
---

### Changed

- fix-docs moves into the `pr-unblock` lane, so its check is `pr-unblock / fix-docs`.
`pr-unblock-dispatch.yml` dispatches it on each push to main, once per open, ready pull request
a person opened that changes a Markdown file, carries no `do-not-merge` or `needs-human` label
and never had a fix-docs run, at most `limits.fix-docs-per-run` (default 2) per run
([README, fix-docs](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/pr-pipeline/README.md)).
- fix-docs runs on the read runner with no write token, on ci-workflows' run-claude-cli and the
automations role's API key first. pr-unblock.yml calls the read runner twice, each call with one
Claude secret: `fix-docs` passes only `AUTOMATIONS_CLAUDE_API_KEY`, and `fix-docs-subscription`
passes only `AUTOMATIONS_CLAUDE_CODE_OAUTH_TOKEN` and runs only after a credit stop or with the
key empty. Every fix-docs job trusts the default branch tip
([pr-run-activity.md, Two calls on one credential each](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/pr-pipeline/pr-run-activity.md#two-calls-on-one-credential-each)).
- fix-docs's change leaves as a patch that the `apply-docs-patch` job pushes as one commit, only
when a patch was written. A refused patch posts the `pr-unblock / apply-patch` check neutral and
labels nothing
([pr-run-activity.md, prepare: apply-patch](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/pr-pipeline/pr-run-activity.md#prepare-apply-patch)).
- The `default` row of
[`patch-rules.json`](https://github.com/melodic-software/claude-code-plugins/blob/main/.github/actions/resolve-config/patch-rules.json)
refuses every agent instruction path, caps a patch at 300 files and takes a moved head with
`--3way` (`skip-moved-head: false`).
- The read runner's launcher takes a skill with no prepare mode that hands on a patch: Bash,
Edit, Write and Agent as tools, no Bash allow rule, and `Read(./.git/**)` denied. It fails red
on `checks`, `self-checks` and `verify-scripts`.
- fix-docs routes to the new `docs-fix` profile, a mechanical worker from the multi-agent role
map, which resolves to sonnet.
- The launcher accepts `web-access` (`domains: any` only), staging the research skills and their
plugins under `.lane/`. fix-docs leaves it off.

### Removed

- The `pr-refine` lane and `pr-refine.yml`, whose `pull_request` trigger ran the read runner's
scan job off main, where the `pr-pipeline-scan-carrier` environment refuses it.
48 changes: 45 additions & 3 deletions .github/actions/collect-base-activity/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ inputs:
the CI context, a scratch directory and the prompt file are staged in
$RUNNER_TEMP/lane-stage for the runner to copy to .lane/ after the head
checkout, and the tool rules name them there. It takes a read skill
only (ci-context included), never a gate or an issue lane.
only (ci-context included, and one with no prepare mode that hands on
a patch), never a gate, an issue lane or an activity with checks,
self-checks or verify-scripts.
required: false
default: claude-code-action

Expand Down Expand Up @@ -217,8 +219,11 @@ runs:
# preamble would not expand.
if [ "$LAUNCHER" = run-claude-cli ]; then
[ -z "${ISSUE_NUMBER:-}" ] || fail "run-claude-cli: an issue lane runs on claude-code-action"
[ "$edits" = read ] || fail "run-claude-cli: only a read skill, ci-context included, runs on the launcher"
[ "$edits" = read ] || { [ "$edits" = patch ] && [ -z "$PREPARE" ]; } ||
fail "run-claude-cli: only a read skill runs on the launcher, ci-context included, or one with no prepare mode that hands on a patch"
[ "$GATING" = advisory ] || fail "run-claude-cli: a gate skill's verdict is not read from the launcher"
jq -e '[.selected.checks, .selected["self-checks"], .selected["verify-scripts"]] | all(. == null or . == [])' "$RESOLVED" >/dev/null ||
fail "run-claude-cli: checks, self-checks and verify-scripts have no wrapper on the launcher"
[[ $WORKSPACE =~ ^/[A-Za-z0-9_./-]+$ && $STAGE =~ ^/[A-Za-z0-9_./-]+$ ]] ||
fail "the workspace or the stage directory has a name a rule cannot carry"
plugin=${SKILL%%:*}
Expand Down Expand Up @@ -252,6 +257,42 @@ runs:
denied+=("Edit(./.lane/ci/**)" "Write(./.lane/ci/**)")
preamble="$preamble Run every step the skill instructions name. Your scratchpad directory is $lane/scratch: put every temporary or state file there. The context file is $lane/ci/context.json. Leave every fix uncommitted in the working tree: the runner turns it into a patch that a later job pushes."
fi
# A skill that hands on a patch with no prepare mode (fix-docs) gets
# Bash for the scripts its own frontmatter grants, which alone run
# under dontAsk: no Bash rule here. The base deny rules keep the git
# directory unread.
plugin_dirs=(".lane/plugins/$plugin")
if [ "$edits" = patch ]; then
tools+=(Bash Edit Write Agent)
allowed+=(Edit Write Agent)
preamble="$preamble Run every step the skill instructions name. Your scratchpad directory is $lane/scratch: put every temporary or state file there. Leave every change uncommitted in the working tree: the runner turns it into a patch that a later job pushes."
# web-access `any` only: the research plugins load from .lane/
# too, read-only like the skill's own. The docs scripts are not
# staged, so the run reads pages with WebFetch.
if [ "$(jq -r '.selected["web-access"].enabled // false' "$RESOLVED")" = true ]; then
jq -e '.selected["web-access"].domains == "any"' "$RESOLVED" >/dev/null ||
fail "run-claude-cli: web-access takes domains: any"
for research in discovery knowledge; do
[ -d "$BASE_PATH/plugins/$research" ] || fail "web-access: the $research plugin is not in the base"
[ -d "$STAGE/plugins/$research" ] || cp -R "$BASE_PATH/plugins/$research" "$STAGE/plugins/"
# Each plugin dir is an --add-dir too, so it gets the empty
# server list the skill's own copy gets.
mcp=$STAGE/plugins/$research/.mcp.json
[ -e "$mcp" ] || [ -L "$mcp" ] || printf '{"mcpServers":{}}\n' >"$mcp"
[ "$research" = "$plugin" ] || plugin_dirs+=(".lane/plugins/$research")
done
[ -z "$(find "$STAGE/plugins" -type l -print -quit)" ] || fail "a research plugin holds a symbolic link"
tools+=(WebFetch WebSearch)
allowed+=("WebFetch(domain:*)" WebSearch "Skill(discovery:read-docs)" "Skill(discovery:research)" "Skill(knowledge:docpage-digest)" "Skill(knowledge:map-corpus)")
# A deny rule beats an allow rule.
kept=()
for rule in "${denied[@]}"; do
[[ $rule == WebFetch || $rule == WebSearch ]] || kept+=("$rule")
done
denied=("${kept[@]}")
preamble="$preamble This run may search the web and read any page with WebFetch; the docs scripts the research skills use are not available, so read each page with WebFetch, and cite each page you read."
fi
fi
skill_args=$(jq -r '.selected.args // ""' "$RESOLVED")
for target in "${targets[@]}"; do
skill_args="${skill_args:+$skill_args }$target"
Expand All @@ -269,7 +310,8 @@ runs:
printf '%s\n' "${allowed[@]}" "$delimiter"
echo "disallowed-tools<<$delimiter"
printf '%s\n' "${denied[@]}" "$delimiter"
echo "plugin-dirs=.lane/plugins/$plugin"
echo "plugin-dirs<<$delimiter"
printf '%s\n' "${plugin_dirs[@]}" "$delimiter"
echo "preamble=$preamble"
} >>"$GITHUB_OUTPUT"
exit 0
Expand Down
112 changes: 107 additions & 5 deletions .github/actions/collect-base-activity/collect-base-activity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1268,7 +1268,7 @@ function launch(overrides = {}) {
tools: outputOf(text, "tools"),
allowedTools: outputOf(text, "allowed-tools"),
deniedTools: outputOf(text, "disallowed-tools"),
pluginDirs: /^plugin-dirs=(.*)$/m.exec(text)?.[1] ?? "",
pluginDirs: outputOf(text, "plugin-dirs").join("\n"),
preamble: /^preamble=(.*)$/m.exec(text)?.[1] ?? "",
promptFile: (() => {
try {
Expand Down Expand Up @@ -1316,14 +1316,26 @@ test("run-claude-cli: a read skill without ci-context gets no Bash, Edit or Writ
assert.doesNotMatch(run.preamble, /context file/);
});

test("run-claude-cli refuses an issue lane, a writing skill, a patch-producing read skill and a gate before any output", () => {
const appliedBy = path.join(mkdtempSync(path.join(root, "launch-ab-")), "resolved.json");
writeFileSync(appliedBy, JSON.stringify({ selected: { args: "--ci", "applied-by": { patch: "apply-patch" } } }));
// A resolved file for the launcher tests: the activity's args plus `extra`.
function launchResolved(extra) {
const file = path.join(mkdtempSync(path.join(root, "launch-ab-")), "resolved.json");
writeFileSync(file, JSON.stringify({ selected: { args: "fix", ...extra } }));
return file;
}
const PATCH = { "applied-by": { patch: "apply-patch" } };

test("run-claude-cli refuses an issue lane, a writing skill, a patch-producing skill with a prepare mode, a gate and any check before any output", () => {
const appliedBy = launchResolved(PATCH);
for (const [overrides, message] of [
[{ ISSUE_NUMBER: "7" }, /an issue lane runs on claude-code-action/],
[{ EFFECT: "mutate-branch", CAN_COMMIT: "true" }, /only a read skill/],
[{ RESOLVED: appliedBy }, /only a read skill/],
[{ RESOLVED: appliedBy, PREPARE: "review-threads" }, /only a read skill/],
[{ RESOLVED: appliedBy, PREPARE: "dependency-review" }, /only a read skill/],
[{ RESOLVED: appliedBy, PREPARE: "merge-base" }, /only a read skill/],
[{ GATING: "gate" }, /a gate skill's verdict is not read from the launcher/],
[{ RESOLVED: launchResolved({ ...PATCH, checks: ["scripts/check-x.sh --check"] }) }, /checks, self-checks and verify-scripts have no wrapper/],
[{ RESOLVED: launchResolved({ ...PATCH, "self-checks": ["scripts/check-x.sh"] }) }, /checks, self-checks and verify-scripts have no wrapper/],
[{ RESOLVED: launchResolved({ ...PATCH, "verify-scripts": [VERIFY] }) }, /checks, self-checks and verify-scripts have no wrapper/],
]) {
const run = launch(overrides);
assert.notEqual(run.status, 0, JSON.stringify(overrides));
Expand Down Expand Up @@ -1359,3 +1371,93 @@ test("run-claude-cli: a skill plugin holding a symbolic link fails red", () => {
assert.match(run.stdout, /the skill's plugin holds a symbolic link/);
assert.deepEqual(run.tools, []);
});

// fix-docs: a read skill with no prepare mode that hands on a patch gets Bash
// for its own frontmatter grants (no Bash rule), Edit, Write and Agent, the
// git directory denied, and the instruction to leave its change uncommitted.
test("run-claude-cli: a patch-producing skill with no prepare mode gets Bash, Edit, Write and Agent, no Bash rule, and .git denied", () => {
const run = launch({ RESOLVED: launchResolved(PATCH), SKILL: "source-control:pull-request" });
assert.equal(run.status, 0, run.stdout);
assert.deepEqual(run.tools.toSorted(), ["Agent", "Bash", "Edit", "Glob", "Grep", "Read", "Skill", "Write"]);
assert.deepEqual(run.allowedTools.toSorted(), ["Agent", "Edit", "Glob", "Grep", "Skill(source-control:pull-request)", "Write"]);
for (const rule of ["WebFetch", "WebSearch", "Read(./.git/**)", "Read(//proc/**)", "Edit(./.lane/plugins/**)", "Write(./.lane/plugins/**)", "Edit(./.lane/trusted-context.json)", "Write(./.lane/trusted-context.json)"]) {
assert.ok(run.deniedTools.includes(rule), `${rule} in ${run.deniedTools.join(" | ")}`);
}
assert.equal(run.pluginDirs, ".lane/plugins/source-control");
assert.ok(statSync(path.join(run.stageDir, "plugins", "source-control")).isDirectory());
assert.ok(statSync(path.join(run.stageDir, "scratch")).isDirectory());
assert.ok(run.preamble.includes(`Your scratchpad directory is ${WORKSPACE}/.lane/scratch:`), run.preamble);
assert.ok(run.preamble.includes(UNCOMMITTED_SENTENCE), run.preamble);
assert.doesNotMatch(run.preamble, /context file/);
assert.equal(run.promptFile, "Call the Skill tool with skill source-control:pull-request and args fix, then follow the skill instructions to the end.\n");
assert.equal(run.args, "", "no claude_args for the launcher");
assert.deepEqual(run.plugins, []);
});

test("run-claude-cli: a ci-context skill that hands on a patch keeps the ci-context tool set and preamble", () => {
const ciDir = mkdtempSync(path.join(root, "ci-launch-ab-"));
writeFileSync(path.join(ciDir, "context.json"), "{}");
const run = launch({ RESOLVED: launchResolved(PATCH), PREPARE: "ci-context", CI_DIR: ciDir, SKILL: "source-control:pull-request" });
assert.equal(run.status, 0, run.stdout);
assert.ok(run.allowedTools.includes("Bash"), "ci-context keeps its own tool set");
assert.ok(!run.preamble.includes(UNCOMMITTED_SENTENCE), run.preamble);
});

// web-access on the launcher: `any` only, with the research plugins staged
// under .lane/ beside the skill's own and the web tools neither stripped by
// --restricted nor denied.
function webBase() {
const dir = mkdtempSync(path.join(root, "base-web-"));
for (const plugin of ["source-control", "discovery", "knowledge"]) mkdirSync(path.join(dir, "plugins", plugin), { recursive: true });
mkdirSync(path.join(dir, ".claude-plugin"));
writeFileSync(path.join(dir, ".claude-plugin", "marketplace.json"), JSON.stringify({ name: "melodic-software" }));
return dir;
}
const RESEARCH = ["Skill(discovery:read-docs)", "Skill(discovery:research)", "Skill(knowledge:docpage-digest)", "Skill(knowledge:map-corpus)"];

test("run-claude-cli: web-access any gives a patch skill WebFetch and WebSearch, the research skills and their plugins under .lane/", () => {
const run = launch({
BASE_PATH: webBase(),
RESOLVED: launchResolved({ ...PATCH, "web-access": { enabled: true, domains: "any" } }),
SKILL: "source-control:pull-request",
});
assert.equal(run.status, 0, run.stdout);
assert.ok(run.tools.includes("WebFetch") && run.tools.includes("WebSearch"), run.tools.join(" | "));
for (const rule of ["WebFetch(domain:*)", "WebSearch", ...RESEARCH]) {
assert.ok(run.allowedTools.includes(rule), `${rule} in ${run.allowedTools.join(" | ")}`);
}
assert.ok(!run.deniedTools.includes("WebFetch") && !run.deniedTools.includes("WebSearch"), run.deniedTools.join(" | "));
for (const rule of ["Read(./.git/**)", "Read(//proc/**)", "Edit(./.lane/plugins/**)", "Write(./.lane/plugins/**)"]) {
assert.ok(run.deniedTools.includes(rule), `${rule} in ${run.deniedTools.join(" | ")}`);
}
assert.equal(run.pluginDirs, ".lane/plugins/source-control\n.lane/plugins/discovery\n.lane/plugins/knowledge");
for (const plugin of ["discovery", "knowledge"]) {
assert.ok(statSync(path.join(run.stageDir, "plugins", plugin)).isDirectory(), plugin);
assert.equal(readFileSync(path.join(run.stageDir, "plugins", plugin, ".mcp.json"), "utf8"), '{"mcpServers":{}}\n', plugin);
}
assert.match(run.preamble, /cite each page you read/);
assert.ok(!run.allowedTools.some((rule) => rule.startsWith("Bash")), "no docs-script rule");
});

test("run-claude-cli: a patch skill without web-access gets no web tools and only its own plugin", () => {
const run = launch({ BASE_PATH: webBase(), RESOLVED: launchResolved(PATCH), SKILL: "source-control:pull-request" });
assert.equal(run.status, 0, run.stdout);
assert.ok(!run.tools.includes("WebFetch") && !run.tools.includes("WebSearch"), run.tools.join(" | "));
assert.ok(run.deniedTools.includes("WebFetch") && run.deniedTools.includes("WebSearch"), run.deniedTools.join(" | "));
assert.equal(run.pluginDirs, ".lane/plugins/source-control");
assert.doesNotMatch(run.preamble, /WebFetch/);
});

test("run-claude-cli: web-access with a domain list, or a research plugin missing from the base, fails red before any output", () => {
const noKnowledge = webBase();
rmSync(path.join(noKnowledge, "plugins", "knowledge"), { recursive: true });
for (const [overrides, message] of [
[{ BASE_PATH: webBase(), RESOLVED: launchResolved({ ...PATCH, "web-access": { enabled: true, domains: ["code.claude.com"] } }) }, /web-access takes domains: any/],
[{ BASE_PATH: noKnowledge, RESOLVED: launchResolved({ ...PATCH, "web-access": { enabled: true, domains: "any" } }) }, /the knowledge plugin is not in the base/],
]) {
const run = launch({ SKILL: "source-control:pull-request", ...overrides });
assert.notEqual(run.status, 0, JSON.stringify(overrides));
assert.match(run.stdout, message);
assert.deepEqual(run.tools, []);
}
});
6 changes: 6 additions & 0 deletions .github/actions/resolve-config/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ outputs:
applied-by-patch:
description: The activity that applies the selected activity's patch (its applied-by.patch); empty when it hands on none.
value: ${{ steps.resolve.outputs.applied-by-patch }}
permission-mode:
description: >
A skill activity's launcher permission mode, `dontAsk` (the default)
or `auto`, for run-claude-cli's `permission-mode` input; empty for a
script.
value: ${{ steps.resolve.outputs.permission-mode }}
issue-dimensions:
description: >
`<size>/<complexity>/<uncertainty>` as models.issue-dimensions reads
Expand Down
4 changes: 4 additions & 0 deletions .github/actions/resolve-config/activity-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"forbidden-effects": ["publish-artifact", "mutate-tracker", "merge"],
"never": "Posts to the pull request, merges, closes, or pushes under .github/"
},
"fix-docs": {
"forbidden-effects": ["publish-artifact", "mutate-tracker", "mutate-branch", "merge"],
"never": "Holds a write token: it runs a script on head files, and its change leaves only as a patch"
},
"fix-ci": {
"forbidden-effects": ["publish-artifact", "mutate-tracker", "mutate-branch", "merge"],
"never": "Holds a write token: it runs head code, and its change leaves only as a patch"
Expand Down
Loading
Loading