@skitterbyte/skitterspec 22.3.1 → 22.3.2
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/assets/review/page.html +46 -4
- package/assets/skills/spec-bug/SKILL.md +51 -20
- package/assets/skills/spec-hotfix/SKILL.md +68 -36
- package/package.json +1 -1
- package/src/cli.js +88 -8
package/assets/review/page.html
CHANGED
|
@@ -1044,10 +1044,43 @@ a.ticket:hover { border-color: var(--accent); }
|
|
|
1044
1044
|
return bands
|
|
1045
1045
|
}
|
|
1046
1046
|
|
|
1047
|
+
// --- one order, for both columns -----------------------------------------
|
|
1048
|
+
// The sidebar and the panes are two views of one list, and they must be the
|
|
1049
|
+
// SAME list in the same order — a reader scrolling the panes watches the
|
|
1050
|
+
// current-file mark walk the sidebar, and a mark that walks backwards is the
|
|
1051
|
+
// whole reason this exists.
|
|
1052
|
+
//
|
|
1053
|
+
// The emitted order cannot be it. `review.js` collects tracked changes and
|
|
1054
|
+
// untracked files in two passes, so a new file is appended after everything
|
|
1055
|
+
// else however its path sorts; git's own order is not the tree's either.
|
|
1056
|
+
//
|
|
1057
|
+
// So the tree's order is the one order, settled here and used by both. It is
|
|
1058
|
+
// expressed as a comparator rather than derived from the built tree because
|
|
1059
|
+
// the panes are built long before the tree is, and a second traversal that
|
|
1060
|
+
// had to agree with the first is the thing that went wrong.
|
|
1061
|
+
function treeOrder(a, b) {
|
|
1062
|
+
var A = a.split('/')
|
|
1063
|
+
var B = b.split('/')
|
|
1064
|
+
var n = Math.min(A.length, B.length)
|
|
1065
|
+
for (var i = 0; i < n; i++) {
|
|
1066
|
+
// A segment with anything after it is a directory. `renderTree` walks
|
|
1067
|
+
// every directory at a level — label and subtree — before that level's
|
|
1068
|
+
// files, so a directory sorts ahead of a file it ties with by name.
|
|
1069
|
+
var aDir = i < A.length - 1
|
|
1070
|
+
var bDir = i < B.length - 1
|
|
1071
|
+
if (aDir !== bDir) return aDir ? -1 : 1
|
|
1072
|
+
if (A[i] !== B[i]) return A[i].localeCompare(B[i])
|
|
1073
|
+
}
|
|
1074
|
+
return 0
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// A copy: `data` is the emitted review, and the page is a view of it.
|
|
1078
|
+
var files = data.files.slice().sort(function (a, b) { return treeOrder(a.path, b.path) })
|
|
1079
|
+
|
|
1047
1080
|
// --- per-file page state -------------------------------------------------
|
|
1048
1081
|
// Band state lives HERE, never in the emitted data: the page is a view of an
|
|
1049
1082
|
// immutable review, and expanding context is a property of this reading of it.
|
|
1050
|
-
var state =
|
|
1083
|
+
var state = files.map(function (f) {
|
|
1051
1084
|
var rows = parsePatch(f.patch)
|
|
1052
1085
|
var st = {
|
|
1053
1086
|
file: f,
|
|
@@ -1717,7 +1750,16 @@ a.ticket:hover { border-color: var(--accent); }
|
|
|
1717
1750
|
function renderTree(node) {
|
|
1718
1751
|
var ul = el('ul')
|
|
1719
1752
|
;(function walk(n, depth) {
|
|
1720
|
-
|
|
1753
|
+
// `treeOrder` above is the authority and these two sorts must match it,
|
|
1754
|
+
// which is why the directories sort by `localeCompare` rather than by the
|
|
1755
|
+
// default `.sort()`: that compares UTF-16 code units, so it puts every
|
|
1756
|
+
// capitalised name ahead of every lowercase one and the panes would drift
|
|
1757
|
+
// from the sidebar again on the first `README/` in a diff.
|
|
1758
|
+
//
|
|
1759
|
+
// Insertion order would be free — the list arrives sorted — but a key
|
|
1760
|
+
// that looks like an array index (`2024/`) is enumerated first whatever
|
|
1761
|
+
// order it was inserted in, and a dated directory is not exotic.
|
|
1762
|
+
Object.keys(n.dirs).sort(function (a, b) { return a.localeCompare(b) }).forEach(function (name) {
|
|
1721
1763
|
var li = el('li')
|
|
1722
1764
|
li.appendChild(indent(el('span', 'dir', name + '/'), depth, 0.6))
|
|
1723
1765
|
ul.appendChild(li)
|
|
@@ -1750,9 +1792,9 @@ a.ticket:hover { border-color: var(--accent); }
|
|
|
1750
1792
|
}
|
|
1751
1793
|
|
|
1752
1794
|
var tree = document.getElementById('tree')
|
|
1753
|
-
tree.appendChild(renderTree(buildTree(
|
|
1795
|
+
tree.appendChild(renderTree(buildTree(files)))
|
|
1754
1796
|
document.getElementById('tree-summary').textContent =
|
|
1755
|
-
'Files (' +
|
|
1797
|
+
'Files (' + files.length + ')'
|
|
1756
1798
|
if (window.matchMedia && !window.matchMedia('(min-width: 60rem)').matches) {
|
|
1757
1799
|
document.getElementById('tree-wrap').open = false
|
|
1758
1800
|
}
|
|
@@ -47,21 +47,49 @@ bug's **own branch**, never directly on `main`. Provision the worktree **now**,
|
|
|
47
47
|
before the failing test, so the test, the fix, and the spec all land together and
|
|
48
48
|
arrive as one reviewable PR.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
**Provision before you write, not after.** §1 put nothing on disk and the bug's
|
|
51
|
+
name is settled by the end of it, so this is the one moment where a worktree
|
|
52
|
+
costs nothing to obtain and everything — the red test, the fix, the spec — is
|
|
53
|
+
still ahead of it:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
skitterspec spec-env up bug-<name> --docs
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**The spec does not exist yet, and that is the lane.** `up` opens its authoring
|
|
60
|
+
branch on exactly three signals — a spec it could not find, `--docs`, and a name
|
|
61
|
+
shaped `bug-<something>` — records the branch so the name resolves from here on,
|
|
62
|
+
and prints a `git worktree add … -b bug/<name>` forked from `main`. Run what it
|
|
63
|
+
prints, then move this session into the worktree with a plain `cd`; the Bash
|
|
64
|
+
working directory persists between calls, so from here on the test, the fix and
|
|
65
|
+
the spec are all written on the bug's own branch. Confirm the move landed rather
|
|
66
|
+
than reading silence as success (`.claude/rules/negative-checks.md` rule 1):
|
|
67
|
+
`skitterspec spec-env resolve` with no argument must name this spec. If it does
|
|
68
|
+
not, say so and stop rather than writing into a tree you cannot name.
|
|
69
|
+
|
|
70
|
+
**Then run `up` again, without the flag:**
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
skitterspec spec-env up bug-<name>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`--docs` skips the `setup` commands — everything whose only purpose is making a
|
|
77
|
+
tree *runnable* — because provisioning somewhere to write markdown should cost a
|
|
78
|
+
`git worktree add` and nothing else. `/spec` leaves them to `/spec-start`, but
|
|
79
|
+
**there is no `/spec-start` in a bug's life**: §3 runs the project's suite three
|
|
80
|
+
steps from here, in this same tree, and
|
|
81
|
+
**a tree that cannot run the suite cannot go red**.
|
|
82
|
+
The flagless re-run re-attaches the worktree that now exists
|
|
83
|
+
(no second `-b` fork) and prints the bootstrap steps the flag deferred.
|
|
84
|
+
|
|
85
|
+
**Why the stub is no longer seeded on the base branch.** This step used to open
|
|
86
|
+
by writing `specs/in-progress/bug-<name>/00-overview.md` in the primary checkout
|
|
87
|
+
for the engine to resolve, and `main` is where work **lands**, not where it
|
|
88
|
+
happens: where the project installs `.claude/hooks/main-guard.cjs` that write is
|
|
89
|
+
refused, and the skill's documented first instruction becomes the one thing the
|
|
90
|
+
repo will not do. Nothing is seeded and nothing is moved now — the spec is
|
|
91
|
+
written in the worktree, at §4, on the branch it belongs to.
|
|
92
|
+
|
|
65
93
|
- **Bootstrap the worktree.** A fresh worktree has no installed dependencies and
|
|
66
94
|
none of the repo's gitignored files (`.env`, local overrides). Run the printed
|
|
67
95
|
`in the worktree, run:` steps (file seeding, then `setup`) in order, before
|
|
@@ -71,9 +99,10 @@ provision from — you'll flesh it out in §4:
|
|
|
71
99
|
— run `/add-dir <trusted root>` before editing into the worktree, or the first
|
|
72
100
|
edits will prompt.
|
|
73
101
|
- **Do everything below in the worktree**, on the branch — the red test, the fix,
|
|
74
|
-
and the rest of the spec.
|
|
75
|
-
|
|
76
|
-
changes only when the branch
|
|
102
|
+
and the rest of the spec. The `cd` above is what makes that the default rather
|
|
103
|
+
than something to remember; where a step has to reach out of the tree, use an
|
|
104
|
+
absolute path or `git -C <worktreePath>`. `main` changes only when the branch
|
|
105
|
+
merges (at `/spec-complete`).
|
|
77
106
|
|
|
78
107
|
## 3. Write the failing test FIRST (RED) — mandatory
|
|
79
108
|
|
|
@@ -89,8 +118,10 @@ Encode the **correct** (expected) behaviour as a test, then run it and confirm i
|
|
|
89
118
|
|
|
90
119
|
## 4. Write the Bug spec
|
|
91
120
|
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
Write the spec's entry point `00-overview.md`. **When isolated**, you are already
|
|
122
|
+
standing in this spec's own worktree — §2 provisioned it and moved you there, so
|
|
123
|
+
create the folder and write it here, on the bug's branch. Nothing was seeded
|
|
124
|
+
ahead of it and nothing is moved across.
|
|
94
125
|
**When not isolated**, create the spec **folder**
|
|
95
126
|
`specs/in-progress/bug-<kebab-name>/` with its entry point `00-overview.md` now
|
|
96
127
|
(every spec is a folder — never a bare file). A bug is
|
|
@@ -55,39 +55,69 @@ Hotfixes are concrete — confirm, don't over-grill. Establish:
|
|
|
55
55
|
- **Root cause:** read the code **at the base tag** and trace it to `file:line`.
|
|
56
56
|
The fix belongs on the tag's line, so reason about that code, not `main`'s.
|
|
57
57
|
|
|
58
|
-
## 3.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
58
|
+
## 3. Provision the worktree from the tag, then write in it
|
|
59
|
+
|
|
60
|
+
**Provision before you write, not after.** §1 settled the tag and the name and
|
|
61
|
+
§2 put nothing on disk, so this is the one moment where a worktree costs nothing
|
|
62
|
+
to obtain and everything — the red test, the fix, the spec — is still ahead of
|
|
63
|
+
it:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
skitterspec spec-env up hotfix-<name> --docs --from <tag>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**`--from` is how the tag reaches the engine, and it is not the header twice.**
|
|
70
|
+
Everywhere else `up` forks a hotfix from the spec's own
|
|
71
|
+
`> **Base version:**` line — but here the spec that would carry it does not
|
|
72
|
+
exist yet, which is the whole of what this lane is for. Without the flag the
|
|
73
|
+
branch would fork from `main`: the fix gets written against code prod is not
|
|
74
|
+
running, while the tag and the cherry-pick downstream go on believing it is on
|
|
75
|
+
the release line, and nothing says otherwise until someone asks why the patch
|
|
76
|
+
did not take. The engine refuses a ref git does not know rather than forking
|
|
77
|
+
from somewhere else quietly; where git cannot answer at all it passes the ref
|
|
78
|
+
through to `git worktree add`, which refuses far more loudly than a planner
|
|
79
|
+
guessing from an absence.
|
|
80
|
+
|
|
81
|
+
Run what it prints — the `git worktree add … -b hotfix/<slug> <tag>` is checked
|
|
82
|
+
out **at the tag**, not at `main` — then move this session into the worktree
|
|
83
|
+
with a plain `cd`; the Bash working directory persists between calls, so from
|
|
84
|
+
here on the test, the fix and the spec are all written on the hotfix's own
|
|
85
|
+
branch. Confirm the move landed rather than reading silence as success
|
|
86
|
+
(`.claude/rules/negative-checks.md` rule 1): `skitterspec spec-env resolve` with
|
|
87
|
+
no argument must name this spec. If it does not, say so and stop rather than
|
|
88
|
+
writing into a tree you cannot name.
|
|
89
|
+
|
|
90
|
+
**Then run `up` again, without the flags:**
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
skitterspec spec-env up hotfix-<name>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`--docs` skips the `setup` commands — everything whose only purpose is making a
|
|
97
|
+
tree *runnable* — because provisioning somewhere to write markdown should cost a
|
|
98
|
+
`git worktree add` and nothing else. A hotfix has no `/spec-start` to leave them
|
|
99
|
+
to: §4 runs the project's suite in this same tree, and
|
|
100
|
+
**a tree that cannot run the suite cannot go red**.
|
|
101
|
+
The flagless re-run re-attaches the worktree that now exists (no second `-b`
|
|
102
|
+
fork, and no fork point — the branch is already on the tag's line) and prints
|
|
103
|
+
the bootstrap steps the flag deferred.
|
|
104
|
+
|
|
105
|
+
**Why nothing is seeded on the base branch, and nothing is moved.** This step
|
|
106
|
+
used to write `specs/in-progress/hotfix-<name>/00-overview.md` in the primary
|
|
107
|
+
checkout for the engine to resolve, and then `mv` the folder across into a
|
|
108
|
+
worktree checked out at a tag — a move with a hazard paragraph of its own, since
|
|
109
|
+
`specs/in-progress/` is usually absent at an old release tag. Both halves are
|
|
110
|
+
gone. `main` is where work **lands**, not where it happens: where the project
|
|
111
|
+
installs `.claude/hooks/main-guard.cjs` that first write is refused outright,
|
|
112
|
+
and a step whose documented opening the repo will not perform is not a step. The
|
|
113
|
+
spec is written in the worktree at §5, on the branch it belongs to.
|
|
114
|
+
|
|
115
|
+
**§5 records the same tag you passed to `--from`**, on the spec's
|
|
116
|
+
`> **Base version:**` header. That is not bookkeeping: `/spec-complete` tags the
|
|
117
|
+
new patch and cherry-picks off the header, so a worktree forked from one ref and
|
|
118
|
+
a header naming another is this whole bug again with the halves swapped.
|
|
119
|
+
|
|
120
|
+
- **Bootstrap the worktree.** A fresh worktree has no installed dependencies and
|
|
91
121
|
none of the repo's gitignored files (`.env`, local overrides). Run the printed
|
|
92
122
|
`in the worktree, run:` steps (file seeding, then `setup`) in order, before
|
|
93
123
|
anything else.
|
|
@@ -96,8 +126,9 @@ that header — must exist **before** `spec-env up`:
|
|
|
96
126
|
— run `/add-dir <trusted root>` before editing into the worktree, or the first
|
|
97
127
|
edits will prompt.
|
|
98
128
|
- **Do everything below in the worktree**, on the `hotfix/<slug>` branch — the red
|
|
99
|
-
test, the fix, and the rest of the spec.
|
|
100
|
-
|
|
129
|
+
test, the fix, and the rest of the spec. The `cd` above is what makes that the
|
|
130
|
+
default rather than something to remember; where a step has to reach out of
|
|
131
|
+
the tree, use an absolute path or `git -C <worktreePath>`. `main` changes
|
|
101
132
|
only at `/spec-complete` (via cherry-pick, not merge).
|
|
102
133
|
|
|
103
134
|
## 4. Write the failing test FIRST (RED) — mandatory
|
|
@@ -114,7 +145,8 @@ Encode the **correct** (expected) behaviour as a test, then run it and confirm i
|
|
|
114
145
|
|
|
115
146
|
## 5. Write the Hotfix spec
|
|
116
147
|
|
|
117
|
-
|
|
148
|
+
Write `00-overview.md` in the worktree — §3 provisioned it at the tag and moved
|
|
149
|
+
you there, so create the folder and write it here. A hotfix
|
|
118
150
|
is usually a single-pass fix, so the `## Fix` block can live directly in
|
|
119
151
|
`00-overview.md`. Keep it lean:
|
|
120
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec",
|
|
3
|
-
"version": "22.3.
|
|
3
|
+
"version": "22.3.2",
|
|
4
4
|
"description": "Spec-driven development for Claude Code \u2014 a tracker-free filesystem workflow: lifecycle skills and per-spec isolation. For Linear sync, install @skitterbyte/skitterspec-linear instead.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/src/cli.js
CHANGED
|
@@ -863,6 +863,9 @@ function specEnvUp(dir, config, specArg, flags = {}) {
|
|
|
863
863
|
// THE AUTHORING LANE — `up <name> --docs` for a spec that is not written yet.
|
|
864
864
|
// `/spec` provisions BEFORE it writes a line (feat-main-is-a-landing-zone
|
|
865
865
|
// phase 2), so the name resolves to nothing at exactly the moment this runs.
|
|
866
|
+
// `/spec-bug` takes the same lane and then re-runs `up` WITHOUT the flag for
|
|
867
|
+
// the `setup` commands it skipped — it has no `/spec-start` to defer them to,
|
|
868
|
+
// and its next step runs the suite in that tree.
|
|
866
869
|
// It opens only on a positive signal, all three parts: the not-found refusal
|
|
867
870
|
// itself (by code, never by message), `--docs`, and a spec-shaped name — so a
|
|
868
871
|
// typo'd `up` without the flag, or with a name no spec could have, refuses
|
|
@@ -883,6 +886,19 @@ function specEnvUp(dir, config, specArg, flags = {}) {
|
|
|
883
886
|
// a caller that runs the printed commands must never own a worktree the
|
|
884
887
|
// engine cannot name. `spec: true` is what makes the record resolve with
|
|
885
888
|
// the spec's own type and slug rather than as `/no-spec` work.
|
|
889
|
+
// `--from <ref>` is the fork point for a spec whose own header cannot yet
|
|
890
|
+
// supply one. `/spec-hotfix` is the caller: its worktree belongs on the
|
|
891
|
+
// released line, and `> **Base version:**` is written INSIDE that tree.
|
|
892
|
+
// Everywhere else `spec.baseRef` comes off the spec and stays the
|
|
893
|
+
// authority — which is why passing this to a spec that resolves refuses
|
|
894
|
+
// below rather than quietly losing to the header.
|
|
895
|
+
if (flags.from !== undefined) {
|
|
896
|
+
const bad = badForkRef(dir, flags.from)
|
|
897
|
+
if (bad) {
|
|
898
|
+
process.stdout.write(bad)
|
|
899
|
+
return
|
|
900
|
+
}
|
|
901
|
+
}
|
|
886
902
|
const beforeAuth = readRegistry(dir, config)
|
|
887
903
|
const resolved = resolveSpecless(specArg, dir, config, { spec: true })
|
|
888
904
|
writeRegistry(
|
|
@@ -891,9 +907,23 @@ function specEnvUp(dir, config, specArg, flags = {}) {
|
|
|
891
907
|
recordSpecless(beforeAuth, specArg, { branch: resolved.branch, spec: true }),
|
|
892
908
|
)
|
|
893
909
|
spec = resolved
|
|
910
|
+
if (flags.from !== undefined) spec.baseRef = flags.from
|
|
894
911
|
authoring = true
|
|
895
912
|
}
|
|
896
913
|
|
|
914
|
+
// A flag that cannot be honoured is refused, never ignored. The operator
|
|
915
|
+
// typed a fork point; silently forking from base instead would build a hotfix
|
|
916
|
+
// against `main`'s code while everything downstream believes it is on the
|
|
917
|
+
// release line — the exact silent failure this flag exists to prevent.
|
|
918
|
+
if (flags.from !== undefined && !authoring) {
|
|
919
|
+
process.stdout.write(
|
|
920
|
+
`spec-env up: --from applies to a spec that is not written yet. ` +
|
|
921
|
+
`${specArg} already exists, and its own \`> **Base version:**\` header is ` +
|
|
922
|
+
'the fork point — edit that instead.\n',
|
|
923
|
+
)
|
|
924
|
+
return
|
|
925
|
+
}
|
|
926
|
+
|
|
897
927
|
// Checkout mode: the branch is built in the primary checkout, so none of the
|
|
898
928
|
// worktree machinery below applies — no slot, no trust entry and no bootstrap.
|
|
899
929
|
// Handled first precisely so none of that runs by accident.
|
|
@@ -933,13 +963,24 @@ function specEnvUp(dir, config, specArg, flags = {}) {
|
|
|
933
963
|
const upGit = gitReader(dir)
|
|
934
964
|
const upStatus = upGit(['status', '--porcelain'])
|
|
935
965
|
const upDirtyPaths = dirtyPaths(upGit)
|
|
936
|
-
//
|
|
937
|
-
// known to be nowhere, and that is the healthy state of
|
|
938
|
-
// "would fork without the spec it is for"
|
|
939
|
-
// gate's cannot-tell branch, which allows
|
|
940
|
-
// untracked read is skipped the same way: there
|
|
941
|
-
|
|
942
|
-
|
|
966
|
+
// A spec with no document on disk does not ask whether the spec is on the
|
|
967
|
+
// fork point — it is known to be nowhere, and that is the healthy state of
|
|
968
|
+
// the authoring lane, not the "would fork without the spec it is for"
|
|
969
|
+
// refusal. `onFork: undefined` is the gate's cannot-tell branch, which allows
|
|
970
|
+
// (negative-checks rule 4). The untracked read is skipped the same way: there
|
|
971
|
+
// is no folder to ask about.
|
|
972
|
+
//
|
|
973
|
+
// NOT `authoring`, which is only the run that OPENED the lane. The test-first
|
|
974
|
+
// skills re-run `up` without `--docs` for the setup commands before the spec
|
|
975
|
+
// is written, and that run resolves — off the registry record the lane left —
|
|
976
|
+
// so `authoring` is false while `spec.path` is still null. Asking git about a
|
|
977
|
+
// null path threw, which is a crash rather than a refusal: the condition is
|
|
978
|
+
// whether there is a document, and it always was.
|
|
979
|
+
const noDocument = authoring || !spec.path
|
|
980
|
+
const upOnFork = noDocument
|
|
981
|
+
? { onFork: undefined, foundOn: null }
|
|
982
|
+
: specOnForkPoint(dir, upGit, spec)
|
|
983
|
+
const upSpecUntracked = noDocument ? false : specIsUntracked(dir, upGit, spec)
|
|
943
984
|
|
|
944
985
|
// Trust the shared worktree root so edits into the freshly-provisioned worktree
|
|
945
986
|
// don't prompt. One absolute entry (the root) covers every spec; self-heals on
|
|
@@ -1031,9 +1072,15 @@ function specEnvUp(dir, config, specArg, flags = {}) {
|
|
|
1031
1072
|
// Positive, so an operator reading the plan knows the missing spec folder is
|
|
1032
1073
|
// the lane working rather than something the engine failed to find.
|
|
1033
1074
|
if (authoring) {
|
|
1034
|
-
out.push(' authoring: no spec document yet —
|
|
1075
|
+
out.push(' authoring: no spec document yet — the skill writes it in the worktree')
|
|
1035
1076
|
out.push(` recorded: added to ${config.registry} until the spec is written`)
|
|
1036
1077
|
}
|
|
1078
|
+
// Only where one was NAMED. The fork point is otherwise base HEAD, which is
|
|
1079
|
+
// the unremarkable case, and a line asserting it on every provision would
|
|
1080
|
+
// make the one run that forked from a tag look like every other.
|
|
1081
|
+
if (authoring && flags.from !== undefined) {
|
|
1082
|
+
out.push(` fork: ${flags.from} (the branch forks from here, not from base)`)
|
|
1083
|
+
}
|
|
1037
1084
|
if (trust.reason === 'malformed') {
|
|
1038
1085
|
out.push(
|
|
1039
1086
|
' trusted: ! .claude/settings.local.json is not valid JSON — left it;' +
|
|
@@ -1084,6 +1131,38 @@ function speclessMap(dir, config) {
|
|
|
1084
1131
|
}
|
|
1085
1132
|
|
|
1086
1133
|
// A read-only git reader over `cwd`: returns trimmed stdout, or null on failure.
|
|
1134
|
+
/**
|
|
1135
|
+
* Is `ref` a fork point git will accept? Returns a refusal string, or null.
|
|
1136
|
+
*
|
|
1137
|
+
* THIS IS AN ACCUSATION, so it fires only on a positive signal
|
|
1138
|
+
* (`.claude/rules/negative-checks.md` rule 1). `rev-parse --verify` returning
|
|
1139
|
+
* nothing means one of two very different things — the ref does not exist, or
|
|
1140
|
+
* git could not answer at all — and only the first is evidence. So the control
|
|
1141
|
+
* question is asked first: if git cannot even name its own directory, the ref
|
|
1142
|
+
* is passed straight through and `git worktree add` refuses it far more loudly
|
|
1143
|
+
* than a planner guessing from an absence ever could (rule 4).
|
|
1144
|
+
*
|
|
1145
|
+
* WHAT WOULD FOOL THIS: a ref that exists but is not a commit — an annotated
|
|
1146
|
+
* tag object is peeled by `^{commit}`, so that case is covered, but a blob or
|
|
1147
|
+
* tree ref would be rejected here and rejected by `git worktree add` too. Being
|
|
1148
|
+
* wrong that way costs a refusal on something that could never have worked.
|
|
1149
|
+
*/
|
|
1150
|
+
function badForkRef(cwd, ref) {
|
|
1151
|
+
const git = gitReader(cwd)
|
|
1152
|
+
if (!ref) {
|
|
1153
|
+
return 'spec-env up: --from needs a ref — the tag or commit to fork from.\n'
|
|
1154
|
+
}
|
|
1155
|
+
// The control question. No repo, no git, an unreadable directory — all land
|
|
1156
|
+
// here, and none of them is evidence about the ref.
|
|
1157
|
+
if (git(['rev-parse', '--git-dir']) === null) return null
|
|
1158
|
+
if (git(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]) !== null) return null
|
|
1159
|
+
return (
|
|
1160
|
+
`spec-env up: --from ${ref} — git does not know that ref here, so the ` +
|
|
1161
|
+
'worktree would fork from somewhere else without saying so.\n' +
|
|
1162
|
+
' check the tag exists (git tag --list), or fetch it first.\n'
|
|
1163
|
+
)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1087
1166
|
function gitReader(cwd) {
|
|
1088
1167
|
return (argv) => {
|
|
1089
1168
|
try {
|
|
@@ -5016,6 +5095,7 @@ async function specEnv(rest) {
|
|
|
5016
5095
|
else if (args[i] === '--phase') flags.phase = args[++i]
|
|
5017
5096
|
else if (args[i] === '--record-primary') flags.recordPrimary = true
|
|
5018
5097
|
else if (args[i] === '--docs') flags.docs = true
|
|
5098
|
+
else if (args[i] === '--from') flags.from = args[++i]
|
|
5019
5099
|
else if (args[i] === '--session') flags.session = args[++i]
|
|
5020
5100
|
else if (args[i] === '--assert-primary-clean') flags.assertPrimaryClean = true
|
|
5021
5101
|
else positional.push(args[i])
|