Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c66cf7f
feat(pr-pipeline): restore pr-refine as the refine stage for fix-docs
kyle-sexton Oct 23, 2026
1bab702
Merge remote-tracking branch 'origin/main' into feat/pr-refine-restore
kyle-sexton Oct 23, 2026
1e92c84
feat(harness-ops): list pr-refine.yml as a morning-brief lane again
kyle-sexton Oct 23, 2026
7fa85ad
Merge remote-tracking branch 'origin/main' into feat/pr-refine-restore
kyle-sexton Oct 23, 2026
ad8fac8
Merge branch 'main' into feat/pr-refine-restore
kyle-sexton Oct 23, 2026
f9c82ae
Merge remote-tracking branch 'origin/main' into feat/pr-refine-restore
kyle-sexton Oct 23, 2026
808cdc5
docs(loop-lane): list pr-refine in the posting-lanes record
kyle-sexton Oct 23, 2026
38a39ad
fix(pr-pipeline): leave dispatched pr-refine runs out of the pr-merge…
kyle-sexton Oct 23, 2026
bc2c2d3
Merge remote-tracking branch 'origin/main' into feat/pr-refine-restore
kyle-sexton Oct 23, 2026
39af152
fix(pr-pipeline): start fix-docs-subscription on a jobs-API handoff read
kyle-sexton Oct 23, 2026
1f96c11
Merge remote-tracking branch 'origin/main' into feat/pr-refine-restore
kyle-sexton Oct 23, 2026
383e517
Merge remote-tracking branch 'origin/main' into feat/pr-refine-restore
kyle-sexton Oct 23, 2026
a6e77b3
fix(pr-pipeline): label fix-docs-subscription fallback: handoff
kyle-sexton Oct 23, 2026
80fea35
fix(pr-pipeline): take apply-docs-patch inputs from the scan job
kyle-sexton Oct 23, 2026
24bfb02
Merge remote-tracking branch 'origin/main' into feat/pr-refine-restore
kyle-sexton Oct 23, 2026
f0f6070
Merge remote-tracking branch 'origin/main' into feat/pr-refine-restore
kyle-sexton Oct 23, 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
7 changes: 7 additions & 0 deletions .changes/harness-ops/feat-pr-refine-restore-db899809.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Changed

- `/harness-ops:morning-brief` lists `pr-refine.yml` among its default lane workflows again, now that the pr-pipeline `pr-refine` lane runs `fix-docs`.
14 changes: 14 additions & 0 deletions .changes/pr-pipeline/feat-pr-refine-restore-9a0d0a8e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
bump: minor
---

### Changed

- `fix-docs` moves from `pr-unblock` back into its own `pr-refine` lane (stage `refine`): `pr-refine.yml` holds the `fix-docs`, `fix-docs-subscription` and `apply-docs-patch` jobs, and `pr-unblock.yml` no longer takes `fix-docs`. `pr-unblock-dispatch.yml` dispatches `pr-refine.yml`, still once per PR and at most `limits.fix-docs-per-run` per run; a PR that already has a `pr-unblock / fix-docs` run counts as done.
- `fix-docs` now tidies every ready PR that changes Markdown, lanes App PRs included, except Dependabot's, the release App's and the standards-sync App's, matched by account id.
- Both activity runners name `pr-refine.yml` as the dispatched workflow for the `pr-refine` lane. pr-merge's refire of a broker `workflow-modified` refusal leaves `pr-refine` runs out: a `pr-refine` run is dispatched on main, so an empty commit on the PR cannot make it pass, and a listed person re-dispatches it.

### Security

- `fix-docs-subscription` no longer starts on `fix-docs` outputs, which its head-code run job could forge. A `fix-docs-handoff` job reads this run's jobs through the Actions API, as `fix-ci-handoff` does for fix-ci. The fallback runs only when the broker allowed `fix-docs` and its report job posted no check (#8890).
- `apply-docs-patch` no longer takes its patch inputs or head from `fix-docs` run-job outputs. It takes them from the answering leg's scan job and the head from `fix-docs-head`, as fix-ci's `apply-patch` does (#8890).
160 changes: 127 additions & 33 deletions .github/actions/resolve-config/callers.test.mjs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .github/actions/resolve-config/dispatch-limits.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const ciFixPull = (number) => {
};
};

// Ready PRs a person opened that change Markdown and never had fix-docs, as
// Ready PRs that change Markdown and never had fix-docs, as
// fix-docs-dispatch.mjs lists them.
const docsPull = (number) => {
const head = String(number).padStart(40, "d");
Expand All @@ -70,7 +70,7 @@ const docsPull = (number) => {
isCrossRepository: false,
baseRefName: "main",
headRefOid: head,
author: { __typename: "User", login: "someone" },
author: { __typename: "User", login: "someone", databaseId: 4242 },
labels: { nodes: [] },
files: { nodes: [{ path: "README.md" }], pageInfo: { hasNextPage: false } },
commits: { nodes: [{ commit: { oid: head, statusCheckRollup: { contexts: { nodes: [] } } } }] },
Expand Down
39 changes: 22 additions & 17 deletions .github/actions/resolve-config/live-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ test("live config: report-escalation applies and is granted publish-artifact onl
});

// Expected values from #7032's acceptance criteria and effect-grants.json.
test("live config: pr-refine is the refine stage: fix-docs, then its push job", () => {
const { lane } = resolve(FILES, { lane: "pr-refine", activity: "fix-docs", configPath: CONFIG_PATH, basePath: ".base", facts: FACTS });
assert.equal(lane.stage, "refine");
assert.deepEqual(lane.slots.map((slot) => slot.activity ?? slot.name), ["fix-docs", "apply-patch"]);
assert.deepEqual(selected("pr-unblock", "apply-patch").needs, ["fix-ci"]);
});

test("live config: pr-unblock runs its slots in order", () => {
const { lane } = resolve(FILES, {
lane: "pr-unblock",
Expand All @@ -129,7 +136,6 @@ test("live config: pr-unblock runs its slots in order", () => {
"apply-dependency-patch",
"apply-dependency-verdict",
"fix-ci",
"fix-docs",
"apply-patch",
"report-escalation",
],
Expand Down Expand Up @@ -190,7 +196,7 @@ test("live config: every other PR activity still waits on needs-human", () => {
for (const [lane, activity, facts] of [
["pr-unblock", "resolve-conflicts", {}],
["pr-unblock", "review-dependency", { mergeable: "clean" }],
["pr-unblock", "fix-docs", {}],
["pr-refine", "fix-docs", {}],
]) {
assert.equal(skipReason(lane, activity, { ...facts, labels: ["needs-human"] }), "awaiting-human", activity);
}
Expand All @@ -210,8 +216,7 @@ test("live config: publish-feedback runs the reply script after address-feedback
);
});

// fix-docs hands no PR to a person, so report-escalation does not follow it.
test("live config: report-escalation follows every other pr-unblock activity but fix-docs", () => {
test("live config: report-escalation follows every pr-unblock activity", () => {
assert.deepEqual(selected("pr-unblock", "report-escalation").needs, [
"resolve-conflicts",
"address-feedback",
Expand Down Expand Up @@ -359,7 +364,7 @@ test("live config: resolve-issue reads official docs hosts only; review-dependen
enabled: true,
domains: ["github.com", "raw.githubusercontent.com", "www.npmjs.com", "registry.npmjs.org", "pypi.org"],
});
assert.notEqual(selected("pr-unblock", "fix-docs")["web-access"]?.enabled, true);
assert.notEqual(selected("pr-refine", "fix-docs")["web-access"]?.enabled, true);
const others = Object.entries(doc.activities).filter(
([name, a]) => !["resolve-issue", "review-dependency"].includes(name) && a["web-access"]?.enabled,
);
Expand Down Expand Up @@ -445,31 +450,31 @@ test("live config: label-unblocked may only write issues", () => {
});

test("live config: fix-docs applies to a Markdown change and waits on needs-human", () => {
assert.equal(selected("pr-unblock", "fix-docs").applies, true);
assert.equal(selected("pr-refine", "fix-docs").applies, true);
assert.equal(
skipReason("pr-unblock", "fix-docs", { labels: ["needs-human"] }),
skipReason("pr-refine", "fix-docs", { labels: ["needs-human"] }),
"awaiting-human",
);
});

test("live config: fix-docs does not apply to a draft PR", () => {
assert.equal(skipReason("pr-unblock", "fix-docs", { draft: true }), "not-applicable");
assert.equal(skipReason("pr-refine", "fix-docs", { draft: true }), "not-applicable");
});

// The model run holds no write token; only the script that pushes its patch
// may write the branch, from its own job in pr-unblock.
test("live config: fix-docs holds no write token and its push job in pr-unblock may only write its branch", () => {
const grant = (activity) => resolveGrant(FILES, { lane: "pr-unblock", activity, configPath: CONFIG_PATH });
// may write the branch, from its own job in pr-refine.
test("live config: fix-docs holds no write token and its push job in pr-refine may only write its branch", () => {
const grant = (activity) => resolveGrant(FILES, { lane: "pr-refine", activity, configPath: CONFIG_PATH });
assert.deepEqual(grant("fix-docs"), { effect: "read", grant: { contents: "read", "pull-requests": "read", issues: "read" } });
assert.deepEqual(grant("apply-patch"), {
effect: "mutate-branch",
grant: { contents: "write", "pull-requests": "read", issues: "read" },
});
const fix = selected("pr-unblock", "fix-docs");
const fix = selected("pr-refine", "fix-docs");
assert.equal(fix.prepare, undefined, "no prepare mode: apply-patch takes the default row");
assert.deepEqual(fix["applied-by"], { patch: "apply-patch" });
const apply = selected("pr-unblock", "apply-patch");
assert.deepEqual(apply.needs, ["fix-ci", "fix-docs"]);
const apply = selected("pr-refine", "apply-patch");
assert.deepEqual(apply.needs, ["fix-docs"]);
assert.equal(apply.script, ".github/scripts/apply-patch/apply-patch.mjs");
assert.equal(apply.prepare, "apply-patch");
});
Expand All @@ -478,15 +483,15 @@ test("live config: fix-docs holds no write token and its push job in pr-unblock
// auto, so no activity asks for it; fix-docs takes the dontAsk default and
// runs only what the stage's allow rules name.
test("live config: no activity sets permission-mode", () => {
assert.equal(selected("pr-unblock", "fix-docs")["permission-mode"], undefined);
assert.equal(selected("pr-refine", "fix-docs")["permission-mode"], undefined);
const { activities } = parse(FILES.config);
assert.deepEqual(Object.keys(activities).filter((name) => Object.hasOwn(activities[name], "permission-mode")), []);
});

// fix-docs hands on a patch, so its targets leave out instruction surfaces
// and CODEOWNERS paths, as a write activity's do.
test("live config: fix-docs is not asked to fix an instruction surface", () => {
const slot = selected("pr-unblock", "fix-docs", { changedTargets: ["docs/guide.md", "CLAUDE.md", "plugins/p/skills/s/SKILL.md"] });
const slot = selected("pr-refine", "fix-docs", { changedTargets: ["docs/guide.md", "CLAUDE.md", "plugins/p/skills/s/SKILL.md"] });
assert.deepEqual(slot["changed-paths"], ["docs/guide.md"]);
});

Expand All @@ -496,7 +501,7 @@ test("live config: fix-docs with a write effect is refused", () => {
const writes = FILES.config.replace("effect: read\n applied-by:\n patch: apply-patch\n", "effect: mutate-branch\n");
assert.notEqual(writes, FILES.config);
assert.throws(
() => resolve({ ...FILES, config: writes }, { lane: "pr-unblock", activity: "fix-docs", configPath: CONFIG_PATH, basePath: ".base", facts: FACTS }),
() => resolve({ ...FILES, config: writes }, { lane: "pr-refine", activity: "fix-docs", configPath: CONFIG_PATH, basePath: ".base", facts: FACTS }),
(error) => error.code === "forbidden-by-activity",
);
});
Expand Down
4 changes: 2 additions & 2 deletions .github/actions/resolve-config/models.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ test("live config: the live issue-dimensions maps every org option and runs a kn
});

test("live config: an activity with no issue input takes no dimensions", () => {
const slot = liveSlot("pr-unblock", "fix-docs");
const slot = liveSlot("pr-refine", "fix-docs");
assert.equal(slot["issue-dimensions"], undefined);
assert.equal(slot["timeout-minutes"], undefined);
});
Expand Down Expand Up @@ -495,7 +495,7 @@ for (const activity of ["fix-ci", "address-feedback"]) {
// The role map's worker runs mechanical work on sonnet at its own effort,
// medium; the profile keeps standard's 75 turns.
test("live config: fix-docs runs as a mechanical worker, sonnet at the worker's effort, 75 turns", () => {
const slot = liveSlot("pr-unblock", "fix-docs");
const slot = liveSlot("pr-refine", "fix-docs");
assert.deepEqual(slot.models, {
profile: "docs-fix",
role: "worker",
Expand Down
4 changes: 2 additions & 2 deletions .github/lane-funding.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
{ "id": "fix-ci-subscription", "workflow": "pr-unblock.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_CODE_OAUTH_TOKEN"] },
{ "id": "fix-ci-rung-up", "workflow": "pr-unblock.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_API_KEY"] },
{ "id": "fix-ci-rung-up-subscription", "workflow": "pr-unblock.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_CODE_OAUTH_TOKEN"] },
{ "id": "fix-docs", "workflow": "pr-unblock.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_API_KEY"] },
{ "id": "fix-docs-subscription", "workflow": "pr-unblock.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_CODE_OAUTH_TOKEN"] },
{ "id": "fix-docs", "workflow": "pr-refine.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_API_KEY"] },
{ "id": "fix-docs-subscription", "workflow": "pr-refine.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_CODE_OAUTH_TOKEN"] },
{ "id": "address-feedback", "workflow": "pr-unblock.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_API_KEY"] },
{ "id": "address-feedback-subscription", "workflow": "pr-unblock.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_CODE_OAUTH_TOKEN"] },
{ "id": "review-dependency", "workflow": "pr-unblock.yml", "launcher": "run-claude-cli", "triggers": ["workflow_dispatch"], "currentSecrets": ["AUTOMATIONS_CLAUDE_API_KEY"] },
Expand Down
10 changes: 5 additions & 5 deletions .github/scripts/apply-patch/apply-patch.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ for (const [path, edit] of [
writeFileSync(join(repo, "README.md"), "# fixed\n");
edit(repo);
});
assert.equal((await pushWith(fix, fakeApi(fix.repo, fix.head), { lane: "pr-unblock", activity: "fix-docs" })).outcome, "pushed");
assert.equal((await pushWith(fix, fakeApi(fix.repo, fix.head), { lane: "pr-refine", activity: "fix-docs" })).outcome, "pushed");
});
}

Expand All @@ -341,7 +341,7 @@ test("a fix-docs patch that edits docs/CODEOWNERS is refused patch-protected-pat
});
const api = fakeApi(fix.repo, fix.head);
const lines = [];
assert.deepEqual(await pushWith(fix, api, { lane: "pr-unblock", activity: "fix-docs", log: (line) => lines.push(line) }), {
assert.deepEqual(await pushWith(fix, api, { lane: "pr-refine", activity: "fix-docs", log: (line) => lines.push(line) }), {
outcome: "escalate",
escalationReason: "patch-protected-path",
});
Expand All @@ -364,12 +364,12 @@ test("a fix-docs patch of 300 files pushes, and one of 301 escalates patch-inval
assert.deepEqual(refused.calls, []);
});

test("a fix-docs patch commits Apply fix-docs on the head with the Lane-Activity: pr-unblock/fix-docs trailer", async (t) => {
test("a fix-docs patch commits Apply fix-docs on the head with the Lane-Activity: pr-refine/fix-docs trailer", async (t) => {
const fix = repoWithPatch(t, (repo) => writeFileSync(join(repo, "README.md"), "# fixed\n"));
const api = fakeApi(fix.repo, fix.head);
assert.deepEqual(await pushWith(fix, api, { lane: "pr-unblock", activity: "fix-docs" }), { outcome: "pushed", commit: "c".repeat(40), branch: "feat/x" });
assert.deepEqual(await pushWith(fix, api, { lane: "pr-refine", activity: "fix-docs" }), { outcome: "pushed", commit: "c".repeat(40), branch: "feat/x" });
const { createCommit } = api.calls.find((call) => call.createCommit);
assert.equal(createCommit.message, "Apply fix-docs on feat/x\n\nLane-Activity: pr-unblock/fix-docs\n");
assert.equal(createCommit.message, "Apply fix-docs on feat/x\n\nLane-Activity: pr-refine/fix-docs\n");
assert.deepEqual(createCommit.parents, [fix.head]);
});

Expand Down
9 changes: 5 additions & 4 deletions .github/scripts/pr-unblock/dispatch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
// (dependency-dispatch.mjs), fix-ci for lanes App PRs and, with an
// `adoption` section, PRs of the base's trusted actors, whose ci-status or
// pr-test-windows run failed past the lost-run reconciler's re-run
// (fix-ci-dispatch.mjs) and fix-docs once per ready PR that changes
// Markdown (fix-docs-dispatch.mjs), and their run ids join the same record.
// (fix-ci-dispatch.mjs) and pr-refine.yml's fix-docs once per ready PR that
// changes Markdown (fix-docs-dispatch.mjs), and their run ids join the same
// record.
// resolve-conflicts and review-dependency read the one open-PR listing. A head whose check runs handle it for
// good is kept in DISPATCH_RECORD (dispatch-record.mjs), which the workflow
// carries to the next run through the Actions cache; the next run skips that
Expand Down Expand Up @@ -280,8 +281,8 @@ export function createGitHubApi({
const data = await graphql(DOCS_FIX_QUERY, { owner, name, base, after: after ?? null });
return { rateLimit: data.rateLimit, pullRequests: data.repository.pullRequests };
},
async readDocsFixHistory({ number, appId, checkName }) {
const data = await graphql(DOCS_FIX_HISTORY_QUERY, { owner, name, number, appId, checkName });
async readDocsFixHistory({ number, appId, checkName, earlierCheckName }) {
const data = await graphql(DOCS_FIX_HISTORY_QUERY, { owner, name, number, appId, checkName, earlierCheckName });
return { rateLimit: data.rateLimit, pullRequest: data.repository.pullRequest };
},
async getPulls(numbers) {
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/pr-unblock/dispatch.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ const docsPull = (number) => {
isCrossRepository: false,
baseRefName: "main",
headRefOid: head,
author: { __typename: "User", login: "someone" },
author: { __typename: "User", login: "someone", databaseId: 4242 },
labels: { nodes: [] },
files: { nodes: [{ path: "README.md" }], pageInfo: { hasNextPage: false } },
commits: { nodes: [{ commit: { oid: head, statusCheckRollup: { contexts: { nodes: [] } } } }] },
Expand Down
Loading
Loading