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.
- package/CHANGELOG.md +35 -0
- package/dist/lib/dashboard.js +4 -3
- package/dist/lib/doctor.js +21 -7
- package/dist/lib/settings.js +4 -0
- package/dist/lib/state.js +125 -2
- package/dist/lib/status.js +21 -4
- package/package.json +1 -1
- package/template/.agents/skills/audit/SKILL.md +13 -7
- package/template/.agents/skills/complete/SKILL.md +22 -6
- package/template/.agents/skills/complete/reference/land-on-top.md +174 -0
- package/template/.agents/skills/distill/SKILL.md +2 -1
- package/template/.agents/skills/feature/SKILL.md +4 -1
- package/template/.agents/skills/fix/SKILL.md +3 -2
- package/template/.agents/skills/implement/SKILL.md +38 -8
- package/template/.agents/skills/implement/reference/item-worktree.md +116 -0
- package/template/.agents/skills/implement/reference/worktrees.md +175 -0
- package/template/.agents/skills/rollback/SKILL.md +3 -1
- package/template/.claude/hooks/write-handoff.mjs +80 -3
- package/template/.claude/skills/audit/SKILL.md +13 -7
- package/template/.claude/skills/complete/SKILL.md +22 -6
- package/template/.claude/skills/complete/reference/land-on-top.md +174 -0
- package/template/.claude/skills/distill/SKILL.md +2 -1
- package/template/.claude/skills/feature/SKILL.md +4 -1
- package/template/.claude/skills/fix/SKILL.md +3 -2
- package/template/.claude/skills/implement/SKILL.md +38 -8
- package/template/.claude/skills/implement/reference/item-worktree.md +116 -0
- package/template/.claude/skills/implement/reference/worktrees.md +175 -0
- package/template/.claude/skills/rollback/SKILL.md +3 -1
- package/template/AGENTS.md +9 -3
- package/template/CLAUDE.md +9 -3
- package/template/religion/config.json +2 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
-
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readdirSync, readFileSync, readSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
|
|
14
14
|
const STATE = "religion";
|
|
@@ -49,10 +49,10 @@ const capped = (items) =>
|
|
|
49
49
|
items.length > LISTED ? [...items.slice(0, LISTED), `and ${items.length - LISTED} more in \`${STATE}/context/findings.md\``] : items;
|
|
50
50
|
|
|
51
51
|
const blocking = capped(
|
|
52
|
-
[...findings.matchAll(/^### (F-\d+) \[(P[01])\] (open|fixed) - (.+)$/gim)].map((m) => `${m[1]} [${m[2]}] ${m[3]} - ${plain(m[4])}`)
|
|
52
|
+
[...findings.matchAll(/^### ((?:[\w-]+\/)?F-\d+) \[(P[01])\] (open|fixed) - (.+)$/gim)].map((m) => `${m[1]} [${m[2]}] ${m[3]} - ${plain(m[4])}`)
|
|
53
53
|
);
|
|
54
54
|
// Unconfirmed findings are the least trusted titles in the ledger, so only their identifiers.
|
|
55
|
-
const unverifiedIds = [...findings.matchAll(/^### (F-\d+) \[(P[0-3])\] unverified - /gim)].map((m) => `${m[1]} [${m[2]}]`);
|
|
55
|
+
const unverifiedIds = [...findings.matchAll(/^### ((?:[\w-]+\/)?F-\d+) \[(P[0-3])\] unverified - /gim)].map((m) => `${m[1]} [${m[2]}]`);
|
|
56
56
|
const unverified = unverifiedIds.length > 10 ? [...unverifiedIds.slice(0, 10), `and ${unverifiedIds.length - 10} more`] : unverifiedIds;
|
|
57
57
|
|
|
58
58
|
// The same stamp overview writes and status compares: the plans' SHA-256, first 16 characters.
|
|
@@ -62,6 +62,78 @@ const stale = stamp !== undefined && stamp !== plansHash;
|
|
|
62
62
|
|
|
63
63
|
const notes = (inbox.match(/^- .+$/gm) ?? []).length;
|
|
64
64
|
|
|
65
|
+
// Items active in the repository's other worktrees, found as status finds them: from git's own
|
|
66
|
+
// worktree records, since this hook runs no subprocess. A spec there is written by whoever
|
|
67
|
+
// works in that worktree, so what is copied from it is cleaned like a ledger title.
|
|
68
|
+
const quiet = (fn) => {
|
|
69
|
+
try {
|
|
70
|
+
return fn();
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const gitDir = (dotgit) => {
|
|
76
|
+
const stat = quiet(() => statSync(dotgit));
|
|
77
|
+
if (!stat) return null;
|
|
78
|
+
if (stat.isDirectory()) return dotgit;
|
|
79
|
+
const target = quiet(() => readFileSync(dotgit, "utf8"))?.match(/^gitdir:\s*(.+)$/m)?.[1]?.trim();
|
|
80
|
+
return target ? path.resolve(path.dirname(dotgit), target) : null;
|
|
81
|
+
};
|
|
82
|
+
const headBranch = (git) => quiet(() => readFileSync(path.join(git, "HEAD"), "utf8"))?.match(/^ref:\s*refs\/heads\/(.+)$/m)?.[1]?.trim() ?? null;
|
|
83
|
+
// Another worktree's branch decides what its spec is: a link could point at a device that never
|
|
84
|
+
// ends, or at a file whose first heading would be copied in as a title. So it is opened without
|
|
85
|
+
// following a link or blocking, checked through the open handle, and read up to a cap.
|
|
86
|
+
const readPlain = (file, within) =>
|
|
87
|
+
quiet(() => {
|
|
88
|
+
// The folder is resolved so a linked directory cannot lead outside that worktree; the spec
|
|
89
|
+
// itself is not, so a spec that is a link is refused below.
|
|
90
|
+
const folder = realpathSync(path.dirname(file));
|
|
91
|
+
if (!(folder + path.sep).startsWith(within + path.sep)) return null;
|
|
92
|
+
const resolved = path.join(folder, path.basename(file));
|
|
93
|
+
const fd = openSync(resolved, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
94
|
+
try {
|
|
95
|
+
const stat = fstatSync(fd);
|
|
96
|
+
if (!stat.isFile() || stat.size > 256 * 1024) return null;
|
|
97
|
+
// Windows has no O_NOFOLLOW: the name must still be a plain file, the one that was opened.
|
|
98
|
+
const named = lstatSync(resolved);
|
|
99
|
+
if (!named.isFile() || named.ino !== stat.ino || named.dev !== stat.dev) return null;
|
|
100
|
+
const buffer = Buffer.alloc(stat.size);
|
|
101
|
+
return buffer.subarray(0, readSync(fd, buffer, 0, stat.size, 0)).toString("utf8");
|
|
102
|
+
} finally {
|
|
103
|
+
closeSync(fd);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const elsewhere = (() => {
|
|
107
|
+
const self = quiet(() => realpathSync(root));
|
|
108
|
+
let home = self;
|
|
109
|
+
let own = null;
|
|
110
|
+
while (home && !(own = gitDir(path.join(home, ".git")))) home = path.dirname(home) === home ? null : path.dirname(home);
|
|
111
|
+
if (!home || !own) return [];
|
|
112
|
+
const commondir = quiet(() => readFileSync(path.join(own, "commondir"), "utf8"));
|
|
113
|
+
const common = commondir ? path.resolve(own, commondir.trim()) : own;
|
|
114
|
+
const records = path.basename(common) === ".git" ? [{ dir: path.dirname(common), git: common }] : [];
|
|
115
|
+
for (const id of quiet(() => readdirSync(path.join(common, "worktrees"))) ?? []) {
|
|
116
|
+
const git = path.join(common, "worktrees", id);
|
|
117
|
+
const target = quiet(() => readFileSync(path.join(git, "gitdir"), "utf8"))?.trim();
|
|
118
|
+
// Git calls a record prunable once the `.git` file it points at is gone.
|
|
119
|
+
if (target && existsSync(path.resolve(git, target))) records.push({ dir: path.dirname(path.resolve(git, target)), git });
|
|
120
|
+
}
|
|
121
|
+
const offset = path.relative(home, self);
|
|
122
|
+
return records.flatMap(({ dir, git }) => {
|
|
123
|
+
const tree = quiet(() => realpathSync(dir));
|
|
124
|
+
if (!tree || tree === home) return [];
|
|
125
|
+
// A wave's step worktree holds a copy of its parent item's spec, not an item of its own.
|
|
126
|
+
if (/^step-\d+$/.test(path.basename(tree)) && existsSync(path.join(path.dirname(tree), "base"))) return [];
|
|
127
|
+
const spec = readPlain(path.join(tree, offset, STATE, "context", "current-work.md"), tree) ?? "";
|
|
128
|
+
if (!spec.trim() || /_Nothing in progress\./.test(spec)) return [];
|
|
129
|
+
const item = spec.match(/^\*\*From build plan:\*\*[ \t]*item[ \t]+([0-9a-z]+)/im)?.[1]?.toLowerCase();
|
|
130
|
+
const name = plain(spec.match(/^#\s+(.+)$/m)?.[1] ?? "Active work");
|
|
131
|
+
// Spaces and tabs only: `\s` crosses newlines, which makes blank lines quadratic.
|
|
132
|
+
const steps = `${(spec.match(/^[ \t]*- \[x\]/gim) ?? []).length} of ${(spec.match(/^[ \t]*- \[[ x]\]/gim) ?? []).length}`;
|
|
133
|
+
return [`- **${name}**${item ? ` (item ${plain(item)})` : ""} on \`${plain(headBranch(git) ?? tree)}\`: ${steps} step(s) done.`];
|
|
134
|
+
});
|
|
135
|
+
})();
|
|
136
|
+
|
|
65
137
|
// Only names the status rule accepts reach this file, which every session loads: no free text
|
|
66
138
|
// from a record any skill can write.
|
|
67
139
|
let run = null;
|
|
@@ -97,6 +169,11 @@ if (active) {
|
|
|
97
169
|
lines.push("Nothing in progress.", "");
|
|
98
170
|
}
|
|
99
171
|
|
|
172
|
+
if (elsewhere.length > 0) {
|
|
173
|
+
const more = elsewhere.length - LISTED;
|
|
174
|
+
lines.push("In flight in other worktrees:", "", ...elsewhere.slice(0, LISTED), ...(more > 0 ? [`- and ${more} more`] : []), "");
|
|
175
|
+
}
|
|
176
|
+
|
|
100
177
|
lines.push(
|
|
101
178
|
`Plan: ${done} of ${done + open} item(s) complete.` + (nextItem ? ` Next up: ${nextItem}` : ""),
|
|
102
179
|
""
|
|
@@ -96,7 +96,7 @@ context; this does. Create it with a `# Findings` heading if it is missing.
|
|
|
96
96
|
One block per finding. The header line is the machine-readable part and keeps this exact
|
|
97
97
|
shape; the prose below it is for people.
|
|
98
98
|
|
|
99
|
-
### F-
|
|
99
|
+
### 12/F-3 [P0] open - Retained auth volumes carry the run label
|
|
100
100
|
|
|
101
101
|
**File:** ops/compose.yaml:86
|
|
102
102
|
**Found:** 2026-09-04 by audit (scope: current; lens: security)
|
|
@@ -105,12 +105,18 @@ shape; the prose below it is for people.
|
|
|
105
105
|
**Resolution:**
|
|
106
106
|
|
|
107
107
|
When the pass is re-reviewing a repair and finds a new problem in the code that repair
|
|
108
|
-
changed, add `**Surfaced by:**
|
|
109
|
-
it
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
changed, add `**Surfaced by:** <identifier>` after **Found**, naming the finding whose repair
|
|
109
|
+
exposed it, in full: `**Surfaced by:** 12/F-3`. That link is what keeps a defect a repair
|
|
110
|
+
introduced attached to the repair.
|
|
111
|
+
|
|
112
|
+
**Identifiers carry the item they were raised in.** While the active spec has a
|
|
113
|
+
`**From build plan:** item <n>` line, a new finding is `<n>/F-<k>`: item 12's first is
|
|
114
|
+
`12/F-1`, and `k` is one past the highest present with that prefix. Two items built in
|
|
115
|
+
separate worktrees then never hand out the same identifier. With no active planned item, a
|
|
116
|
+
finding is bare, `F-<k>`, one past the highest bare identifier present, as entries raised
|
|
117
|
+
before prefixes are. Either way an identifier is never reused and never renumbered while its
|
|
118
|
+
entry lives in the ledger. Completion archives resolved entries under the item's prefix, and
|
|
119
|
+
that prefixed form is the permanent reference.
|
|
114
120
|
|
|
115
121
|
| Status | Meaning | Blocks completion at P0/P1 |
|
|
116
122
|
| --- | --- | --- |
|
|
@@ -93,9 +93,9 @@ Read the spec's type.
|
|
|
93
93
|
|
|
94
94
|
The number lives in the path. It never appears in the commit message.
|
|
95
95
|
|
|
96
|
-
**Record what landed.** After the work commit exists in Step 3,
|
|
97
|
-
|
|
98
|
-
commit that preceded them:
|
|
96
|
+
**Record what landed.** After the work commit exists in Step 3, and after Step 4 has landed
|
|
97
|
+
it on top of anything that landed first, append a `## Landed` section to the archive with the
|
|
98
|
+
exact commits this item produced, oldest first, and the commit that preceded them:
|
|
99
99
|
|
|
100
100
|
## Landed
|
|
101
101
|
|
|
@@ -110,9 +110,10 @@ commits sit among others on the same branch, so this record is the only reliable
|
|
|
110
110
|
|
|
111
111
|
**Prune the ledger.** Append every `closed`, `accepted`, and `invalid` entry to a
|
|
112
112
|
`## Findings` section in the archive just written, at its final status, with the reason
|
|
113
|
-
preserved for accepted entries.
|
|
114
|
-
globally unique
|
|
115
|
-
|
|
113
|
+
preserved for accepted entries. Every archived identifier carries a prefix, so it is
|
|
114
|
+
globally unique, and that prefixed form is the permanent reference. One raised under the
|
|
115
|
+
item's prefix already has it and moves across unchanged: item 12's `12/F-3` stays `12/F-3`.
|
|
116
|
+
Prefix only a bare one, with the archive name: item 12's `F-03` becomes `12/F-03`.
|
|
116
117
|
|
|
117
118
|
Then remove only those entries from the ledger. `open`, `fixed`, and `unverified` entries
|
|
118
119
|
stay, with their identifiers, so nothing is silently dropped. A `fixed` P2 does not block
|
|
@@ -195,6 +196,15 @@ is refused outright when any of those commits has already been pushed.
|
|
|
195
196
|
Under **`git.mode: trunk`** the work is already on the branch you were on. There is nothing
|
|
196
197
|
to merge. Go to Step 5.
|
|
197
198
|
|
|
199
|
+
**Under either branching mode, land on top of what already landed.** Before the squash merge
|
|
200
|
+
or the push, follow `reference/land-on-top.md`. When the branch the item lands in has moved
|
|
201
|
+
since the item began, because another item built in its own worktree landed first, it brings
|
|
202
|
+
that in, takes the landed copy of each of the plan, ledger, journal, overview and handoff that
|
|
203
|
+
the other item changed, and redoes this item's share of them, so those files are never merged
|
|
204
|
+
as text. Any other conflict stops
|
|
205
|
+
for review. When nothing landed in between, it does nothing. Write the `## Landed` record after
|
|
206
|
+
it, as its last section says, so a later rollback reverses this item's work alone.
|
|
207
|
+
|
|
198
208
|
Under **`branch-per-item`**:
|
|
199
209
|
|
|
200
210
|
1. Squash-merge into the default branch, only with an explicit yes, so the item lands as
|
|
@@ -245,6 +255,12 @@ confirm the work commit is actually in it, and only then delete the branch local
|
|
|
245
255
|
the remote. A squash-merge leaves no merge parent, so `git branch -d` will refuse; verify
|
|
246
256
|
the content landed before reaching for `-D`.
|
|
247
257
|
|
|
258
|
+
An item built in its own worktree holds its branch there, so the cleanup runs from the main
|
|
259
|
+
checkout: remove the worktree first, then the branch. Removing the worktree is deleting files,
|
|
260
|
+
so it asks, every time, and only once the work commit is confirmed in the default branch. When
|
|
261
|
+
the state directory is ignored, the item's record is in the worktree alone until the routine's
|
|
262
|
+
last section has copied it to the main checkout; say so when asking.
|
|
263
|
+
|
|
248
264
|
## Step 5 - say how to try it
|
|
249
265
|
|
|
250
266
|
Always end with the manual walkthrough for what just landed: what to start, where to go,
|
|
@@ -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
|
|
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.
|
|
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-
|
|
18
|
-
`/fix F-
|
|
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
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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-
|
|
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.
|