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
14 changes: 14 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Append-only board rows and notes. Every feature branch adds a row to the same table tail
# or the same prepend point, so two parallel branches collide on lines neither one got
# wrong. Union keeps both sides. Without this file the collision surfaced as a stash-pop
# conflict on _meta/BACKLOG.md that a session hand-resolved four times in one sitting.
#
# Caveat, deliberate: union never drops a line, so a row both sides EDITED (two branches
# flipping the same ID's Status) comes out TWICE rather than merged, and only a union re-merge
# runs `lib/board/backlog.sh dedupe-all` on its own. After a plain merge or rebase the next
# `board set <ID> ...` refuses with "matches N rows; dedupe first", which is the loud failure
# this trades the conflict for. Run `dedupe-all` then, and check which side's Status it kept.
# A table whose rows are REWRITTEN in place rather than appended does not belong on this list.
_meta/BACKLOG.md merge=union
_meta/backlog-staging.md merge=union
docs/implementation-notes/*.md merge=union
1 change: 1 addition & 0 deletions _meta/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Lane = the WORKFLOW.md risk tier (`tiny` / `normal` / `full`).

| ID | Title | Source | Target artifact | Lane | Status |
|----|-------|--------|-----------------|------|--------|
| ID-886 | declare the kit's append-only logs merge=union #wrap #git | the repo shipped every union mechanism and no .gitattributes of its own, so _meta/BACKLOG.md was never declared union here and parallel sessions hand-resolved the same stash-pop conflict four times; precedent: lib/wrap/wrap.sh pull_past_dirty path; lane=normal; source: wrap step 7b of the 2026-09-16 kit sweep session | shipped [https://github.com/dwarvesf/dwarves-kit/pull/663] |
| ID-904 | spec-next mints against open PR heads, not only the local listing #spec #concurrency | Intent: three parallel workers on 2026-09-16 each got SPEC-289 from spec-next because it only scanned docs/specs/, local branches, and commit subjects, none of which show a number an open unmerged PR already holds, and none of them called reserve. Added a gh-backed scan of every open PR head's docs/specs/ listing (contents API, no clone), skippable via SPEC_NEXT_NO_PR_SCAN=1, degrading to the old local-only scan with a stderr note when gh is missing or unauthenticated. lane=normal. Source: wrap step 7b, 2026-09-16 kit sweep session. | shipped [PR #661] |
| ID-903 | session observe tools: an --errors <tool> flag that groups a tool's error results by message prefix #observe #session #u-mid | Intent: session observe tools shows a tool's error COUNT (EnterWorktree 15 percent, ExitWorktree 17 percent this week) and nothing about WHY; a 30-line python over ~/.claude/projects grouping is_error tool_result content by its first 160 chars answered it in one run (17 of 19 EnterWorktree errors were subagents with a cwd override). Precedent: lib/session/observe/bin/session-observe owns the tools view; this is a flag on it, not a sibling script. lane=full. Goal draft: .claude/goals/observe-tool-errors.md. Source: wrap step 7b, 2026-09-16 kit sweep session. | queued |
| ID-885 | dispatch Step 6 releases a settled task's attempt record, and the lane parsers read a markdown-bold `Lane:` header #kit #dispatch #gate | Source: this session's follow-ups from SPEC-290 and SPEC-289. SPEC-290 wired most attempt-state verbs into commands/dispatch.md but never `release`, so kit-attempts/ grew forever. A worker writing `**Lane**: full` had its push BLOCKED with "Spec has no 'Lane:' header"; three files parsed `^Lane:` with the same expression, so all three had to change together. | commands/dispatch.md, hooks/ship-gate.sh, commands/ship.md, commands/mega.md, commands/spec.md, tests/test-ship-gate-fail-closed.sh, tests/test-meta.sh | full | shipped feat/attempt-release-lane-header [SPEC-293, proof docs/verification/attempt-release-lane-header.md] |
Expand Down
2 changes: 1 addition & 1 deletion commands/wrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Re-run the step 0 check first. Then, in this order: remove the session's own wor
- Pass `--worktrees` on every call, dry run and apply alike, unless `wrap.tidy_worktrees` is false. It is the flag the operator asks for by saying "clean up worktrees", and `apply` refuses a dirty, detached, checked-out or unproven worktree on its own, so the flag is not the safety. Omitting it also strands every branch a worktree holds: `apply` skips those with `held by a worktree` and the branch survives with it. With the knob false, drop the flag, leave every worktree, list them under `Left alone`, and name the knob in `FYI`; the session's own worktree bullet below is unaffected, because that removal is proven per worktree rather than swept.
- `bin/wrap apply` without `--apply` changes nothing; always read its dry-run SKIP lines before adding `--apply`.
- Pull on the default branch is `--ff-only`; off the default branch, `apply` fetches the default branch into itself instead and reports a refusal as `FAILED`, never forced. That fetch refuses outright when the default branch is checked out in another worktree, which is the second way a repo stays behind; the main-checkout rule above is what avoids both.
- `wrap.pull_past_dirty` (default `false`) governs the third way a repo stays behind: a sibling session's dirty TRACKED file that git refuses to overwrite. `false` reports `FAILED` and leaves the checkout behind. `true` stashes only the blocking files under a run-named stash, pulls, and pops that stash by ref; a pop conflict prints `PULLED, POP CONFLICT: <files>, stash <name> kept` and exits 2, leaving the markers and the stash for the operator. Untracked files, a dirty index, and pre-existing stashes are out of its reach at either setting. Name the knob in `FYI` whenever it changed what the pull did. If a run is interrupted between the stash and the pop, the blocking files are still in the stash: `git -C <repo> stash list | grep wrap-pull-past-dirty` names it and `git stash pop <ref>` finishes the restore.
- `wrap.pull_past_dirty` (default `false`) governs the third way a repo stays behind: a sibling session's dirty TRACKED file that git refuses to overwrite. `false` reports `FAILED` and leaves the checkout behind. `true` stashes only the blocking files under a run-named stash, pulls, and pops that stash by ref; a pop conflict prints `PULLED, POP CONFLICT: <files>, stash <name> kept` and exits 2, leaving the markers and the stash for the operator. Whether a dirty file reaches that conflict at all is the repo's own call: a file `.gitattributes` declares `merge=union` never blocks the pull in the first place (its local lines are carried across, below the `---` anchor, or by git's union driver when the file has no anchor), and one that rides in the stash resolves during the pop. This repo declares its board and its implementation notes union for exactly that reason. Untracked files, a dirty index, and pre-existing stashes are out of its reach at either setting. Name the knob in `FYI` whenever it changed what the pull did. If a run is interrupted between the stash and the pop, the blocking files are still in the stash: `git -C <repo> stash list | grep wrap-pull-past-dirty` names it and `git stash pop <ref>` finishes the restore.
- The session's own `EnterWorktree` worktree goes FIRST, before `wrap apply` runs, so the harness records the removal instead of finding the directory gone. Once `wrap scan` proves its branch squash-merged (tip matches the PR head) and the worktree is clean, remove it: `ExitWorktree remove` with `discard_changes: true` (the squashed commits are not ancestors of the default branch, so the tool asks; the proof is the confirmation), then `git branch -D <branch>`. The operator gave that confirmation once, as a standing rule, and a worktree whose PR merged this session is finished work, never something to keep. `ExitWorktree keep` only when the worktree is dirty, the branch is not proven merged, or the proof is unavailable (no `gh`); say which in `Left alone`. A plain secondary worktree still routes through `wrap apply --worktrees`, which skips it when dirty, detached, held by the checked-out branch, or on a branch no merge proof covers, and otherwise removes it and deletes that branch. A LOCK is not a skip reason there: the Agent tool locks every worktree it creates, so `apply` overrides the lock once the merge proof holds, then confirms the path is gone and reports `FAILED` when it is not.
- Never remove a dirty or foreign worktree, under `--worktrees` or otherwise.
- Never force-push and never rewrite history to make a delete or a pull succeed.
Expand Down
4 changes: 2 additions & 2 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ GENERATED , do not hand-edit. Regenerate: `bash lib/registry/feature-registry.sh
| `/kit:design` | `[H/I]` | Opt-in interactive solution-design beat between /think and /spec. Explores 2-3 approaches one question at a time, holds for your approval p… | SPEC-003, SPEC-004, SPEC-005 +105 | test-command-emit-sweep.sh, test-command-triggers.sh, test-design-record.sh +21 |
| `/kit:devs-team` | `[H/I]` | Parallel multi-lens critique of a solution design (the active spec if present, else the decision brief). Dispatches 5 engineering lenses, m… | SPEC-016, SPEC-018, SPEC-019 +11 | test-gate-vocab-recording.sh, test-meta.sh, test-outcome-emit-sweep.sh |
| `/kit:dispatch` | `[H/I]` | Fire several disjoint VALIDATED specs concurrently, each in its own worktree, then converge. Cross-goal fan-out behind a disjointness gate … | SPEC-002, SPEC-016, SPEC-017 +80 | test-advisor-ledger-emit.sh, test-agent-effectiveness.sh, test-attempt-state.sh +31 |
| `/kit:docs` | `[H/I]` | Update all project documentation to match the current codebase. Cross-references the diff against every doc file and fixes drift. | SPEC-001, SPEC-002, SPEC-003 +194 | proof-loop-09-scenario-b.sh, run-all.sh, run-workflow.sh +70 |
| `/kit:docs` | `[H/I]` | Update all project documentation to match the current codebase. Cross-references the diff against every doc file and fixes drift. | SPEC-001, SPEC-002, SPEC-003 +194 | proof-loop-09-scenario-b.sh, run-all.sh, run-workflow.sh +71 |
| `/kit:draft-agent` | `[H/I]` | Meta-agent agent-builder. From a one-line description, generates a new subagent definition OR a mega-goal sub-goal file and (by default) in… | SPEC-089, SPEC-108, SPEC-139 +1 | test-agent-effectiveness.sh, test-command-emit-sweep.sh, test-meta-agent.sh +1 |
| `/kit:execute` | `[H/I]` | Autonomous spec execution with verification. Dispatches worker subagents per task, verifies each with task-verifier, retries fixable failur… | SPEC-001, SPEC-003, SPEC-004 +60 | test-break-it.sh, test-gate-vocab-recording.sh, test-hooks.sh +9 |
| `/kit:explain` | `[H/I]` | Turn a merged change into a literate-diff explainer a human READS to understand: background -> goal + intuition -> a prose-ordered diff -> … | SPEC-050, SPEC-060, SPEC-094 +18 | proof-loop-09-scenario-b.sh, test-boundary-lint.sh, test-command-emit-sweep.sh +11 |
Expand Down Expand Up @@ -50,7 +50,7 @@ GENERATED , do not hand-edit. Regenerate: `bash lib/registry/feature-registry.sh
| `/kit:verify` | `[H/I]` | Re-run the test levels (task-verifier + integration-verifier + acceptance-verifier + system-verifier) on the current spec/branch read-only,… | SPEC-002, SPEC-003, SPEC-006 +37 | test-break-it.sh, test-codex-hooks.sh, test-command-emit-sweep.sh +8 |
| `/kit:visual-team` | `[H/I]` | Parallel multi-lens critique of a visual/UI design. Dispatches 5 design lenses, merges findings, reports a verdict. Report-only, downstream… | SPEC-016, SPEC-018, SPEC-019 +10 | test-command-emit-sweep.sh, test-meta.sh |
| `/kit:wayfind` | `[H]` | Plan a chunk of work too big for one agent session as a shared decision map: map.md + typed decision tickets in the mega-goal folder, resol… | SPEC-206, SPEC-207, SPEC-217 +2 | - |
| `/kit:wrap` | `[H/I]` | The session-scoped landing step after ship: flips board rows, merges the operator's own green PRs one at a time, checks deploys, tidies bra… | SPEC-020, SPEC-060, SPEC-072 +12 | test-bin-forwarders.sh, test-boundary-lint.sh, test-config-registry.sh +4 |
| `/kit:wrap` | `[H/I]` | The session-scoped landing step after ship: flips board rows, merges the operator's own green PRs one at a time, checks deploys, tidies bra… | SPEC-020, SPEC-060, SPEC-072 +12 | test-bin-forwarders.sh, test-boundary-lint.sh, test-config-registry.sh +5 |

## Agents

Expand Down
61 changes: 61 additions & 0 deletions docs/implementation-notes/gitattributes-union.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# gitattributes-union

Delta from the brief. The brief asked for a resolver on `bin/wrap apply`'s POP CONFLICT path
that would resolve a `merge=union` file's conflict hunks by hand.

## 2026-09-16 14:00 The premise was refuted, so the root cause shipped instead

Context: the brief said `git stash pop` uses the ordinary 3-way merge and leaves conflict
markers on a union-declared file, and that a session hand-resolved that four times.

Decision: no resolver. Declare the repo's append-only files `merge=union` in a
`.gitattributes` this repo never had.

Why, in three parts.

1. Measured. A fresh repo declaring one file `merge=union`, an upstream commit prepending a
row and a local uncommitted row in the same hunk: `git stash push`, fast-forward,
`git stash pop` exits 0, both rows land, no markers, the file stays unstaged. Union
resolves during the pop. Binary-declared and NUL-byte variants do fail the pop, and they
leave no markers, so a hunk resolver never sees them either.
2. Already asserted. `tests/test-wrap.sh` "knob on: a union-marked file blocked by the same
pull resolves during the pop" has pinned this since the pull-past-dirty work, and
`lib/wrap/wrap.sh` states the same contract in the `_unstash` comment.
3. The real cause. This repo carried no `.gitattributes` at all, so
`git check-attr merge -- _meta/BACKLOG.md` answered `unspecified`. The board was never
union HERE. The proposed `check-attr == union` gate would not have fired either. The
sibling repo ops-toolkit declares the same file union and never needed the hand-resolve.

Alternatives: ship the resolver anyway as a safety net. Rejected, because no text-file path
reaches it, and unreachable code carries no proof.

Impact: the four-step hand procedure is gone at the source, and parallel branches adding a
board row now merge.

## 2026-09-16 14:20 The union declaration exposed a placement bug, so it is fixed here

Context: declaring `_meta/BACKLOG.md` union makes `_pull_default`'s carry-across-pull path
reachable for it. That path inserts carried lines below the first `---` line. The board has no
such line, so `_log_anchor_head_lines` returns 0 and the carried row prepended to line 1, above
the document title and outside the table.

Decision: `_union_carry_back` keeps the anchor rule for a file that has an anchor, and runs
`git merge-file --union` over pulled/base/local for a file that has none. `_pull_default` saves
the pre-pull content beside the local copy, because after the pull the base exists nowhere in
the worktree.

Why: git's driver is the same one the declaration names, so a carried row lands where a merge
or a stash pop would put it, at the table tail. A first attempt placed the block after the line
it followed locally; review broke that in four ways (a blank neighbour fell back to line 1, a
repeated neighbour picked the wrong table, non-contiguous added lines moved as one block, an
empty first line matched any blank). The driver answers all four and is less code.

The anchor branch survives because the driver orders the incoming entry above the local one.
For a newest-first `LAB_LOG` that is backwards, and `tests/test-wrap.sh` asserts the carried
line sits above the older entries. Narrowing the new path to anchorless files leaves every
asserted contract untouched rather than weakening one. ops-toolkit's board has a `---` at line
112 and so never hit the placement bug at all.

Open questions: a file declared union that `git merge-file` refuses (a binary one) now restores
its pre-pull content and reports `FAILED carry` rather than merging. Declaring a binary file
union is a configuration error; this makes it a loud one.
62 changes: 62 additions & 0 deletions docs/verification/gitattributes-union.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Proof of done: the repo declares its append-only files `merge=union`

2026-09-16. Acceptance: `git check-attr merge` answers `union` for `_meta/BACKLOG.md`,
`_meta/backlog-staging.md` and `docs/implementation-notes/*.md` and `unspecified` for source
files; a sibling session's uncommitted board row survives `bin/wrap apply --apply` with
`wrap.pull_past_dirty` true, lands inside the table rather than above the title, stays
unstaged, and leaves no stash; two branches each adding a row merge with no conflict. Lane:
normal. Files: `.gitattributes`, `lib/wrap/wrap.sh`, `tests/test-gitattributes-union.sh`,
`commands/wrap.md`.

## The failure this replaces

The kit shipped every union mechanism and no `.gitattributes` of its own, so the board was
never declared union HERE while the sibling repo ops-toolkit declared the same file and never
collided. Two sessions adding a row hit a stash-pop conflict; one sitting resolved it four
times by hand, each time with a stash push, an ff-only merge, a conflicted pop, a throwaway
script over the hunks, and a reset to leave the row unstaged.

The brief proposed a hunk resolver on wrap's POP CONFLICT path instead. A measured repro
refuted the premise: `git stash pop` resolves a union-declared text file with no markers.
Evidence and the rejected alternative: `docs/implementation-notes/gitattributes-union.md`.

## Green run

Command: `bash tests/test-gitattributes-union.sh`
Exit: 0
Output: `27/27 passed`
Verdict: PASS. Four cases against real git repos built on disk, two of them the
no-declaration controls that reproduce the hand-resolved conflict and the refused merge.

Command: `bash tests/test-wrap.sh`
Exit: 0
Output: `test-wrap: all 508 passed`
Verdict: PASS. The carry-across-pull contract is unchanged for anchored files; the new
union-driver path is reached only by a file with no `---` anchor.

## Negative control

Command: `bash lib/gate/negctl.sh . "bash tests/test-gitattributes-union.sh" "git rm -q --cached .gitattributes >/dev/null 2>&1; rm -f .gitattributes"`
Exit: 0
Output:

```
Exit: 0 (green before mutation)
Mutation: git rm -q --cached .gitattributes >/dev/null 2>&1; rm -f .gitattributes
Changed: .gitattributes
Exit: 1 (under mutation, RED expected)
Restore: git checkout HEAD -- .gitattributes
Exit: 0 (green after restore)
Verdict: PASS
```

Verdict: PASS. Deleting the declaration turns the suite red and restoring it turns it green,
so the suite measures the declaration and not the fixtures around it.

## Reproduce

```
git -C <repo> checkout feat/wrap-pop-union
bash tests/test-gitattributes-union.sh
bash tests/run-all.sh
```
Loading
Loading