Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions .claude/skills/issues/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,9 +64,12 @@ paragraph; put the smallest next action in **Detail / next action**.
open or resolved table as appropriate.
- IDs are monotonic and never reused — always allocate from the `issues:next-id` marker and bump it.
- Escape `|` inside cell text (write `\|`) so the markdown table stays intact.
- This file uses `merge=union` in `.gitattributes`. Never resolve a conflict by taking one side
wholesale — that drops the other agent's rows. `npm run check:outstanding-issues` fails on
duplicate IDs or a stale next-id marker.
- This file deliberately has **no** merge driver, so an overlapping edit conflicts loudly.
`merge=union` was tried and removed: it concatenated both sides silently, duplicating rows and
the `next-id` marker (`#133`). Never resolve a conflict by taking one side wholesale — that
drops the other agent's rows. Rebuild from `origin/main` and re-apply only the rows you
changed. `npm run check:outstanding-issues` fails on duplicate IDs, a stale next-id marker,
or a merge driver reappearing.
- Respect the repo's RAG/clinical/privacy flagging rules if an item _itself_ touches a protected
surface — recording it here is fine, but acting on it later still needs the usual gate.

Expand Down
14 changes: 10 additions & 4 deletions .gitattributes
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,13 @@
# but also reintroduced babysit twins that failed the ledger guard).
docs/branch-review-ledger.md merge=ledger

# Outstanding-issue rows are also append-mostly across concurrent agent sessions.
# Union merge preserves both sides' rows; scripts/check-outstanding-issues.mjs
# still fails on duplicate IDs because row-level union alone cannot allocate IDs.
docs/outstanding-issues.md merge=union
# Outstanding-issue rows deliberately get NO merge driver. Union was tried (PR
# #1416) and removed: ledger #133 recorded that it "concatenates conflicting
# hunks, so two sides each bumping the marker produce two `next-id` lines,
# corrupting the file silently where a conflict would fail loudly", and on
# 2026-07-30 (PR #1430) it duplicated the entire open-items table on four
# separate merges. Unlike docs/branch-review-ledger.md this file allocates IDs
# by read-modify-write, so concurrent appends need manual renumbering whatever
# the driver does — union bought nothing and hid the overlap. Default 3-way
# merge conflicts honestly instead; check-outstanding-issues.mjs enforces that
# no driver is set, and still gates IDs, the marker and row shape.
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -618,8 +618,11 @@ named PR). Future process only.
- Start from a fresh `origin/main` worktree/branch (`newtask`); do not pile new work onto a
stale head that already shares hot files with the open queue.
- Treat `docs/branch-review-ledger.md` and `docs/outstanding-issues.md` as hot shared files.
Both use `merge=union`. Append with `npm run ledger:append` / the `/issues` skill — never
hand-write ledger rows, and never resolve an outstanding-issues conflict by taking one side
`docs/branch-review-ledger.md` uses the custom `merge=ledger` driver (union with exact-row
dedupe); `docs/outstanding-issues.md` deliberately has **no** driver, so overlapping edits
conflict loudly rather than being silently concatenated — union was tried and removed
(`#133`). Append with `npm run ledger:append` / the `/issues` skill — never hand-write
ledger rows, and never resolve an outstanding-issues conflict by taking one side
wholesale. `npm run check:outstanding-issues` fails on duplicate IDs or a stale
`issues:next-id` marker.
- Before calling GitHub `DIRTY`/`CONFLICTING` a real conflict, run
Expand Down
2 changes: 1 addition & 1 deletion docs/outstanding-issues.md

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions docs/process-hardening.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,10 +86,12 @@ artifact before release; see
Future-process only — do not mutate unrelated active PRs unless explicitly asked.
- **Outstanding-issues concurrency (`#112`):** the structural gate landed in PR #1410
(`npm run check:outstanding-issues` in `verify:cheap` / CI `static-pr` — duplicate IDs,
both-tables, stale `issues:next-id`, malformed rows). PR #1416 adds `merge=union` in
`.gitattributes` and a runtime attribute check so concurrent appends keep both sides'
rows; union merge still cannot allocate unique IDs, so the structural gate remains
required.
both-tables, stale `issues:next-id`, malformed rows). PR #1416 added `merge=union` in
`.gitattributes`; it is now **removed** and the runtime attribute check inverted to require
no driver at all. Union could not allocate unique IDs either, and it concatenated
conflicting hunks instead of failing — two marker bumps became two `next-id` lines (`#133`),
and on 2026-07-30 it duplicated the whole open-items table on four merges (PR #1430).
Default 3-way merge conflicts loudly instead; the structural gate remains required.
- **Silent CI on conflicted PRs (`#116`):** when GitHub cannot build
`refs/pull/<n>/merge`, every `pull_request` workflow is skipped with no failing check.
`.github/workflows/pr-mergeability.yml` checks trusted `pull_request_target` events and
Expand Down
2 changes: 1 addition & 1 deletion docs/scripts-index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ migration has shipped (see `docs/maturity-backlog-workorders.md` L1).
| `dev-free-port.mjs`, `ensure-local-server.mjs` | Project-stable localhost port selection + background server ensure |
| `check-node-engine.cjs`, `install-git-hooks.mjs`, `guard-push.mjs`, `guard-next-build.mjs` | Install/preflight guards |
| `ci-change-scope.mjs`, `ci-triage.mjs`, `pr-policy.mjs`, `pr-mergeability.mjs` | CI change classification + PR policy + conflict signal (self-tested via `check:ci-scope`/`check:ci-triage`/`check:pr-policy`/`check:pr-mergeability`) |
| `check-outstanding-issues.mjs`, `check-pr-mergeability-workflow.mjs` | Outstanding-issues ID/marker/union guard + PR mergeability workflow contract |
| `check-outstanding-issues.mjs`, `check-pr-mergeability-workflow.mjs` | Outstanding-issues ID/marker/no-driver guard + PR mergeability workflow contract |
| `check-installed-lock-parity.mjs`, `phone-chrome-plan.mjs`, `verify-phone-chrome.mjs` | Lock-trust preflight plus change-scoped phone contracts, ownership journeys, and smart full-UI escalation |
| `final-merge-audit.mjs` | Fail-closed local merge-tree audit; explicit provider mode adds PR/check/thread/tree/deployment proof |
| `child-process-result.mjs`, `cli-utils.ts`, `productivity-core.mjs` | Shared helpers |
Expand Down
58 changes: 45 additions & 13 deletions scripts/check-outstanding-issues.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,11 +2,14 @@
// Structural gate for docs/outstanding-issues.md.
//
// Ledger #112. The `issues:next-id` marker is a plain HTML comment that every
// editor read-modify-writes with no lock. The file now has `merge=union` (PR
// #1416), which preserves concurrent row appends the same way as
// docs/branch-review-ledger.md, but union merge cannot allocate unique IDs —
// two agents can still collide, and a hurried conflict resolution can still
// take one side wholesale. On 2026-07-29 that happened three times in one hour
// editor read-modify-writes with no lock. A `merge=union` driver was tried (PR
// #1416) and removed: unlike docs/branch-review-ledger.md this file allocates
// IDs by read-modify-write, so union could not allocate unique IDs either, and
// it silently concatenated conflicting hunks — two marker bumps became two
// `next-id` lines (#133), and on 2026-07-30 the whole open-items table was
// duplicated on four merges (#1430). This gate now requires that NO driver is
// set, so overlapping edits conflict loudly. A hurried conflict resolution can
// still take one side wholesale. On 2026-07-29 that happened three times in one hour
// on a single PR, and nothing noticed: no gate read this file's structure at
// all. This structural gate is what makes those failures loud.
//
Expand DownExpand Up@@ -404,6 +407,36 @@ function selfTest() {
console.log("outstanding-issues self-test passed.");
}

/**
* The only acceptable state for this file's `merge` attribute.
*
* Git distinguishes three non-driver states, and they are NOT interchangeable
* (see gitattributes, "merge"): *Unspecified* — no pattern matches — is the
* documented default 3-way text merge, which is the contract here. *Unset*
* (`-merge`) instead takes the current branch's version and declares the merge
* conflicted, so every two-sided edit becomes a manual resolution — a different
* regression from a driver, but a regression all the same, and one a global or
* future attributes file could introduce while this gate stayed green. A named
* driver (`union`, `ledger`, …) is the case #133 removed.
*
* Exported so the distinction is unit-tested rather than only reasoned about.
*/
export function mergeAttributeProblem(mergeAttribute) {
if (mergeAttribute === "unspecified") return null;
if (mergeAttribute === "unset") {
return (
`${ISSUES_PATH} must leave \`merge\` unspecified (found \`-merge\`, i.e. Unset) — ` +
"Unset takes the current branch's version and declares a conflict instead of running the " +
"default 3-way merge, so drop the negated attribute rather than adding one (ledger #133)"
);
}
return (
`${ISSUES_PATH} must have NO merge driver (found merge=${mergeAttribute || "empty"}) — ` +
"remove it from .gitattributes so overlapping edits conflict loudly instead of " +
"silently concatenating both sides (ledger #133)"
);
}

function effectiveMergeAttribute() {
const output = execFileSync("git", ["check-attr", "merge", "--", ISSUES_PATH], {
encoding: "utf8",
Expand All@@ -419,13 +452,12 @@ function main() {
}
const markdown = readFileSync(ISSUES_PATH, "utf8");
const problems = checkIssues(markdown);
const mergeAttribute = effectiveMergeAttribute();
if (mergeAttribute !== "union") {
problems.push(
`${ISSUES_PATH} must resolve to merge=union (found ${JSON.stringify(mergeAttribute || "unset")}) — ` +
"set it in .gitattributes so concurrent appends keep both sides' rows",
);
}
// A merge driver on this file is a regression, not an improvement: union
// concatenated conflicting hunks and duplicated the whole table rather than
// failing (#133, and four times on PR #1430). Honest conflicts are the
// contract; ids still need manual renumbering either way.
const mergeProblem = mergeAttributeProblem(effectiveMergeAttribute());
if (mergeProblem) problems.push(mergeProblem);
if (problems.length > 0) {
console.error(`${ISSUES_PATH} check FAILED:`);
for (const problem of problems) console.error(` - ${problem}`);
Expand All@@ -439,7 +471,7 @@ function main() {
const open = rows.filter((row) => row.table === "open").length;
console.log(
`Outstanding-issues guard passed: ${rows.length} rows (${open} open, ${rows.length - open} archived), ` +
`unique ids, next-id=${nextId} above the highest, union merge active.`,
`unique ids, next-id=${nextId} above the highest, no merge driver.`,
);
}

Expand Down
28 changes: 28 additions & 0 deletions tests/repo-hygiene.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ import {
sanitizeCell,
} from "../scripts/branch-review-ledger.mjs";
import { validateLedger } from "../scripts/check-branch-review-ledger.mjs";
import { mergeAttributeProblem } from "../scripts/check-outstanding-issues.mjs";

describe("check-env-parity name parsing", () => {
it("extracts UPPER_SNAKE schema keys from env.ts-style text", () => {
Expand DownExpand Up@@ -548,3 +549,30 @@ describe("branch-review-ledger guard", () => {
expect(validateLedger({ ...valid, ledger: legacy }).failures).toEqual([]);
});
});

describe("outstanding-issues merge attribute", () => {
// Ledger #133 removed `merge=union` so overlapping edits conflict loudly rather
// than being silently concatenated. Git's three non-driver states are not
// interchangeable, and only one of them is that contract.
it("accepts an unspecified attribute, the documented default 3-way merge", () => {
expect(mergeAttributeProblem("unspecified")).toBeNull();
});

it("rejects `-merge`, which conflicts every two-sided edit instead of merging", () => {
// Unset is not Unspecified: it takes the current branch's version and declares
// a conflict. `git check-attr` reports it as "unset", and an earlier revision of
// this guard accepted that string while printing "no merge driver".
expect(mergeAttributeProblem("unset")).toMatch(/must leave `merge` unspecified/);
});

it("rejects a named driver, including the one #133 removed", () => {
expect(mergeAttributeProblem("union")).toMatch(/must have NO merge driver/);
expect(mergeAttributeProblem("ledger")).toMatch(/must have NO merge driver/);
});

it("rejects an empty reading rather than treating it as absence", () => {
// An empty string means check-attr output did not parse — silently accepting it
// would make the whole check vacuous.
expect(mergeAttributeProblem("")).toMatch(/must have NO merge driver/);
});
});
Loading