@skitterbyte/skitterspec 22.1.0 → 22.2.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/assets/rules/spec-reports.md +21 -5
- package/assets/skills/no-spec/SKILL.md +3 -1
- package/assets/skills/spec/SKILL.md +3 -1
- package/assets/skills/spec-bug/SKILL.md +3 -2
- package/assets/skills/spec-diff/SKILL.md +3 -1
- package/assets/skills/spec-next/SKILL.md +3 -1
- package/package.json +1 -1
- package/src/cli.js +47 -3
- package/src/env/resolve.js +23 -3
|
@@ -201,6 +201,16 @@ escape hatch is what let a whole class of runs go on asking questions nobody was
|
|
|
201
201
|
listening for. Where a watch is unavailable the wait is the turn ending, which
|
|
202
202
|
every harness can do.
|
|
203
203
|
|
|
204
|
+
**Run it under a persistent watch primitive where the harness has one** — one
|
|
205
|
+
that lives for the whole session and whose deliberate stop is an explicit act
|
|
206
|
+
(Claude Code: the `Monitor` tool with `persistent: true`, ended by `TaskStop`).
|
|
207
|
+
A backgrounded shell is reclaimed on a long idle — that is the death the next
|
|
208
|
+
paragraph recovers from — where a session-length primitive is designed to
|
|
209
|
+
survive it. Where the harness has none, background the command exactly as
|
|
210
|
+
before. The engine's wait stays the one watcher either way: its heartbeat goes
|
|
211
|
+
to stderr, so stdout carries only the start line and the arrival, and the exit
|
|
212
|
+
ends the watch.
|
|
213
|
+
|
|
204
214
|
**A watch that dies is re-armed, silently, on the same window.** A backgrounded
|
|
205
215
|
wait does not always survive a long idle: three sessions lost theirs over one
|
|
206
216
|
lunch break, and two in another were killed at 43 and 30 minutes. So a wait that
|
|
@@ -212,11 +222,17 @@ would strand exactly that pass.
|
|
|
212
222
|
**Say nothing when you do it.** Nothing happened and the window is unchanged; a
|
|
213
223
|
line on every death turns a working loop into a stream of non-events.
|
|
214
224
|
|
|
215
|
-
**An operator who stopped it is not a death
|
|
216
|
-
makes the wait unstoppable, which is worse than a wait that stops.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
225
|
+
**An operator who stopped it is not a death** — re-arming over a deliberate
|
|
226
|
+
stop makes the wait unstoppable, which is worse than a wait that stops. But a
|
|
227
|
+
deliberate stop is an **explicit act with positive evidence**: this session
|
|
228
|
+
called TaskStop, or the operator said to stop waiting. A bare killed-task
|
|
229
|
+
notification carries neither and is read as a **death**. This clause once said
|
|
230
|
+
to treat the ambiguous kill as deliberate, and that safe branch ate the
|
|
231
|
+
recovery whole: every observed kill was the harness reclaiming an idle shell
|
|
232
|
+
and none was a person, so the case it protected against does not occur — while
|
|
233
|
+
the `/spec-reviewed` it charged was paid on every idle review. The deliberate
|
|
234
|
+
stop is the positive signal, so its absence is what a death looks like
|
|
235
|
+
(`.claude/rules/negative-checks.md` rule 1).
|
|
220
236
|
|
|
221
237
|
**It is bounded by the age of the window, not by a count of retries.** Keep
|
|
222
238
|
re-arming while the window is younger than **12 hours**, then stop. A count means
|
|
@@ -113,7 +113,9 @@ because writing it means reading the diff, and that spend is the user's call.
|
|
|
113
113
|
|
|
114
114
|
1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
|
|
115
115
|
the whole scope of what you may claim.
|
|
116
|
-
2. **Run the engine's wait
|
|
116
|
+
2. **Run the engine's wait, and end your turn** — under the harness's
|
|
117
|
+
persistent watch primitive where one exists, else in the background
|
|
118
|
+
(`.claude/rules/spec-reports.md` carries the preference):
|
|
117
119
|
|
|
118
120
|
```
|
|
119
121
|
skitterspec spec-env review wait <name> --since <the timestamp>
|
|
@@ -396,7 +396,9 @@ the verdict — and the reader's conclusion is about what the page showed them.
|
|
|
396
396
|
|
|
397
397
|
1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
|
|
398
398
|
the whole scope of what you may claim.
|
|
399
|
-
2. **Run the engine's wait
|
|
399
|
+
2. **Run the engine's wait, and end your turn** — under the harness's
|
|
400
|
+
persistent watch primitive where one exists, else in the background
|
|
401
|
+
(`.claude/rules/spec-reports.md` carries the preference):
|
|
400
402
|
|
|
401
403
|
```
|
|
402
404
|
skitterspec spec-env review wait <spec> --since <the timestamp>
|
|
@@ -232,8 +232,9 @@ skitterspec spec-env review arm <spec>
|
|
|
232
232
|
|
|
233
233
|
**Then wait**, and `/spec-next` §5 owns the sequence — follow it there rather
|
|
234
234
|
than reading a second copy here: note the moment, run
|
|
235
|
-
`skitterspec spec-env review wait <spec> --since <that moment>`
|
|
236
|
-
|
|
235
|
+
`skitterspec spec-env review wait <spec> --since <that moment>` under the
|
|
236
|
+
harness's persistent watch primitive where one exists (else in the
|
|
237
|
+
background), end your turn, and let `--claim-since` pick the one pass that
|
|
237
238
|
arrived inside the window. **Do not compose a watcher**, and do not give the
|
|
238
239
|
wait a timeout — the reasoning is in `/spec-next` §5 and is not repeated here.
|
|
239
240
|
The banner it describes is what this skill emits in place of a `Review` row,
|
|
@@ -614,7 +614,9 @@ already arrived. Those get the `Review` row, no question, and nothing is owed.
|
|
|
614
614
|
|
|
615
615
|
1. **Note the moment**, as an ISO timestamp, before you start. That instant is
|
|
616
616
|
the entire scope of what may be claimed without a person naming it.
|
|
617
|
-
2. **Run the engine's wait
|
|
617
|
+
2. **Run the engine's wait, and end your turn** — under the harness's
|
|
618
|
+
persistent watch primitive where one exists, else in the background
|
|
619
|
+
(`.claude/rules/spec-reports.md` carries the preference):
|
|
618
620
|
|
|
619
621
|
```
|
|
620
622
|
skitterspec spec-env review wait <spec> --since <timestamp>
|
|
@@ -436,7 +436,9 @@ reading — and that is the gap the whole gate exists to close.
|
|
|
436
436
|
|
|
437
437
|
1. **Note the moment you start waiting**, as an ISO timestamp. That instant is
|
|
438
438
|
the whole scope of what you may claim.
|
|
439
|
-
2. **Run the engine's wait
|
|
439
|
+
2. **Run the engine's wait, and end your turn** — under the harness's
|
|
440
|
+
persistent watch primitive where one exists, else in the background
|
|
441
|
+
(`.claude/rules/spec-reports.md` carries the preference):
|
|
440
442
|
|
|
441
443
|
```
|
|
442
444
|
skitterspec spec-env review wait <spec> --since <the timestamp>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skitterbyte/skitterspec",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.2.0",
|
|
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
|
@@ -856,9 +856,42 @@ function specEnvNospec(dir, config, name, flags = {}) {
|
|
|
856
856
|
}
|
|
857
857
|
|
|
858
858
|
function specEnvUp(dir, config, specArg, flags = {}) {
|
|
859
|
-
const spec = resolveSpecWithWorktree(dir, config, specArg)
|
|
860
859
|
const docs = flags.docs === true
|
|
861
860
|
|
|
861
|
+
// THE AUTHORING LANE — `up <name> --docs` for a spec that is not written yet.
|
|
862
|
+
// `/spec` provisions BEFORE it writes a line (feat-main-is-a-landing-zone
|
|
863
|
+
// phase 2), so the name resolves to nothing at exactly the moment this runs.
|
|
864
|
+
// It opens only on a positive signal, all three parts: the not-found refusal
|
|
865
|
+
// itself (by code, never by message), `--docs`, and a spec-shaped name — so a
|
|
866
|
+
// typo'd `up` without the flag, or with a name no spec could have, refuses
|
|
867
|
+
// exactly as before. Worktree mode only: in checkout mode `/spec` writes
|
|
868
|
+
// where the session stands and there is nothing to provision first.
|
|
869
|
+
let spec
|
|
870
|
+
let authoring = false
|
|
871
|
+
try {
|
|
872
|
+
spec = resolveSpecWithWorktree(dir, config, specArg)
|
|
873
|
+
} catch (err) {
|
|
874
|
+
const specShaped = typeof specArg === 'string' && /^(feat|bug|hotfix)-./.test(specArg)
|
|
875
|
+
if (err.code !== 'SPEC_NOT_FOUND' || !docs || !specShaped || config.mode === 'checkout') {
|
|
876
|
+
throw err
|
|
877
|
+
}
|
|
878
|
+
// Record first, then plan — the same order `nospec` keeps, for the same
|
|
879
|
+
// reason: the record is what makes the name resolvable (a bare `resolve`
|
|
880
|
+
// in the fresh worktree must name this spec before its folder exists), so
|
|
881
|
+
// a caller that runs the printed commands must never own a worktree the
|
|
882
|
+
// engine cannot name. `spec: true` is what makes the record resolve with
|
|
883
|
+
// the spec's own type and slug rather than as `/no-spec` work.
|
|
884
|
+
const beforeAuth = readRegistry(dir, config)
|
|
885
|
+
const resolved = resolveSpecless(specArg, dir, config, { spec: true })
|
|
886
|
+
writeRegistry(
|
|
887
|
+
dir,
|
|
888
|
+
config,
|
|
889
|
+
recordSpecless(beforeAuth, specArg, { branch: resolved.branch, spec: true }),
|
|
890
|
+
)
|
|
891
|
+
spec = resolved
|
|
892
|
+
authoring = true
|
|
893
|
+
}
|
|
894
|
+
|
|
862
895
|
// Checkout mode: the branch is built in the primary checkout, so none of the
|
|
863
896
|
// worktree machinery below applies — no slot, no trust entry and no bootstrap.
|
|
864
897
|
// Handled first precisely so none of that runs by accident.
|
|
@@ -898,8 +931,13 @@ function specEnvUp(dir, config, specArg, flags = {}) {
|
|
|
898
931
|
const upGit = gitReader(dir)
|
|
899
932
|
const upStatus = upGit(['status', '--porcelain'])
|
|
900
933
|
const upDirtyPaths = dirtyPaths(upGit)
|
|
901
|
-
|
|
902
|
-
|
|
934
|
+
// An authoring run does not ask whether the spec is on the fork point — it is
|
|
935
|
+
// known to be nowhere, and that is the healthy state of the lane, not the
|
|
936
|
+
// "would fork without the spec it is for" refusal. `onFork: undefined` is the
|
|
937
|
+
// gate's cannot-tell branch, which allows (negative-checks rule 4). The
|
|
938
|
+
// untracked read is skipped the same way: there is no folder to ask about.
|
|
939
|
+
const upOnFork = authoring ? { onFork: undefined, foundOn: null } : specOnForkPoint(dir, upGit, spec)
|
|
940
|
+
const upSpecUntracked = authoring ? false : specIsUntracked(dir, upGit, spec)
|
|
903
941
|
|
|
904
942
|
// Trust the shared worktree root so edits into the freshly-provisioned worktree
|
|
905
943
|
// don't prompt. One absolute entry (the root) covers every spec; self-heals on
|
|
@@ -988,6 +1026,12 @@ function specEnvUp(dir, config, specArg, flags = {}) {
|
|
|
988
1026
|
if (plan.docs) {
|
|
989
1027
|
out.push(' docs: documents only — no setup commands, no docker')
|
|
990
1028
|
}
|
|
1029
|
+
// Positive, so an operator reading the plan knows the missing spec folder is
|
|
1030
|
+
// the lane working rather than something the engine failed to find.
|
|
1031
|
+
if (authoring) {
|
|
1032
|
+
out.push(' authoring: no spec document yet — /spec writes it in the worktree')
|
|
1033
|
+
out.push(` recorded: added to ${config.registry} until the spec is written`)
|
|
1034
|
+
}
|
|
991
1035
|
if (trust.reason === 'malformed') {
|
|
992
1036
|
out.push(
|
|
993
1037
|
' trusted: ! .claude/settings.local.json is not valid JSON — left it;' +
|
package/src/env/resolve.js
CHANGED
|
@@ -488,17 +488,26 @@ function assertPrimaryOnMain(config, git) {
|
|
|
488
488
|
*/
|
|
489
489
|
function resolveSpecless(name, dir, config, entry = {}) {
|
|
490
490
|
const { repo, repoSlug } = repoInfo(dir)
|
|
491
|
-
|
|
491
|
+
// AN AUTHORING RECORD RESOLVES AS THE SPEC IT WILL BE. `spec: true` marks a
|
|
492
|
+
// record written by `up --docs` for a spec not written yet (the `/spec`
|
|
493
|
+
// authoring lane), so type and slug come from the name's own prefix — the
|
|
494
|
+
// branch and worktree it answers with MUST equal what the written spec will
|
|
495
|
+
// later resolve to, or the confirm step lands in one tree and the documents
|
|
496
|
+
// in another. A plain `/no-spec` record keeps the whole name as its slug,
|
|
497
|
+
// byte-identical to before.
|
|
498
|
+
const authored = entry && entry.spec === true
|
|
499
|
+
const slug = authored ? splitPrefix(name).slug : name
|
|
492
500
|
const tokens = { repo, repoSlug, slug }
|
|
493
501
|
const spec = {
|
|
494
502
|
folder: name,
|
|
495
503
|
bucket: null,
|
|
496
504
|
path: null,
|
|
497
|
-
type: (entry && entry.type) || 'chore',
|
|
505
|
+
type: authored ? splitPrefix(name).type : (entry && entry.type) || 'chore',
|
|
498
506
|
slug,
|
|
499
507
|
stack: 'worktree',
|
|
500
508
|
baseRef: null,
|
|
501
509
|
specless: true,
|
|
510
|
+
unwritten: authored,
|
|
502
511
|
}
|
|
503
512
|
const branch = (entry && entry.branch) || branchFor(spec, config)
|
|
504
513
|
const worktreeRoot = expandTokens(config.worktree.root, tokens)
|
|
@@ -532,10 +541,14 @@ function resolveSpec(specArg, dir, config, opts = {}) {
|
|
|
532
541
|
}
|
|
533
542
|
// Name the roots we looked under: the usual cause is a spec that only exists
|
|
534
543
|
// on its own branch, and the message should say where we didn't find it.
|
|
535
|
-
|
|
544
|
+
// The code is what lets `up --docs` catch exactly this refusal and open the
|
|
545
|
+
// authoring lane — matching on the message would make its wording API.
|
|
546
|
+
const err = new Error(
|
|
536
547
|
`spec not found under specs/**: ${specArg} ` +
|
|
537
548
|
`(searched: ${[...preferDirs, dir, ...searchDirs].join(', ')})`,
|
|
538
549
|
)
|
|
550
|
+
err.code = 'SPEC_NOT_FOUND'
|
|
551
|
+
throw err
|
|
539
552
|
}
|
|
540
553
|
|
|
541
554
|
const { type, slug } = splitPrefix(found.folder)
|
|
@@ -643,7 +656,14 @@ function allSpecs(dir, config, worktreePaths, specless = {}) {
|
|
|
643
656
|
// not. Leaving them out is what would make a `/no-spec` worktree invisible to
|
|
644
657
|
// `status` and to every bare resolution — the outcome decision 7 rejected for
|
|
645
658
|
// a plain git branch, arrived at by omission instead.
|
|
659
|
+
//
|
|
660
|
+
// A FOLDER SHADOWS ITS OWN RECORD. An authoring record (`up --docs` on an
|
|
661
|
+
// unwritten spec) shares its name with the folder `/spec` then writes, and
|
|
662
|
+
// once that folder exists it is the answer — listing the record beside it
|
|
663
|
+
// would show one spec twice.
|
|
664
|
+
const collected = new Set(specs.map((s) => s.folder))
|
|
646
665
|
for (const name of Object.keys(specless).sort()) {
|
|
666
|
+
if (collected.has(name)) continue
|
|
647
667
|
const spec = resolveSpecless(name, dir, config, specless[name])
|
|
648
668
|
specs.push({
|
|
649
669
|
folder: spec.folder,
|