create-religion 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/lib/dashboard.js +4 -3
  3. package/dist/lib/doctor.js +21 -7
  4. package/dist/lib/settings.js +4 -0
  5. package/dist/lib/state.js +125 -2
  6. package/dist/lib/status.js +21 -4
  7. package/package.json +1 -1
  8. package/template/.agents/skills/audit/SKILL.md +13 -7
  9. package/template/.agents/skills/complete/SKILL.md +22 -6
  10. package/template/.agents/skills/complete/reference/land-on-top.md +174 -0
  11. package/template/.agents/skills/distill/SKILL.md +2 -1
  12. package/template/.agents/skills/feature/SKILL.md +4 -1
  13. package/template/.agents/skills/fix/SKILL.md +3 -2
  14. package/template/.agents/skills/implement/SKILL.md +38 -8
  15. package/template/.agents/skills/implement/reference/item-worktree.md +116 -0
  16. package/template/.agents/skills/implement/reference/worktrees.md +175 -0
  17. package/template/.agents/skills/rollback/SKILL.md +3 -1
  18. package/template/.claude/hooks/write-handoff.mjs +80 -3
  19. package/template/.claude/skills/audit/SKILL.md +13 -7
  20. package/template/.claude/skills/complete/SKILL.md +22 -6
  21. package/template/.claude/skills/complete/reference/land-on-top.md +174 -0
  22. package/template/.claude/skills/distill/SKILL.md +2 -1
  23. package/template/.claude/skills/feature/SKILL.md +4 -1
  24. package/template/.claude/skills/fix/SKILL.md +3 -2
  25. package/template/.claude/skills/implement/SKILL.md +38 -8
  26. package/template/.claude/skills/implement/reference/item-worktree.md +116 -0
  27. package/template/.claude/skills/implement/reference/worktrees.md +175 -0
  28. package/template/.claude/skills/rollback/SKILL.md +3 -1
  29. package/template/AGENTS.md +9 -3
  30. package/template/CLAUDE.md +9 -3
  31. package/template/religion/config.json +2 -1
@@ -0,0 +1,174 @@
1
+ # Landing on top of what already landed
2
+
3
+ Used before an item's branch is squash-merged or pushed, when the branch it lands in has moved
4
+ since the item began: another item, built in its own worktree, landed first. Both items wrote
5
+ the same state files, and a text merge of those conflicts every time, because each holds
6
+ bookkeeping derived from its own branch. So the landed copy of each is taken, and this item's
7
+ share of it is done again on top.
8
+
9
+ The **target** is the branch the item lands in: the default branch, or an automated run's
10
+ integration branch. Under `pull-request`, fetch it first, which is a network read and asks
11
+ outside an automated run, and use the remote-tracking name, such as `origin/main`.
12
+
13
+ The merge this makes is a commit on the item's own branch, like the work commit, and is covered
14
+ by the same yes; it never writes the target.
15
+
16
+ Run each block as **one** command, from the item's checkout, after the work commit. Each runs in
17
+ a subshell and sets what it needs. A block that stops ends the landing: report what it printed
18
+ and stop. A stop after block 1 leaves the merge open: either put right what the block named and
19
+ run it again, or abandon the merge with the block in "Abandoning the merge" below.
20
+
21
+ ## 1. Bring in what landed
22
+
23
+ (
24
+ root=$(git rev-parse --show-toplevel) || exit 1
25
+ target='<the target>'
26
+ [ -z "$(git -C "$root" status --porcelain -- . ':!religion/context/handoff.md')" ] ||
27
+ { echo "uncommitted changes; make the work commit first"; exit 1; }
28
+ git -C "$root" checkout -q -- religion/context/handoff.md 2>/dev/null
29
+ git -C "$root" rev-parse -q --verify "$target^{commit}" >/dev/null || { echo "no such target: $target"; exit 1; }
30
+ if git -C "$root" merge-base --is-ancestor "$target" HEAD; then echo "nothing landed since"; exit 0; fi
31
+ git -C "$root" merge --no-commit --no-ff "$target"
32
+ git -C "$root" rev-parse -q --verify MERGE_HEAD >/dev/null || { echo "the merge did not start"; exit 1; }
33
+ base=$(git -C "$root" merge-base HEAD MERGE_HEAD)
34
+ for f in build-plan.md context/findings.md learning/journal.md context/project-overview.md context/handoff.md; do
35
+ p="religion/$f"
36
+ git -C "$root" diff --quiet "$base" MERGE_HEAD -- "$p" && continue
37
+ if git -C "$root" cat-file -e "MERGE_HEAD:$p" 2>/dev/null; then
38
+ git -C "$root" checkout MERGE_HEAD -- "$p" || exit 1
39
+ echo "taken as landed: $p"
40
+ else
41
+ git -C "$root" rm -q -f --ignore-unmatch -- "$p" || exit 1
42
+ echo "no longer tracked by the target: $p"
43
+ fi
44
+ done
45
+ left=$(git -C "$root" diff --name-only --diff-filter=U)
46
+ if [ -n "$left" ]; then
47
+ echo "conflicts outside the shared state files:"; echo "$left"
48
+ git -C "$root" merge --abort; exit 1
49
+ fi
50
+ echo "merged; redo this item's share of each file taken as landed"
51
+ )
52
+
53
+ `nothing landed since` means the target is already part of the branch: skip the rest. Only a
54
+ shared file the target changed since the item began is taken from it; one it left alone merges
55
+ as usual, carrying this item's changes with nothing to redo. A shared file the target stopped
56
+ tracking is removed, as the target has it; this item's copy is still in `HEAD`. The handoff is generated and rewritten at the end of every turn, so discarding it loses nothing.
57
+
58
+ Taking the target's copy of a shared file resolves that file, conflicted or not. A conflict in
59
+ any other file, product code included, is not this routine's to settle: the merge is abandoned,
60
+ the branch is exactly as it was, and the paths are reported for review. Never resolve them by
61
+ taking either side.
62
+
63
+ While the merge is open, `HEAD` is this item's own work and `MERGE_HEAD` is what landed.
64
+
65
+ ## 2. Redo this item's share
66
+
67
+ For each file block 1 took as landed, edit it as it now stands, taking this item's part from
68
+ `HEAD`'s copy, as `git show HEAD:religion/<file>` prints it:
69
+
70
+ | File | This item's share |
71
+ | --- | --- |
72
+ | `build-plan.md` | a feature ticks its line, and its parent once every sub-item is ticked; a rollback unticks its target and adds its note; a fix or refactor campaign has none |
73
+ | `context/findings.md` | append each entry `HEAD`'s copy holds that the merge base's did not, still `open`, `fixed` or `unverified`, unchanged; one whose identifier the landed copy already holds, two fixes' bare `F-1` say, takes its permanent prefixed form, the archive's name before it as completion would archive it, and the rename is reported; when the landed copy is the stub, replace its placeholder line |
74
+ | `learning/journal.md` | append its entry, the one whose `**Archive:**` line names this item's archive |
75
+ | `context/project-overview.md` | none: left as landed, and stale until $overview runs, as after any completion |
76
+ | `context/handoff.md` | none |
77
+
78
+ Then compare `HEAD`'s ledger with the merge base's,
79
+ `git diff $(git merge-base HEAD MERGE_HEAD) HEAD -- religion/context/findings.md`. A change this
80
+ item made to an entry it did not add is not carried, since the landed copy is the one kept; name
81
+ each such entry in the report so it can be made again by hand.
82
+
83
+ Everything else, this item's archive included, merged as git merges it.
84
+
85
+ ## 3. Check, then commit the merge
86
+
87
+ Stage the redone files for the merge commit, then check them:
88
+
89
+ (
90
+ root=$(git rev-parse --show-toplevel) || exit 1
91
+ for f in build-plan.md context/findings.md learning/journal.md context/project-overview.md context/handoff.md; do
92
+ [ -e "$root/religion/$f" ] && git -C "$root" ls-files --error-unmatch -- "religion/$f" >/dev/null 2>&1 &&
93
+ { git -C "$root" add -- "religion/$f" || exit 1; }
94
+ done
95
+ for f in build-plan.md context/findings.md learning/journal.md; do
96
+ ! grep -n -e '^<<<<<<< ' -e '^>>>>>>> ' -- "$root/religion/$f" 2>/dev/null || { echo "conflict markers in religion/$f"; exit 1; }
97
+ done
98
+ [ -z "$(git -C "$root" diff --name-only --diff-filter=U)" ] || { echo "unresolved paths remain"; exit 1; }
99
+ echo "ready for Verify"
100
+ )
101
+
102
+ Only the files edited by hand are searched: git leaves no markers in a file it merged cleanly,
103
+ and a document elsewhere may show one on purpose.
104
+
105
+ Run the project's full `Verify` on the result: it is new code, made of both items, and neither
106
+ was checked against the other. A turn that ends before the merge is committed has its handoff
107
+ rewritten by the hook; run block 3 again before committing, which stages it only while the index
108
+ still tracks it.
109
+
110
+ When `Verify` passes, commit the merge with a message that says what was brought in, such as
111
+ `merge: bring in what landed on main`.
112
+
113
+ When it fails, abandon the merge with the block below, which returns the branch to its work
114
+ commit, and stop with the failure. The item is complete on its branch but cannot land as it
115
+ stands: the way forward is $fix here, describing the failure, built on this branch.
116
+ Completing that fix lands both through this routine: block 2 then redoes the share of both, the
117
+ item's and the fix's, every row, each taken from `HEAD`, and the `## Landed` record goes into
118
+ both archives with the one range they share, each saying it is shared.
119
+
120
+ ## Abandoning the merge
121
+
122
+ (
123
+ set -o pipefail
124
+ root=$(git rev-parse --show-toplevel) || exit 1
125
+ git -C "$root" rev-parse -q --verify MERGE_HEAD >/dev/null || { echo "no merge open"; exit 0; }
126
+ g="$(git -C "$root" rev-parse --absolute-git-dir)/abandon"
127
+ list() { git -C "$root" -c core.quotePath=false "$@"; }
128
+ list ls-files -o | LC_ALL=C sort >| "$g.before" || exit 1
129
+ { list diff --name-only --no-renames HEAD && list diff --cached --name-only --no-renames HEAD; } >| "$g.tracked" || exit 1
130
+ git -C "$root" reset -q --hard HEAD; rc=$?
131
+ list ls-files -o | LC_ALL=C sort >| "$g.after" || exit 1
132
+ { cat "$g.tracked"; LC_ALL=C comm -23 "$g.before" "$g.after"; } | LC_ALL=C sort -u | sed 's/^/discarded: /'
133
+ rm -f "$g.before" "$g.after" "$g.tracked"
134
+ [ "$rc" -eq 0 ] || { echo "the reset failed: the merge is still open, and a path named above may not be set back"; exit 1; }
135
+ list ls-files -o --exclude-standard --directory | sed 's/^/left untracked: /'
136
+ echo "back at the work commit"
137
+ )
138
+
139
+ Block 1 started from a clean tree, so the work commit is the whole of this item's work, and
140
+ resetting to it ends the merge and returns the branch exactly as it was, whatever the merge left:
141
+ redone shares, a handoff the hook rewrote, output `Verify` regenerated, paths still in conflict.
142
+ Each path the reset set back or removed is named once: every tracked path whose working or staged copy
143
+ differed from the work commit, an edit made by hand while the merge was open among them, and every untracked or
144
+ ignored file in the way of a path the work commit has, such as one inside a directory the target
145
+ put where a file was, found by listing them with git before the reset and after it. Other
146
+ untracked files are left where they are and named, a whole directory as one line. Names are
147
+ printed as git prints them, quoted where they hold unusual characters. So nothing is lost without
148
+ a word. A reset that fails, such as on a directory it cannot write, still names what it removed,
149
+ then says the merge is still open: put right what git named and run the block again. Never
150
+ abandon with `git merge --abort` alone, which refuses while a file the merge touched has unstaged
151
+ changes. With no merge open, the block does nothing.
152
+
153
+ ## 4. Record what landed
154
+
155
+ When a merge was made, the item's `## Landed` record names the last such merge's second parent,
156
+ the target it brought in, as **Base**, and that merge commit and anything after it as **Commits**:
157
+ `git diff` from that base to the last commit is then this item's work alone, which is what a
158
+ rollback reverses. Without a merge, the record is as `$complete` describes. Commit the
159
+ record: in a worktree, an uncommitted file goes when the worktree is removed.
160
+
161
+ ## When the state directory is ignored
162
+
163
+ Then none of these files are in git, and the merge carries none of them: this item's archive,
164
+ plan tick, journal entry and ledger exist only in its worktree's copy of `religion/`. Before
165
+ the worktree can be removed, redo this item's share, as the table in block 2 lists it, in the
166
+ main checkout's own `religion/` files, and copy this item's archive there. When this landing
167
+ follows a failed `Verify`, that is the share and the archive of both, the item's and the fix's. With no `HEAD` copy
168
+ or merge base in git, the worktree's own files are this item's part, and the ledger saved when
169
+ the worktree was made, `religion/.state/findings-at-start.md` in it, is the base: an entry the
170
+ worktree's ledger holds that the saved one did not is this item's. Comparing the two also shows
171
+ each entry this item changed or removed, a repair it closed and pruned among them: make the
172
+ same change in the main checkout's ledger, or name the entry in the report, so a finding this
173
+ item closed does not stay open there. Removing the worktree deletes ignored files with it, so
174
+ until then they are the only record.
@@ -56,7 +56,8 @@ finding, already repaired, is an incident.
56
56
 
57
57
  For each group that earns it, propose the coding-standard rule that would have prevented every
58
58
  finding in it, in the standards' own voice, as an instruction for work not yet started, with the
59
- findings cited by identifier. A rule that only restates one finding is not a standard. Skip a
59
+ findings cited by their archived identifier, prefix included, such as `39a/F-2` or `27/F-161`.
60
+ A rule that only restates one finding is not a standard. Skip a
60
61
  group already recorded in `religion/learning/journal-archive.md` unless it has grown since.
61
62
 
62
63
  ## Step 3 - route by kind
@@ -65,7 +65,10 @@ plan top to bottom and take the first unchecked leaf that waits on nothing: an i
65
65
  those is unticked, or says `spike first` and no archive under `religion/history/spikes/` records
66
66
  `**For:** item N` with `**Settled:** yes`. Wording inside a code span does not count.
67
67
  Name each item passed over and what it waits on, and point a spike-first one at
68
- $spike. State which item you are spec'ing before going further.
68
+ $spike. An item another worktree is already building is passed over too: one that
69
+ `religion status --json` lists under `inFlight` with `here` false, or, without the `religion`
70
+ command, one the handoff lists as in flight in another worktree. Name it with its branch.
71
+ State which item you are spec'ing before going further.
69
72
 
70
73
  Asked by number for an item that waits, say what it waits on and stop: speccing it now
71
74
  builds on an answer nobody has yet. A reference to an item the plan does not have, or items
@@ -14,8 +14,9 @@ numbered, it ticks nothing, and it archives on its own.
14
14
  - **A description** - `$fix "password reset email never sends"`. A problem stated in the
15
15
  conversation already counts; this skill does not scan the project hoping to find
16
16
  something wrong.
17
- - **Finding identifiers** - `$fix F-03` specs the repair of that ledger entry, and
18
- `$fix F-03 F-07` repairs several together. Group findings only when they share a file
17
+ - **Finding identifiers** - `$fix 39a/F-2` specs the repair of that ledger entry, and
18
+ `$fix 39a/F-2 F-07` repairs several together. An identifier is used exactly as the
19
+ ledger heads it, with an item prefix when it has one, bare when it does not. Group findings only when they share a file
19
20
  and a severity, as $auto's fix mode does: one fix then stays one reviewable diff.
20
21
 
21
22
  - **An issue** - `$fix #42` or its link. Read it through the host's own command-line tool, for example `gh issue view 42 --json title,body,url` on
@@ -24,6 +24,10 @@ guess past broken configuration.
24
24
  **Read the spec.** If `religion/context/current-work.md` is still the stub, or its status
25
25
  is already complete, stop and say to run $feature, $fix, or
26
26
  $rollback first.
27
+ When it is the stub and `religion status --json` lists items under `inFlight` with `here`
28
+ false, or the handoff lists items in flight in other worktrees, name each with its worktree
29
+ and branch, and say that resuming one means working from that worktree. Never start one of
30
+ them again here.
27
31
 
28
32
  **Take the context.** Conventions from `religion/context/coding-standards.md`, the data
29
33
  model from the project overview. Code that ignores them is rework, not progress.
@@ -56,6 +60,17 @@ They differ only in how the work leaves the branch at completion, which is not t
56
60
  concern. On a resume the branch exists already, so check it out rather than creating a
57
61
  second one.
58
62
 
63
+ **With `workflow.parallelItems` on**, under either of those modes, a feature from the build
64
+ plan starts in its own worktree instead: follow `reference/item-worktree.md`, which creates
65
+ it on that branch beside the project and moves the spec there. Creating it is part of
66
+ positioning the work, as creating the branch is; removing it is deleting files, and asks.
67
+ An automated run keeps the plain branch, since it removes each item's branch once it lands,
68
+ and a worktree holding the branch would stop that. From then on the item is the worktree's.
69
+ Move the session into it where the tool can; otherwise address every file and
70
+ command to the worktree's path, and tell the user to open later sessions there, since hooks
71
+ keep running where a session started. A resume inside the worktree is the ordinary resume
72
+ below. Fixes, rollbacks and refactor campaigns keep the branch above.
73
+
59
74
  Either way, if the project is not a git repository, say so and ask the user to initialize
60
75
  one. The loop needs history to be reviewable.
61
76
 
@@ -154,16 +169,30 @@ Two waves of one, then a wave of two, then a wave of one.
154
169
  **Refuse a wave rather than guess at one.** Run the wave sequentially, and say why, when:
155
170
 
156
171
  - a marker names a later step, itself, or a step that does not exist
157
- - two steps in the wave would change the same file, which is a conflict the spec did not
158
- intend and the author is better placed to resolve than a merge
172
+ - two steps in the wave would change the same authored file, which is a conflict the spec
173
+ did not intend and the author is better placed to resolve than a merge. Output the
174
+ project's build generates from those files does not count; it is rendered once after
175
+ the join
159
176
  - the wave is larger than three steps, which usually means the spec is describing one step
160
177
  badly rather than three independent ones
161
178
 
162
- **Building a wave.** Each step runs in its own subagent with the step, the spec, the
163
- standards, and nothing else. They build and **return**; they do not tick, do not write the
164
- spec, and do not touch the ledger. The parent applies what comes back, runs the gate for each
165
- step, and performs every write. This is the same rule audit's lenses follow, for the same
166
- reason: concurrent writers to one file is the one thing waves must never introduce.
179
+ **Building a wave.** Each step builds in its own detached git worktree outside the project,
180
+ set up and torn down by `reference/worktrees.md`, so a step cannot see or change another
181
+ step's files. Each runs in its own subagent with the step, the spec, the standards, the path
182
+ of its worktree, and nothing else. They build there and **return** their evidence; they do
183
+ not tick, do not write the spec, and do not touch the ledger. The parent extracts each
184
+ step's patch and performs every write. This is the same rule audit's lenses follow, for the
185
+ same reason: concurrent writers to one file is the one thing waves must never introduce.
186
+
187
+ **Joining a wave.** The parent concatenates the patches in step order and applies them with
188
+ one `git apply`, which lands all of them or none. Then it renders generated output once,
189
+ when any was left out of the patches, and runs each step's gate on the joined tree. A step
190
+ that fails its gate there leaves its patch applied for review, as a failed step always does.
191
+
192
+ **A join that does not apply stops the wave** with the project exactly as it was. Report the
193
+ paths `git apply` named and where each worktree is, keep the worktrees, tick nothing, and
194
+ leave the conflict for review. Never retry with a three-way apply, merge, or edit to make it
195
+ fit.
167
196
 
168
197
  **A failure stops the wave.** Report which steps passed and which did not, tick only what
169
198
  passed, and stop. Do not retry the wave; the next run resumes at the first unticked step,
@@ -180,7 +209,8 @@ block completion, so close the loop now rather than discovering it later.
180
209
 
181
210
  For each `open` P0 or P1:
182
211
 
183
- 1. **Append it to the spec's build steps** first (`- [ ] Repair F-03 - <title>`), so the
212
+ 1. **Append it to the spec's build steps** first (`- [ ] Repair 39a/F-2 - <title>`, using
213
+ the identifier exactly as the ledger heads it), so the
184
214
  repair is on the record and survives a cleared context. That changes what the spec asks
185
215
  for, so record it as an amendment naming the finding, and renew the stamp.
186
216
  2. Build it through the same loop as Step 2: smallest change, diff, explanation, evidence.
@@ -0,0 +1,116 @@
1
+ # Starting an item in its own worktree
2
+
3
+ Used when `workflow.parallelItems` is `true`, `git.mode` is `branch-per-item` or
4
+ `pull-request`, the spec is a feature from the build plan, and no automated run is underway.
5
+ The item gets its own git worktree beside the project, on the branch Step 1 would otherwise
6
+ create, and its spec moves there. The main checkout is then idle, and `$feature` can spec
7
+ the next item from it.
8
+
9
+ Run each block as **one** command. Each runs in a subshell, so a stop ends that block and never
10
+ the shell a tool keeps between commands, and nothing set in one block survives into the next:
11
+ each sets what it needs. A block that stops ends the start: report what it printed and stop.
12
+
13
+ A branch whose name holds a `'` is refused before anything runs, since the blocks quote it.
14
+
15
+ ## 1. Check, then create
16
+
17
+ (
18
+ root=$(git rev-parse --show-toplevel) || exit 1
19
+ [ "$(git -C "$root" rev-parse --absolute-git-dir)" = "$(git -C "$root" rev-parse --path-format=absolute --git-common-dir)" ] ||
20
+ { echo "not the main checkout: $root"; exit 1; }
21
+ [ -z "$(git -C "$root" status --porcelain -- . ':!religion/context/current-work.md' ':!religion/context/handoff.md')" ] ||
22
+ { echo "uncommitted changes beyond the spec"; exit 1; }
23
+ branch='<the branch Step 1 names>'
24
+ tree="${root%/*}/${root##*/}.$(printf '%s' "$branch" | tr / -)"
25
+ [ ! -e "$tree" ] && [ ! -L "$tree" ] || { echo "already exists: $tree"; exit 1; }
26
+ ! git -C "$root" show-ref --verify --quiet "refs/heads/$branch" || { echo "branch exists: $branch"; exit 1; }
27
+ git -C "$root" worktree add -q -b "$branch" "$tree" HEAD || exit 1
28
+ echo "$tree"
29
+ )
30
+
31
+ Started from inside a linked worktree, the first check stops: that worktree is an item already,
32
+ and a second one made from it would land on top of it. Anything uncommitted but the spec and
33
+ the generated handoff stops it too, settings and plan edits included, since the worktree is
34
+ made from `HEAD` and would run without them. Commit them first.
35
+
36
+ The path is the project's directory name, a dot, and the branch with each `/` as `-`, in the
37
+ project's parent directory: `religion` building `feature/export-reports` gets
38
+ `religion.feature-export-reports`. Landing the item later looks for it there, so never choose
39
+ another. A path or branch that already exists is another item's, or a start that stopped after
40
+ this block: when the path is a worktree on this item's branch and the main checkout still holds
41
+ the spec, go on to block 2; otherwise stop and say which, never reusing or replacing it.
42
+
43
+ ## 2. Copy what git does not carry, then move the spec
44
+
45
+ Safe to run again until block 3 has run, and it refuses after that. Use the same branch as
46
+ block 1:
47
+
48
+ (
49
+ root=$(git rev-parse --show-toplevel) || exit 1
50
+ branch='<the branch Step 1 names>'
51
+ tree="${root%/*}/${root##*/}.$(printf '%s' "$branch" | tr / -)"
52
+ [ "$(git -C "$tree" branch --show-current 2>/dev/null)" = "$branch" ] || { echo "not this item's worktree: $tree"; exit 1; }
53
+ spec="religion/context/current-work.md"
54
+ ! grep -q '_Nothing in progress' "$root/$spec" || { echo "the main checkout holds no spec to move"; exit 1; }
55
+ if [ -f "$tree/$spec" ] && ! grep -q '_Nothing in progress' "$tree/$spec" && ! cmp -s "$root/$spec" "$tree/$spec"; then
56
+ echo "the worktree already holds another spec"; exit 1
57
+ fi
58
+ for f in .claude/settings.json .claude/settings.local.json .claude/hooks; do
59
+ if [ -e "$root/$f" ] && git -C "$root" check-ignore -q "$f"; then
60
+ mkdir -p "$tree/$(dirname "$f")" && cp -RPp "$root/$f" "$tree/$(dirname "$f")/" || exit 1
61
+ fi
62
+ done
63
+ if git -C "$root" check-ignore -q religion; then
64
+ (cd "$root/religion" && find . -path ./.state -prune -o -type f -print) > "$tree.files" || exit 1
65
+ while IFS= read -r f; do
66
+ mkdir -p "$tree/religion/$(dirname "$f")" && cp -Pp "$root/religion/$f" "$tree/religion/$f" || exit 1
67
+ done < "$tree.files"
68
+ rm -f "$tree.files"
69
+ fi
70
+ for d in node_modules; do
71
+ [ -d "$root/$d" ] || continue
72
+ [ -e "$tree/$d" ] || [ -L "$tree/$d" ] || ln -s "$root/$d" "$tree/$d" || exit 1
73
+ git -C "$tree" check-ignore -q "$d" || printf '/%s\n' "$d" >> "$(git -C "$root" rev-parse --path-format=absolute --git-path info/exclude)" || exit 1
74
+ done
75
+ if git -C "$root" check-ignore -q religion && [ -f "$root/religion/context/findings.md" ]; then
76
+ mkdir -p "$tree/religion/.state" && cp -p "$root/religion/context/findings.md" "$tree/religion/.state/findings-at-start.md" || exit 1
77
+ fi
78
+ mkdir -p "$tree/religion/context" && cp -p "$root/$spec" "$tree/$spec" && cmp -s "$root/$spec" "$tree/$spec" || exit 1
79
+ echo "copied"
80
+ )
81
+
82
+ The installer's output that git ignores is copied only where git ignores it; a tracked copy is
83
+ already in the worktree. When `religion/` itself is ignored, which `doctor` reports as "state is
84
+ local only", every state file but `religion/.state/` is copied, since each worktree keeps its own
85
+ activity record; the ledger as it stood is saved in the worktree's `religion/.state/` as
86
+ `findings-at-start.md`, the base landing later tells this item's findings apart by. The list of files is written beside the worktree, not piped, so a failed copy
87
+ stops the block in every shell; a block that stopped there leaves that list, `<worktree>.files`,
88
+ and the rerun removes it.
89
+
90
+ The checks at the top are what make a rerun safe. Once block 3 has reset the main checkout's
91
+ spec, there is nothing to move, and copying would put the stub over the only copy of the spec.
92
+ A worktree that already holds a different spec belongs to work that has started, and copying the
93
+ main checkout's over it would replace that work.
94
+
95
+ Each installed dependency directory at the project root that the gate needs is linked, never
96
+ installed: an install needs its own yes. Name each one in the `for d` list; `node_modules` is the
97
+ usual one. A pattern such as `node_modules/` matches a directory but not a link to one, so a link
98
+ git would not ignore is added to the repository's own exclude file, which is never committed;
99
+ without that, the link would be committed with the item's work.
100
+
101
+ ## 3. Reset the main checkout's spec
102
+
103
+ Only after block 2 printed `copied`: reset the main checkout's
104
+ `religion/context/current-work.md` to its stub, exactly as shipped, as `$complete` does.
105
+ The spec now lives only in the worktree. That is a move, not a deletion: if the copy did not
106
+ match, nothing is reset, and the spec is still where it was.
107
+
108
+ ## 4. Confirm
109
+
110
+ From the main checkout, `religion status` lists the item under `inFlight` with its branch and
111
+ `here` false, and reports nothing active. From the worktree, it reports the item as active, and
112
+ `religion doctor` there names the main checkout in its git check. Without the `religion`
113
+ command, the handoff and the two specs say the same.
114
+
115
+ From here on, every file and command of this item is the worktree's. Removing the worktree is
116
+ deleting files, so it asks, every time.
@@ -0,0 +1,175 @@
1
+ # Building a wave in worktrees
2
+
3
+ One detached git worktree per wave step, outside the project, so a step can neither see nor
4
+ change another step's files. The parent runs every command here; a step's subagent only
5
+ works inside the worktree it is given.
6
+
7
+ Run the phases in order. A command that fails stops the wave: report it and stop, as for any
8
+ failed step.
9
+
10
+ ## 1. Base
11
+
12
+ Once per wave, after every earlier step has landed, take the base, make the wave's directory,
13
+ and save the values every later phase needs. Run these lines as **one** command: the save
14
+ needs the values the first lines produce, and a fresh shell would have none of them.
15
+
16
+ (
17
+ root=$(git rev-parse --show-toplevel) || exit 1
18
+ case "$root" in */step-*) [ ! -f "${root%/*}/base" ] || exit 1;; esac
19
+ [ ! -e "$root/religion/.state/wave" ] || { cat "$root/religion/.state/wave"; exit 1; }
20
+ base=$(git -C "$root" stash create) || exit 1
21
+ [ -n "$base" ] || base=$(git -C "$root" rev-parse HEAD) || exit 1
22
+ wave=$(mktemp -d) && wave=$(cd -- "$wave" >/dev/null && pwd -P) || exit 1
23
+ printf '%s\n' "$base" > "$wave/base" || exit 1
24
+ mkdir -p "$root/religion/.state" && printf '%s\n' "$wave" > "$root/religion/.state/wave" || exit 1
25
+ )
26
+
27
+ Each line that can fail stops the block, rather than `set -e`, which would stay on in a tool
28
+ that keeps one shell between commands. The block runs in a subshell, so its exits end only
29
+ the subshell and never that shell; it hands nothing back, since every later phase reads the
30
+ saved values. The `cd` sends its output away so a shell hook that prints on a directory change
31
+ cannot end up in the saved path. Without the stops, a failed `mktemp` leaves `wave`
32
+ empty, `cd ""` stays in the project, and the block saves the project itself as the wave; a
33
+ failed `git stash create` falls back to `HEAD` and drops uncommitted earlier steps.
34
+
35
+ A saved wave that already exists means an earlier one was not torn down, which is what a
36
+ failed join leaves. The block prints the directory it names and stops, so the record of where
37
+ those worktrees are is never overwritten; deal with them first.
38
+
39
+ Every command that touches the project runs against `$root`, never the current directory.
40
+ From a subdirectory, `git apply` skips every path outside it and still exits 0, and
41
+ `git ls-files` lists only what is below it, so a wave started there would join nothing and
42
+ then tear down the work as if it had landed.
43
+
44
+ `git stash create` makes a commit of the working tree without touching the stash list, any
45
+ ref, or the working tree, so uncommitted earlier steps are part of the base. An empty result
46
+ means the tree is clean, and `HEAD` is the base.
47
+
48
+ Never `git stash push`, and never `git add -N` in the project: the user's index and stash
49
+ are not the wave's to change.
50
+
51
+ ## 2. Create
52
+
53
+ `pwd -P` saves the resolved path, the form `git rev-parse` reports, so the check below can
54
+ compare the two. A shell that does not keep variables between commands starts each later
55
+ phase by reading the values back, and stops if the current directory has drifted into one of
56
+ the wave's own worktrees:
57
+
58
+ root=$(git rev-parse --show-toplevel)
59
+ case "$root" in */step-*) [ ! -f "${root%/*}/base" ] || exit 1;; esac
60
+ wave=$(cat "$root/religion/.state/wave")
61
+ base=$(cat "$wave/base")
62
+ case "$root/" in "$wave"/*) exit 1;; esac
63
+
64
+ From inside a worktree, `root` would be that worktree, and the join would land there and
65
+ report success while the project stayed unchanged. The first `case` stops in a step worktree
66
+ of any wave, including one kept by an earlier failed join, recognised by the `base` file in
67
+ the wave directory beside it; the second stops in one of this wave's own. A linked worktree
68
+ the user works from is neither, so it is not refused.
69
+
70
+ Then, once per step, named for the step's number:
71
+
72
+ git -C "$root" worktree add --detach "$wave/step-<n>" "$base"
73
+
74
+ `--detach` creates no branch, whatever `git.mode` says. Keeping the worktrees outside the
75
+ project keeps a second copy of the source out of the project's own tests, linters and file
76
+ watchers.
77
+
78
+ The stash commit leaves untracked files out, so copy them in, all but the workflow's own
79
+ state, which would carry the saved wave path into the worktree:
80
+
81
+ git -C "$root" ls-files --others --exclude-standard -z |
82
+ while IFS= read -r -d '' f; do
83
+ case "$f" in religion/.state/*) continue;; esac
84
+ mkdir -p "$wave/step-<n>/$(dirname "$f")" && cp -RPp "$root/$f" "$wave/step-<n>/$f"
85
+ done
86
+
87
+ `-RP` copies an untracked link as a link. Plain `cp` refuses a link to a directory, and
88
+ following it would copy whatever it points at.
89
+
90
+ Then make the copies part of the step's base, so they are not handed back as new files:
91
+
92
+ git -C "$wave/step-<n>" add -A
93
+ git -C "$wave/step-<n>" write-tree > "$wave/step-<n>.base"
94
+
95
+ `write-tree` saves a tree object and nothing else: no commit, no ref. The saved file is the
96
+ base phase 5 diffs against, and it survives a shell that does not keep variables between
97
+ commands.
98
+
99
+ ## 3. Link
100
+
101
+ A fresh worktree has none of the project's installed dependencies. Symlink each installed
102
+ dependency directory at the project root that the gate needs, such as `node_modules`:
103
+
104
+ [ -e "$wave/step-<n>/node_modules" ] || [ -L "$wave/step-<n>/node_modules" ] ||
105
+ ln -s "$root/node_modules" "$wave/step-<n>/node_modules"
106
+
107
+ Skip the link when anything already sits at that path, such as an untracked link copied in
108
+ phase 2. `ln -s` onto an existing link to a directory does not replace it: it creates a new
109
+ link inside the directory it points at, which here is the project's real dependencies.
110
+
111
+ Never install into a worktree. That is a dependency install, which the Authority section
112
+ does not grant a wave. On Windows the link would be a junction, which this routine has not
113
+ been run against.
114
+
115
+ ## 4. Build
116
+
117
+ When the project's gate reads build output that is not committed, run the project's build in
118
+ the worktree before handing it to the subagent, so the step's checks can run there at all.
119
+ Run it in a subshell, `(cd "$wave/step-<n>" && <build>)`, so the current directory never
120
+ moves into the worktree.
121
+
122
+ ## 5. Extract
123
+
124
+ When the subagent returns, take its patch:
125
+
126
+ git -C "$wave/step-<n>" add -A
127
+ git -C "$wave/step-<n>" diff --cached --binary "$(cat "$wave/step-<n>.base")" -- . <exclusions> > "$wave/step-<n>.patch"
128
+
129
+ `add -A` writes to the worktree's own index, never the project's, and brings new files into
130
+ the patch. `<exclusions>` is one `':!<path>'` for each link made in phase 3, which an ignore
131
+ rule with a trailing slash such as `node_modules/` does not cover, because a link is not a
132
+ directory. Then one more for each path the project's instructions declare as generated, such
133
+ as rendered trees, so two steps that both regenerate a shared file do not
134
+ collide over output the build reproduces. A project that declares none keeps every path, and
135
+ a generated file that both steps touched then fails the join and stops it.
136
+
137
+ ## 6. Join
138
+
139
+ All patches, in step order, through **one** `git apply` of one file:
140
+
141
+ for p in "$wave"/step-<n>.patch "$wave"/step-<m>.patch; do [ -f "$p" ] || exit 1; done
142
+ cat "$wave"/step-<n>.patch "$wave"/step-<m>.patch > "$wave/join.patch"
143
+ git -C "$root" apply "$wave/join.patch"
144
+
145
+ Check that every step's patch exists first, and never pipe into `git apply`: without
146
+ `pipefail` a pipe reports `git apply`'s status alone, so a missing patch would land the
147
+ others as a success and teardown would delete the step that never applied.
148
+
149
+ One concatenated input applies all or nothing, including two patches to the same file.
150
+ Separate patch files given to one `git apply` do not: the first lands even when a later one
151
+ fails. Never `--3way`, never a merge, and never resolve a conflict by editing.
152
+
153
+ On failure the project is exactly as it was. Stop the wave, keep the worktrees, and report
154
+ the paths `git apply` named and where each worktree is.
155
+
156
+ On success, run the project's build once when any path was excluded, so generated output is
157
+ rendered from the joined sources, then run each step's gate on the joined tree.
158
+
159
+ ## 7. Teardown
160
+
161
+ Only after a successful join, and only for worktrees this run created for this wave. Once per
162
+ step:
163
+
164
+ rm "$wave/step-<n>/node_modules"
165
+ git worktree remove --force "$wave/step-<n>"
166
+ rm "$wave/step-<n>.patch" "$wave/step-<n>.base"
167
+
168
+ Then once for the wave:
169
+
170
+ rm "$wave/join.patch" "$wave/base"
171
+ rmdir "$wave"
172
+ rm "$root/religion/.state/wave"
173
+
174
+ Remove each link first, so nothing follows it into the project's real dependencies. Removing
175
+ any other worktree, or these after a failed join, still asks.
@@ -67,7 +67,9 @@ Stop on any failure. Do not repair a mismatch by choosing a nearby commit.
67
67
  1. Every recorded identifier matches `^[0-9a-f]{40}$`. Reject abbreviated or uppercase
68
68
  values.
69
69
  2. Every recorded commit resolves in this repository and is an ancestor of `HEAD`.
70
- 3. The recorded base is the parent of the earliest recorded commit.
70
+ 3. The recorded base is the parent of the earliest recorded commit. For an item landed on top
71
+ of another, that commit is the merge that brought the other in, and the base must be its
72
+ second parent, what it brought in: the first parent would reverse the other item's work.
71
73
  4. The working tree is clean apart from the rollback spec being written.
72
74
 
73
75
  ## Step 3 - preview the product diff