From d42e066d909e8213e96d89e5b9b3364f5529e931 Mon Sep 17 00:00:00 2001 From: bompus Date: Fri, 13 Nov 2026 16:33:49 -0700 Subject: [PATCH 1/2] stage-line: code spans are blanked before the trigger clause is found --- CHANGELOG.md | 1 + skills/hr-what-next/scripts/stage-line.mjs | 17 ++++++------ skills/hr-what-next/test/stage-line.test.mjs | 27 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 skills/hr-what-next/test/stage-line.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ccc422..77e112b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Notable changes to house-rules. Versions follow [semantic versioning](https://se ## Unreleased +- `hr-what-next`'s `scripts/stage-line.mjs` reads the `trigger:` clause of an "Approved, not built:" line outside Markdown code spans: a `;`, `recheck:` or `done-when:` quoted in a code span after the real `trigger:` no longer ends the clause early, and a `trigger:` quoted in a code span is not taken for the field, whatever precedes its backtick. - `hr-what-next`'s `check-open-work.mjs` judges each offer a "Approved, not built:" line names by that offer's own clause (asked, sent, relayed or awaiting a reply), not by words anywhere in the line, (clauses split at `;`, `,` and "and"; approved, selected or "not sent" closes it; "awaiting the user's reply" and "waiting for the user reply" open it; a `trigger:` clause is not read, but an offer written after it is), so a `trigger: pending local build` no longer makes an approved offer count as unanswered and three stage lines each awaiting a reply now give three unanswered offers; and an open stage line whose `trigger:` waits on another thread (an upstream, other or spelled-out "pull request 4", a maintainer) gets the same missing-`recheck:` warning as the line deferred (the spelled-out forms now count for deferred lines too). The stage-line reading moves to `scripts/stage-line.mjs`. - `hr-code-review`'s seat client now passes `MODEL_PROVIDER` to a Codex seat (codex-acp reads it to choose the provider for new sessions, and the narrowed environment dropped it; no other seat, and not `DEFAULT_AUTH_REQUEST` or `INITIAL_AGENT_MODE`, gets it), and `references/reviewer-role.md` says the parent leaves protected paths (secrets, `.env` files, credentials) out of a large diff it saves into the review root and lists what it left out. - `hr-read-reddit` and the README say that Reddit announced RSS support ends November 13, 2026, so feeds can fail, and tell readers to use the partial search fallback when one does (on the cutoff day search feeds answered HTTP 403 while a thread feed still answered); the README's dark banner and overview image URLs carry `?v=1`, per the `web-image-formats` rule (bump it in the commit that changes either image's bytes). diff --git a/skills/hr-what-next/scripts/stage-line.mjs b/skills/hr-what-next/scripts/stage-line.mjs index c1ab91e1..b691c91e 100644 --- a/skills/hr-what-next/scripts/stage-line.mjs +++ b/skills/hr-what-next/scripts/stage-line.mjs @@ -1,19 +1,20 @@ // Reading an "Approved, not built:" ledger line: its `trigger:` clause says what the stage waits on, and each // offer the rest of the line names is judged by its own clause. -// The real `trigger:` field (the last one not written as `trigger:` in backticks) and its clause, which ends at ";", -// `recheck:` or `done-when:`. Offers written after the clause stay part of the rest of the line. +// The real `trigger:` field (the last one outside a Markdown code span) and its clause, which ends at ";", `recheck:` +// or `done-when:` outside code. Offers written after the clause stay part of the rest of the line. /** @param {string} text @returns {{ trigger: string, rest: string }} */ export function splitStage(text) { - const starts = [...text.matchAll(/(? " ".repeat(span.length)); + const start = [...plain.matchAll(/\btrigger:/gi)].at(-1)?.index; if (start === undefined) { return { trigger: "", rest: text }; } - const after = text.slice(start + "trigger:".length); - const end = after.search(/;|\b(?:recheck|done-when):/i); - const cut = end < 0 ? after.length : end; - return { trigger: after.slice(0, cut), rest: text.slice(0, start) + after.slice(cut) }; + const from = start + "trigger:".length; + const end = plain.slice(from).search(/;|\b(?:recheck|done-when):/i); + const cut = end < 0 ? text.length : from + end; + return { trigger: text.slice(from, cut), rest: text.slice(0, start) + text.slice(cut) }; } // What the line says about an offer's reply: asked, sent or relayed, or a reply, answer or selection awaited. diff --git a/skills/hr-what-next/test/stage-line.test.mjs b/skills/hr-what-next/test/stage-line.test.mjs new file mode 100644 index 00000000..fcf329a8 --- /dev/null +++ b/skills/hr-what-next/test/stage-line.test.mjs @@ -0,0 +1,27 @@ +import { expect, test } from "bun:test"; +import { splitStage } from "../scripts/stage-line.mjs"; + +test("a semicolon, recheck: or done-when: inside a code span after the real trigger: stays in the trigger clause", () => { + for (const quoted of ["`a; b`", "`recheck: 2099-01-01`", "`done-when: pr-merged x/y#1`"]) { + const { trigger, rest } = splitStage( + `Approved, not built: export trigger: upstream release ${quoted} ships`, + ); + expect(trigger).toContain(quoted); + expect(trigger).toContain("ships"); + expect(rest).not.toContain("ships"); + } +}); + +test("the clause still ends at a real semicolon, recheck: or done-when: outside code", () => { + expect(splitStage("export trigger: PR 4 lands; asked in Offer X-1").trigger).toBe(" PR 4 lands"); + expect(splitStage("export trigger: PR 4 lands recheck: 2099-01-01").trigger).toBe(" PR 4 lands "); + expect(splitStage("export trigger: PR 4 lands done-when: pr-merged a/b#4").trigger).toBe( + " PR 4 lands ", + ); +}); + +test("a trigger: written inside a code span is not the field", () => { + const text = "export, the `trigger: x` field described trigger: PR 4 lands"; + expect(splitStage(text).trigger).toBe(" PR 4 lands"); + expect(splitStage("export, the `see trigger: x` field").trigger).toBe(""); +}); From 39f4ce288f8a309c6b7d5841cda0dac6c5b52d53 Mon Sep 17 00:00:00 2001 From: bompus Date: Fri, 13 Nov 2026 16:54:23 -0700 Subject: [PATCH 2/2] stage-line: mask code spans of any backtick run length with the plan parser's masking --- CHANGELOG.md | 2 +- skills/hr-what-next/scripts/plan-ledger.mjs | 2 +- skills/hr-what-next/scripts/stage-line.mjs | 4 +++- skills/hr-what-next/test/stage-line.test.mjs | 11 +++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e112b3..d001d512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Notable changes to house-rules. Versions follow [semantic versioning](https://se ## Unreleased -- `hr-what-next`'s `scripts/stage-line.mjs` reads the `trigger:` clause of an "Approved, not built:" line outside Markdown code spans: a `;`, `recheck:` or `done-when:` quoted in a code span after the real `trigger:` no longer ends the clause early, and a `trigger:` quoted in a code span is not taken for the field, whatever precedes its backtick. +- `hr-what-next`'s `scripts/stage-line.mjs` reads the `trigger:` clause of an "Approved, not built:" line outside Markdown code spans: a `;`, `recheck:` or `done-when:` quoted in a code span after the real `trigger:` no longer ends the clause early, and a `trigger:` quoted in a code span is not taken for the field, whatever precedes its backtick (spans of any backtick run length, using the same masking as the plan parser). - `hr-what-next`'s `check-open-work.mjs` judges each offer a "Approved, not built:" line names by that offer's own clause (asked, sent, relayed or awaiting a reply), not by words anywhere in the line, (clauses split at `;`, `,` and "and"; approved, selected or "not sent" closes it; "awaiting the user's reply" and "waiting for the user reply" open it; a `trigger:` clause is not read, but an offer written after it is), so a `trigger: pending local build` no longer makes an approved offer count as unanswered and three stage lines each awaiting a reply now give three unanswered offers; and an open stage line whose `trigger:` waits on another thread (an upstream, other or spelled-out "pull request 4", a maintainer) gets the same missing-`recheck:` warning as the line deferred (the spelled-out forms now count for deferred lines too). The stage-line reading moves to `scripts/stage-line.mjs`. - `hr-code-review`'s seat client now passes `MODEL_PROVIDER` to a Codex seat (codex-acp reads it to choose the provider for new sessions, and the narrowed environment dropped it; no other seat, and not `DEFAULT_AUTH_REQUEST` or `INITIAL_AGENT_MODE`, gets it), and `references/reviewer-role.md` says the parent leaves protected paths (secrets, `.env` files, credentials) out of a large diff it saves into the review root and lists what it left out. - `hr-read-reddit` and the README say that Reddit announced RSS support ends November 13, 2026, so feeds can fail, and tell readers to use the partial search fallback when one does (on the cutoff day search feeds answered HTTP 403 while a thread feed still answered); the README's dark banner and overview image URLs carry `?v=1`, per the `web-image-formats` rule (bump it in the commit that changes either image's bytes). diff --git a/skills/hr-what-next/scripts/plan-ledger.mjs b/skills/hr-what-next/scripts/plan-ledger.mjs index 740ef947..e7cc4b36 100644 --- a/skills/hr-what-next/scripts/plan-ledger.mjs +++ b/skills/hr-what-next/scripts/plan-ledger.mjs @@ -42,7 +42,7 @@ const GITHUB_REF = /^([\w.-]+\/[\w.-]+)#(\d+)$/; * at the next run of exactly the same length; a run with no closing run is literal text and is skipped whole. * @param {string} text */ -function maskCodeSpans(text) { +export function maskCodeSpans(text) { let out = ""; let at = 0; while (at < text.length) { diff --git a/skills/hr-what-next/scripts/stage-line.mjs b/skills/hr-what-next/scripts/stage-line.mjs index b691c91e..ca476dca 100644 --- a/skills/hr-what-next/scripts/stage-line.mjs +++ b/skills/hr-what-next/scripts/stage-line.mjs @@ -1,3 +1,5 @@ +import { maskCodeSpans } from "./plan-ledger.mjs"; + // Reading an "Approved, not built:" ledger line: its `trigger:` clause says what the stage waits on, and each // offer the rest of the line names is judged by its own clause. @@ -6,7 +8,7 @@ /** @param {string} text @returns {{ trigger: string, rest: string }} */ export function splitStage(text) { // Code spans are blanked, same length, so a field name or ";" quoted in code is not read as syntax. - const plain = text.replace(/`[^`]*`/g, (span) => " ".repeat(span.length)); + const plain = maskCodeSpans(text); const start = [...plain.matchAll(/\btrigger:/gi)].at(-1)?.index; if (start === undefined) { return { trigger: "", rest: text }; diff --git a/skills/hr-what-next/test/stage-line.test.mjs b/skills/hr-what-next/test/stage-line.test.mjs index fcf329a8..9c94187d 100644 --- a/skills/hr-what-next/test/stage-line.test.mjs +++ b/skills/hr-what-next/test/stage-line.test.mjs @@ -25,3 +25,14 @@ test("a trigger: written inside a code span is not the field", () => { expect(splitStage(text).trigger).toBe(" PR 4 lands"); expect(splitStage("export, the `see trigger: x` field").trigger).toBe(""); }); + +test("code spans of any backtick run length are masked, and an escaped backtick opens none", () => { + expect(splitStage("export, the ``trigger: x`` field").trigger).toBe(""); + const quoted = splitStage("Approved, not built: export trigger: ``a; b`` ships").trigger; + expect(quoted).toContain("ships"); + expect(quoted).toContain("``a; b``"); + expect(splitStage("export trigger: ``a; b`` ships; asked in Offer X-1").trigger).toBe( + " ``a; b`` ships", + ); + expect(splitStage("export \\`trigger: PR 4 lands").trigger).toBe(" PR 4 lands"); +});