@skitterbyte/skitterspec 21.0.0 → 22.0.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/MIGRATION.md +218 -0
- package/README.md +113 -6
- package/assets/claude-md-section.md +29 -18
- package/assets/commands/spec-remote-review.md +22 -0
- package/assets/core/env.config.json.example +4 -2
- package/assets/core/env.config.md +100 -23
- package/assets/review/page.html +1044 -101
- package/assets/rules/spec-planning.md +35 -3
- package/assets/rules/spec-reports.md +131 -20
- package/assets/skills/spec/SKILL.md +129 -1
- package/assets/skills/spec-bug/SKILL.md +29 -24
- package/assets/skills/spec-diff/SKILL.md +131 -36
- package/assets/skills/spec-hotfix/SKILL.md +22 -19
- package/assets/skills/spec-next/SKILL.md +119 -56
- package/assets/skills/spec-review/SKILL.md +87 -0
- package/assets/skills/spec-reviewed/SKILL.md +31 -5
- package/assets/skills/spec-start/SKILL.md +19 -0
- package/package.json +1 -1
- package/src/cli.js +913 -116
- package/src/env/classify.js +87 -2
- package/src/env/config.js +214 -17
- package/src/env/live.js +94 -0
- package/src/env/resolve.js +36 -2
- package/src/env/review.js +542 -21
- package/src/env/serve.js +298 -19
- package/src/env/supervise.js +8 -1
- package/src/init.js +60 -9
package/src/env/review.js
CHANGED
|
@@ -44,6 +44,24 @@ function isNoise(relPath) {
|
|
|
44
44
|
return relPath.startsWith('specs/') || relPath.startsWith('.spec-env/')
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Is this path one of `folder`'s own spec documents, in any bucket?
|
|
49
|
+
*
|
|
50
|
+
* Every bucket, for the same reason `classifyDirtyTree` checks every bucket: a
|
|
51
|
+
* tree mid-`git mv` is dirty in two of them at once and both halves are the
|
|
52
|
+
* same spec's.
|
|
53
|
+
*
|
|
54
|
+
* It exists because `isNoise` above is exactly wrong for the `docs` mode. That
|
|
55
|
+
* rule — everything under `specs/` is bookkeeping — is right for a phase's code
|
|
56
|
+
* diff, where the spec's own checkbox edits are not what anyone came to read.
|
|
57
|
+
* On a page whose SUBJECT is the spec, applying it folds away every file and
|
|
58
|
+
* renders a page with nothing open on it.
|
|
59
|
+
*/
|
|
60
|
+
function isSpecDocOf(relPath, folder) {
|
|
61
|
+
const parts = String(relPath).split('/')
|
|
62
|
+
return parts.length > 3 && parts[0] === 'specs' && parts[2] === folder
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
/**
|
|
48
66
|
* A git runner bound to one checkout, returning stdout **untrimmed**.
|
|
49
67
|
*
|
|
@@ -174,12 +192,21 @@ function numstatFor(git, ref, file, untracked) {
|
|
|
174
192
|
* `remote` — somewhere else, so it does not.
|
|
175
193
|
* `unknown` — CANNOT TELL, and that is a real answer rather than a soft `local`.
|
|
176
194
|
*
|
|
177
|
-
* **This decides
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* behaviour
|
|
195
|
+
* **This decides two things: the WORDING, and WHAT THE SERVER BINDS TO.** It
|
|
196
|
+
* does not decide *whether* to serve — `review.serve` does — and it never
|
|
197
|
+
* decides to publish. Being wrong therefore stays cheap in both directions: a
|
|
198
|
+
* wrong `local` binds loopback, which is reachable from the machine holding the
|
|
199
|
+
* page and says so; a wrong `remote` binds every interface, which is the
|
|
200
|
+
* existing behaviour and is announced on the render.
|
|
201
|
+
*
|
|
202
|
+
* IT DID DECIDE WHETHER TO SERVE, ONCE, and this comment claimed otherwise
|
|
203
|
+
* throughout — the sentence above used to read "nothing in the engine serves on
|
|
204
|
+
* the strength of it" while `cli.js` served only for a `remote` reader. The bill
|
|
205
|
+
* was a `file://` link on a local machine, and a `file://` page has no server to
|
|
206
|
+
* POST to, so the verdict buttons on it had nowhere to go: the loop the page
|
|
207
|
+
* exists to close was absent from the sessions easiest to use. If you are here
|
|
208
|
+
* wondering whether to make detection load-bearing again, that is what it cost
|
|
209
|
+
* the last time, and `env-serve-start-proof` now fails if you do.
|
|
183
210
|
*
|
|
184
211
|
* `env` is passed in, never read from `process` here, so a test states the world
|
|
185
212
|
* it is testing instead of inheriting the machine the suite happens to run on.
|
|
@@ -234,17 +261,44 @@ function resolveReader(config, env = {}) {
|
|
|
234
261
|
* `buttons` is the button set the page renders — see `BUTTON_SETS`. It is the
|
|
235
262
|
* caller's declaration about the work, not a reading of the gate.
|
|
236
263
|
*/
|
|
237
|
-
function collectReview({ spec, git, mode = 'working', ref, base = null, now, notes = null, gate = null, fellBack = false, buttons = null }) {
|
|
264
|
+
function collectReview({ spec, git, mode = 'working', ref, base = null, now, notes = null, gate = null, fellBack = false, buttons = null, only = null, treePath = null, live = null, tiers = null }) {
|
|
265
|
+
// WHICH TREE THE SPEC'S OWN DOCUMENTS ARE READ FROM. Every render but one
|
|
266
|
+
// reads the spec's worktree, and for those the two are the same path — so
|
|
267
|
+
// `treePath` left null keeps the existing behaviour exactly. The `docs` mode
|
|
268
|
+
// has no worktree to read, which is the whole reason it exists.
|
|
269
|
+
const root = treePath || spec.worktreePath
|
|
270
|
+
|
|
271
|
+
// AN EXPLICIT FILE SET, or every changed file. `only` is a whitelist of
|
|
272
|
+
// repo-relative paths, and it exists because the `docs` mode reads a tree
|
|
273
|
+
// several sessions write into: rendering everything uncommitted there would
|
|
274
|
+
// put another spec's documents on this page and then commit them under this
|
|
275
|
+
// page's verdict. Null — every other caller — filters nothing.
|
|
276
|
+
const wanted = only ? new Set(only) : null
|
|
277
|
+
const keep = (f) => !wanted || wanted.has(f.path)
|
|
278
|
+
|
|
279
|
+
// WHAT COUNTS AS BOOKKEEPING, and it inverts for the `docs` mode. Everywhere
|
|
280
|
+
// else the spec's own documents are the bookkeeping beside the code; on a
|
|
281
|
+
// docs page they are the code, and the companions the project declared (a
|
|
282
|
+
// tracker snapshot, say) are what belongs folded away.
|
|
283
|
+
const noiseOf = (p) => (mode === 'docs' ? !isSpecDocOf(p, spec.folder) : isNoise(p))
|
|
284
|
+
|
|
285
|
+
// THE SURFACES BLOCK, built here so the page never has to know which tiers
|
|
286
|
+
// exist or which of them can be turned on. `null` when the caller passed
|
|
287
|
+
// neither a live state nor a stack — an absent key, not an empty one.
|
|
288
|
+
const surfaces = surfacesFor({ live, tiers })
|
|
289
|
+
|
|
238
290
|
const files = []
|
|
239
291
|
for (const f of trackedFiles(git, ref)) {
|
|
292
|
+
if (!keep(f)) continue
|
|
240
293
|
const { patch, whole } = patchFor(git, ref, f, false)
|
|
241
294
|
const { additions, deletions, binary } = numstatFor(git, ref, f, false)
|
|
242
|
-
files.push({ ...f, additions, deletions, binary, whole, noise:
|
|
295
|
+
files.push({ ...f, additions, deletions, binary, whole, noise: noiseOf(f.path), patch })
|
|
243
296
|
}
|
|
244
297
|
for (const f of untrackedFiles(git)) {
|
|
298
|
+
if (!keep(f)) continue
|
|
245
299
|
const { patch, whole } = patchFor(git, ref, f, true)
|
|
246
300
|
const { additions, deletions, binary } = numstatFor(git, ref, f, true)
|
|
247
|
-
files.push({ ...f, additions, deletions, binary, whole, noise:
|
|
301
|
+
files.push({ ...f, additions, deletions, binary, whole, noise: noiseOf(f.path), patch })
|
|
248
302
|
}
|
|
249
303
|
|
|
250
304
|
// Content hashes and the stored review state, folded on before the totals so
|
|
@@ -261,7 +315,7 @@ function collectReview({ spec, git, mode = 'working', ref, base = null, now, not
|
|
|
261
315
|
// Every bucket, because a page is rendered for specs in `in-progress/` and for
|
|
262
316
|
// finished ones in `complete/` — and the finished one is the case this exists
|
|
263
317
|
// for. `null` when it cannot tell, and the page leaves its button alone.
|
|
264
|
-
const specDir =
|
|
318
|
+
const specDir = root ? findSpecDirIn(root, spec.folder) : null
|
|
265
319
|
const phases = specDir ? readPhases(specDir) : null
|
|
266
320
|
// The PR description this page never had: why the change exists, what it
|
|
267
321
|
// touches, and what this phase set out to do.
|
|
@@ -280,7 +334,11 @@ function collectReview({ spec, git, mode = 'working', ref, base = null, now, not
|
|
|
280
334
|
spec: spec.folder,
|
|
281
335
|
title: spec.folder,
|
|
282
336
|
branch: spec.branch,
|
|
283
|
-
worktree
|
|
337
|
+
// The tree the diff was read from. Identical to the worktree for every
|
|
338
|
+
// mode but `docs`, where there is no worktree and this is the checkout the
|
|
339
|
+
// documents actually live in — saying `worktree` there would name a path
|
|
340
|
+
// that does not exist.
|
|
341
|
+
worktree: root,
|
|
284
342
|
mode,
|
|
285
343
|
base,
|
|
286
344
|
ref,
|
|
@@ -304,6 +362,27 @@ function collectReview({ spec, git, mode = 'working', ref, base = null, now, not
|
|
|
304
362
|
// Same rule again: a gate that was never armed and never skipped adds no
|
|
305
363
|
// key at all.
|
|
306
364
|
...(gateForPage(gate) ? { gate: gateForPage(gate) } : {}),
|
|
365
|
+
// THE PATHS A COMMITTING VERDICT HERE MUST COMMIT, carried on the payload
|
|
366
|
+
// so the skill that routes on the verdict never recomputes them. It matters
|
|
367
|
+
// that they travel with the page rather than being asked for again: a
|
|
368
|
+
// checkout is shared, so the set of this spec's uncommitted documents can
|
|
369
|
+
// differ between the render and the verdict — and the reader's conclusion is
|
|
370
|
+
// about what the page showed them.
|
|
371
|
+
//
|
|
372
|
+
// Absent for every other mode, which is what keeps their payloads identical.
|
|
373
|
+
...(only ? { docs: { paths: only } } : {}),
|
|
374
|
+
// WHERE ELSE THIS REVIEW CAN BE REACHED, and whether it is also running.
|
|
375
|
+
//
|
|
376
|
+
// The page had no idea the other tiers existed — it is opened AT one URL
|
|
377
|
+
// and knows nothing about the rest — so an enable press needs somewhere to
|
|
378
|
+
// live, and that is this block. It carries only what the reader can act on:
|
|
379
|
+
// the live state, each tier that is OFF with the action that turns it on,
|
|
380
|
+
// and the `network` URL when it is on, which is the one address a reader
|
|
381
|
+
// wants that is not the one they are standing on.
|
|
382
|
+
//
|
|
383
|
+
// ABSENT STAYS ABSENT, exactly as `phases` and `gate` do: a caller that
|
|
384
|
+
// passes neither renders the payload it rendered before this existed.
|
|
385
|
+
...(surfaces ? { surfaces } : {}),
|
|
307
386
|
// THE DEFAULT ADDS NO KEY, so a caller that did not ask for a button set —
|
|
308
387
|
// and a caller that asked for the default by name — renders the payload it
|
|
309
388
|
// rendered before this existed. Opting in is the only thing that shows.
|
|
@@ -401,7 +480,15 @@ const NOTES_VERSION = 1
|
|
|
401
480
|
* THE VERDICT NAMES THE ACTION. It was `approve` once, and an approval that
|
|
402
481
|
* only recorded itself is the one thing on a review page that does not describe
|
|
403
482
|
* what happens — a review is the guard in front of an action, so the word is
|
|
404
|
-
* the action: `commit`, `commit-continue`, `
|
|
483
|
+
* the action: `commit`, `commit-continue`, `commit-start`, `changes`,
|
|
484
|
+
* `discuss`.
|
|
485
|
+
*
|
|
486
|
+
* `commit-start` is the authoring verdict: commit the spec that was just
|
|
487
|
+
* written, then put it in flight. It is its own word rather than a
|
|
488
|
+
* context-dependent reading of `commit-continue` for exactly the reason above —
|
|
489
|
+
* a verdict that means `/spec-start` on one page and `/spec-next` on another
|
|
490
|
+
* names neither, and the outcome log would record the same word for two
|
|
491
|
+
* different actions with no way to tell them apart afterwards.
|
|
405
492
|
*
|
|
406
493
|
* `discuss` is the default because it is the behaviour that existed before any
|
|
407
494
|
* verdict did. So a blob from an older page, or one a reader sent without
|
|
@@ -414,9 +501,195 @@ const NOTES_VERSION = 1
|
|
|
414
501
|
* below and is therefore structurally incapable of clearing an armed gate: a
|
|
415
502
|
* phase that ended still owes a committing verdict or a recorded skip.
|
|
416
503
|
*/
|
|
417
|
-
const VERDICTS = ['commit', 'commit-continue', 'continue', 'changes', 'discuss']
|
|
504
|
+
const VERDICTS = ['commit', 'commit-continue', 'commit-start', 'continue', 'changes', 'discuss']
|
|
418
505
|
const DEFAULT_VERDICT = 'discuss'
|
|
419
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Actions the page can send INSTEAD of a verdict.
|
|
509
|
+
*
|
|
510
|
+
* AN ACTION IS NOT A VERDICT, and keeping the two vocabularies apart is the
|
|
511
|
+
* whole point rather than tidiness. A verdict is a conclusion about the work,
|
|
512
|
+
* consumed once by the thing it asked for. An action changes what is *running*
|
|
513
|
+
* — or which tiers are permitted — and then puts the reader back on the same
|
|
514
|
+
* page with the same options, so a run answering one has concluded nothing.
|
|
515
|
+
*
|
|
516
|
+
* THERE IS NO `live-off`, and its absence is the decision. Putting *this* change
|
|
517
|
+
* live is about the diff on screen; handing the whole instance back to `main` is
|
|
518
|
+
* a workspace decision with nothing to do with this review, and a control for it
|
|
519
|
+
* on a review page invites the reader to make it while thinking about something
|
|
520
|
+
* else. The `live:` line names `/spec-live main` instead, which is what a person
|
|
521
|
+
* types.
|
|
522
|
+
*
|
|
523
|
+
* Three consequences follow from that, and all three are structural rather than
|
|
524
|
+
* remembered:
|
|
525
|
+
*
|
|
526
|
+
* - It is absent from `VERDICTS`, so nothing that routes on a verdict can see
|
|
527
|
+
* it.
|
|
528
|
+
* - It is absent from `COMMITTING`, so it is **incapable** of clearing an armed
|
|
529
|
+
* gate: a phase that ended still owes a verdict after the reader has looked
|
|
530
|
+
* at it running. The commit that a `live-on` performs first is the mechanical
|
|
531
|
+
* precondition for `live take` (which refuses a dirty worktree), not the
|
|
532
|
+
* reader's answer.
|
|
533
|
+
* - A blob may carry a verdict or an action, never both. Two answers in one
|
|
534
|
+
* pass would need a rule about which wins, and any such rule would sometimes
|
|
535
|
+
* act on the half the reader did not mean.
|
|
536
|
+
*
|
|
537
|
+
* `allow-network`/`allow-remote` turn a review TIER on, and they exist here
|
|
538
|
+
* because the page can now act — which is the premise
|
|
539
|
+
* `feat-three-review-links` decision 5 rejected a page toggle on, having
|
|
540
|
+
* recorded it as open to reopening. Two things replace the limit it rested on.
|
|
541
|
+
* **Only a reader who can already reach the page can press one**, so widening
|
|
542
|
+
* loopback→network can only be pressed from the machine itself, where the
|
|
543
|
+
* command was available anyway, and the serve token still decides who reaches
|
|
544
|
+
* the page at all. And `allow-remote` **permits publishing without
|
|
545
|
+
* publishing** — the permanent claude.ai page still takes an explicit ask.
|
|
546
|
+
*
|
|
547
|
+
* There is no disable direction, and `tierAction` is where that is enforced:
|
|
548
|
+
* turning `network` off from a page reached over the network kills the page
|
|
549
|
+
* doing the turning.
|
|
550
|
+
*/
|
|
551
|
+
const ACTIONS = ['live-on', 'allow-network', 'allow-remote']
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* The action a tier that is OFF offers, or null.
|
|
555
|
+
*
|
|
556
|
+
* ENABLE-ONLY, and that is a decision rather than an omission: turning
|
|
557
|
+
* `network` off from a page you reached over the network kills the page doing
|
|
558
|
+
* the turning, and the reader gains nothing they could not get by typing the
|
|
559
|
+
* command. `local` has no setting at all — it is the machine the page is on.
|
|
560
|
+
*/
|
|
561
|
+
function tierAction(tier) {
|
|
562
|
+
if (!tier || !tier.off) return null
|
|
563
|
+
if (tier.tier === 'network') return 'allow-network'
|
|
564
|
+
if (tier.tier === 'remote') return 'allow-remote'
|
|
565
|
+
return null
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* What the page shows above the verdicts: where this review can be reached, and
|
|
570
|
+
* whether it is also running. Pure.
|
|
571
|
+
*
|
|
572
|
+
* ONLY WHAT THE READER CAN ACT ON. An `on` tier the reader is already standing
|
|
573
|
+
* on is not information — they are reading it — so the only tier URL carried is
|
|
574
|
+
* `network`'s, which is the one address someone on a laptop wants for their
|
|
575
|
+
* phone. Everything else here is a state plus the one press that changes it.
|
|
576
|
+
*
|
|
577
|
+
* `null` RATHER THAN AN EMPTY BLOCK when there is nothing to say: an
|
|
578
|
+
* `unavailable` live state and no stack means a caller that never asked, and
|
|
579
|
+
* the payload must then be byte-identical to what it was before this key
|
|
580
|
+
* existed.
|
|
581
|
+
*/
|
|
582
|
+
function surfacesFor({ live, tiers }) {
|
|
583
|
+
const rows = []
|
|
584
|
+
|
|
585
|
+
// `unavailable` contributes nothing — the cannot-tell state, routed to
|
|
586
|
+
// silence exactly as `liveStateLine` routes it (`env/live.js`).
|
|
587
|
+
if (live && live.state && live.state !== 'unavailable') {
|
|
588
|
+
rows.push({
|
|
589
|
+
kind: 'live',
|
|
590
|
+
state: live.state,
|
|
591
|
+
url: live.url || null,
|
|
592
|
+
reason: live.reason || null,
|
|
593
|
+
// ONE DIRECTION ONLY. `off` offers the press; `on` and `held` offer none —
|
|
594
|
+
// `on` because handing the instance back to `main` is not this review's
|
|
595
|
+
// business, and `held` because the way out is another spec's to take and a
|
|
596
|
+
// button here would either park someone else's work or do nothing.
|
|
597
|
+
action: live.state === 'off' ? 'live-on' : null,
|
|
598
|
+
// The command for the direction the page does not offer, so the reader is
|
|
599
|
+
// never left knowing what they want and not what to type.
|
|
600
|
+
command: live.state === 'on' ? '/spec-live main' : null,
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
for (const t of tiers || []) {
|
|
605
|
+
const action = tierAction(t)
|
|
606
|
+
// ONLY WHAT THE READER CAN ACT ON. An `on` tier contributes nothing: the
|
|
607
|
+
// reader is standing on one of them, and the terminal render is where they
|
|
608
|
+
// got the link — printing it again here would be two places naming one page,
|
|
609
|
+
// which is the split `.claude/rules/spec-reports.md` records as a failure.
|
|
610
|
+
if (!action) continue
|
|
611
|
+
rows.push({
|
|
612
|
+
kind: 'tier',
|
|
613
|
+
tier: t.tier,
|
|
614
|
+
state: 'off',
|
|
615
|
+
url: null,
|
|
616
|
+
// SAID BEFORE THE PRESS, not after it. Unlike the live action, which
|
|
617
|
+
// touches no tracked file, `allow` edits `specs/.core/env.config.json` in
|
|
618
|
+
// the primary checkout — changing behaviour for everyone who pulls and
|
|
619
|
+
// leaving that tree dirty. A setting change of that reach must not land
|
|
620
|
+
// because someone tapped a button labelled only `Allow remote`.
|
|
621
|
+
note: t.note || 'writes env.config.json — committed, and shared',
|
|
622
|
+
// The same thing from a terminal, for the reader who is nowhere near the
|
|
623
|
+
// page. Only `remote` has a command of its own: `network` is ON by
|
|
624
|
+
// default, so turning it off is a rare deliberate act and a slash command
|
|
625
|
+
// per tier is clutter for the one nobody touches.
|
|
626
|
+
command: t.tier === 'remote' ? '/spec-remote-review' : null,
|
|
627
|
+
action,
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return rows.length ? rows : null
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* The three review tiers, in a FIXED ORDER, each either a URL or the reason it
|
|
636
|
+
* is not one. Pure: it takes what the caller already resolved.
|
|
637
|
+
*
|
|
638
|
+
* Fixed order matters more than it looks. A reader who has learnt which line
|
|
639
|
+
* their phone opens should not have to re-read the labels every render, and a
|
|
640
|
+
* stack that reordered itself by availability would make them.
|
|
641
|
+
*
|
|
642
|
+
* WHY A STACK AT ALL, when the rule said exactly one link: the engine cannot
|
|
643
|
+
* know where the reader is sitting, and every attempt to guess failed the
|
|
644
|
+
* moment they moved. `local` and `network` are two doors into ONE room — the
|
|
645
|
+
* page POSTs to `location.pathname`, so both reach the same server and the same
|
|
646
|
+
* pending store, and one wait covers both. `remote` is a second store, which is
|
|
647
|
+
* why it carries the line saying so.
|
|
648
|
+
*/
|
|
649
|
+
function reviewTierStack({ served, fileUrl, publishedUrl, config }) {
|
|
650
|
+
const tiers = []
|
|
651
|
+
const loopbackUrl = served && served.loopbackUrl ? served.loopbackUrl : null
|
|
652
|
+
|
|
653
|
+
tiers.push(
|
|
654
|
+
loopbackUrl
|
|
655
|
+
? { tier: 'local', url: loopbackUrl }
|
|
656
|
+
: { tier: 'local', url: fileUrl, note: 'a file:// page cannot send a verdict' },
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
if (!config.review.allowNetwork) {
|
|
660
|
+
tiers.push({ tier: 'network', off: true, enable: 'skitterspec spec-env review allow network' })
|
|
661
|
+
} else if (served && !served.loopback && served.url) {
|
|
662
|
+
tiers.push({ tier: 'network', url: served.url, alternates: served.alternates || [] })
|
|
663
|
+
} else {
|
|
664
|
+
// Permitted, but there is no address to offer — a machine with no network,
|
|
665
|
+
// or a server that could not take one. Not a problem to report as an error.
|
|
666
|
+
tiers.push({ tier: 'network', unavailable: true, note: 'no network address on this machine' })
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!config.review.allowRemote) {
|
|
670
|
+
// THE COMMAND A PERSON TYPES. `/spec-remote-review` toggles, so the same
|
|
671
|
+
// line works whichever way the tier currently is — and nobody has to
|
|
672
|
+
// reconstruct `spec-env review allow remote` from a render they are reading
|
|
673
|
+
// on a phone.
|
|
674
|
+
tiers.push({ tier: 'remote', off: true, enable: '/spec-remote-review' })
|
|
675
|
+
} else if (publishedUrl) {
|
|
676
|
+
tiers.push({ tier: 'remote', url: publishedUrl, note: 'a verdict here needs /spec-reviewed' })
|
|
677
|
+
} else {
|
|
678
|
+
tiers.push({ tier: 'remote', unavailable: true, note: 'publishing is an ask — nothing published yet' })
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return tiers
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// One tier as a line, padded so the labels form a column the eye can run down.
|
|
685
|
+
function reviewTierLine(t) {
|
|
686
|
+
const label = ` ${t.tier}:`.padEnd(11)
|
|
687
|
+
if (t.url) return `${label}${t.url}${t.note ? ` (${t.note})` : ''}`
|
|
688
|
+
if (t.off) return `${label}off — turn on with: ${t.enable}`
|
|
689
|
+
return `${label}— (${t.note})`
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
|
|
420
693
|
// The verdicts that COMMIT, and are therefore blocked by an open comment. One
|
|
421
694
|
// list, so a fifth verdict cannot become a way around the single refusal this
|
|
422
695
|
// engine makes — adding a committing verdict means adding it here, and the
|
|
@@ -427,7 +700,7 @@ const DEFAULT_VERDICT = 'discuss'
|
|
|
427
700
|
// obligation that outlives the turn. A mid-run reader saying "carry on" has
|
|
428
701
|
// answered the offer in front of them and nothing else, so the gate a finished
|
|
429
702
|
// phase armed must survive it untouched.
|
|
430
|
-
const COMMITTING = ['commit', 'commit-continue']
|
|
703
|
+
const COMMITTING = ['commit', 'commit-continue', 'commit-start']
|
|
431
704
|
|
|
432
705
|
/**
|
|
433
706
|
* Which set of buttons a rendered page shows.
|
|
@@ -442,8 +715,19 @@ const COMMITTING = ['commit', 'commit-continue']
|
|
|
442
715
|
* `committing` is the default, so a caller that says nothing keeps today's page
|
|
443
716
|
* exactly — the key is left off the payload entirely rather than written out as
|
|
444
717
|
* the default, so an unchanged caller renders an unchanged page.
|
|
718
|
+
*
|
|
719
|
+
* `authoring` is the set for a spec that has just been written: its committing
|
|
720
|
+
* pair is `commit-start` and `commit` — put it in flight now, or keep it for
|
|
721
|
+
* later. `commit-continue` is absent because there is no phase in flight to
|
|
722
|
+
* continue, and `continue` is absent because the run has nothing left to resume.
|
|
723
|
+
*
|
|
724
|
+
* `refresh` is the set for a spec that was re-validated rather than written:
|
|
725
|
+
* `commit`, `changes`, `discuss`, and NO start verdict. A refreshed spec may
|
|
726
|
+
* already be in progress, so offering to put it in flight would be wrong for
|
|
727
|
+
* half this set's inputs — and wrong in the expensive direction, since it would
|
|
728
|
+
* offer to provision a worktree for a spec that already has one.
|
|
445
729
|
*/
|
|
446
|
-
const BUTTON_SETS = ['committing', 'midrun']
|
|
730
|
+
const BUTTON_SETS = ['committing', 'midrun', 'authoring', 'refresh']
|
|
447
731
|
const DEFAULT_BUTTON_SET = 'committing'
|
|
448
732
|
|
|
449
733
|
/**
|
|
@@ -584,9 +868,13 @@ function writeNotes(outPath, notes) {
|
|
|
584
868
|
|
|
585
869
|
const PENDING_VERSION = 1
|
|
586
870
|
|
|
587
|
-
// Beside the page and the notes sidecar, under gitignored `.spec-env/`.
|
|
871
|
+
// Beside the page and the notes sidecar, under gitignored `.spec-env/`. The
|
|
872
|
+
// suffix is named once so the cross-spec scan recognises a store by the same
|
|
873
|
+
// spelling that writes one.
|
|
874
|
+
const PENDING_SUFFIX = '.pending.json'
|
|
875
|
+
|
|
588
876
|
function reviewPendingPath(outPath) {
|
|
589
|
-
return outPath.replace(/\.html$/, '') +
|
|
877
|
+
return outPath.replace(/\.html$/, '') + PENDING_SUFFIX
|
|
590
878
|
}
|
|
591
879
|
|
|
592
880
|
function emptyPending(specFolder) {
|
|
@@ -674,6 +962,59 @@ function addPending(pending, { blob, at, render }, mint = mintPendingCode) {
|
|
|
674
962
|
return { pending: next, code }
|
|
675
963
|
}
|
|
676
964
|
|
|
965
|
+
/**
|
|
966
|
+
* What became of ONE pass, by its code. Reads; writes nothing, refuses nothing.
|
|
967
|
+
*
|
|
968
|
+
* The served page POSTs, is handed six digits, and then has to end on one of
|
|
969
|
+
* two sentences — Claude picked this up, or here is the command that picks it
|
|
970
|
+
* up. It cannot know which at send time, so it asks, and this answers.
|
|
971
|
+
*
|
|
972
|
+
* Three states, and the third is the point:
|
|
973
|
+
*
|
|
974
|
+
* - `waiting` — the code IS in the holding area. Nobody has claimed it.
|
|
975
|
+
* - `claimed` — the decision log NAMES the code. Somebody did.
|
|
976
|
+
* - `unknown` — neither could be established.
|
|
977
|
+
*
|
|
978
|
+
* BOTH ANSWERS REST ON SOMETHING PRESENT, which is what stops this being the
|
|
979
|
+
* fourth row in the table at the top of `.claude/rules/negative-checks.md`. The
|
|
980
|
+
* tempting cheap version reads `claimed` off the code being ABSENT from the
|
|
981
|
+
* holding area — and absence there has four causes, of which exactly one is a
|
|
982
|
+
* claim: `--drop` removes a pass and writes no decision, a store written under
|
|
983
|
+
* a different `--out` is a store this never opens, and a mistyped folder finds
|
|
984
|
+
* an empty one. Three of those four would report a pass as picked up while it
|
|
985
|
+
* sits in a file nobody is reading, which is this spec's own bug told back to
|
|
986
|
+
* the reader with confidence.
|
|
987
|
+
*
|
|
988
|
+
* So `unknown` answers everything that is not a positive signal, and the caller
|
|
989
|
+
* turns it back into the command. That direction is deliberate: a command
|
|
990
|
+
* nobody needed to run costs a glance, and a pass nobody claims costs the
|
|
991
|
+
* review.
|
|
992
|
+
*
|
|
993
|
+
* A code that is not six digits never reaches disk. It cannot be one this
|
|
994
|
+
* engine minted, so there is nothing to look up, and answering it from the
|
|
995
|
+
* filesystem would make a reachable endpoint into a path-shaped probe.
|
|
996
|
+
*/
|
|
997
|
+
function passState(outPath, specFolder, code) {
|
|
998
|
+
const wanted = typeof code === 'string' ? code.trim() : ''
|
|
999
|
+
if (!new RegExp(`^\\d{${PENDING_CODE_LENGTH}}$`).test(wanted)) return { state: 'unknown' }
|
|
1000
|
+
|
|
1001
|
+
const held = readPending(outPath, specFolder)
|
|
1002
|
+
// A store we could not parse is not an empty one. It holds somebody's passes
|
|
1003
|
+
// and we simply cannot see them, so neither answer is available.
|
|
1004
|
+
if (held.corrupt) return { state: 'unknown' }
|
|
1005
|
+
if ((held.pending.passes || []).some((p) => p && p.code === wanted)) return { state: 'waiting' }
|
|
1006
|
+
|
|
1007
|
+
const stored = readNotes(outPath, specFolder)
|
|
1008
|
+
if (stored.corrupt) return { state: 'unknown' }
|
|
1009
|
+
const decisions = Array.isArray(stored.notes.decisions) ? stored.notes.decisions : []
|
|
1010
|
+
if (decisions.some((d) => d && d.code === wanted)) return { state: 'claimed' }
|
|
1011
|
+
|
|
1012
|
+
// Gone from the holding area and unnamed in the log. It may have been
|
|
1013
|
+
// dropped, it may predate this field, the store may not be the one the page
|
|
1014
|
+
// was served from. Say so.
|
|
1015
|
+
return { state: 'unknown' }
|
|
1016
|
+
}
|
|
1017
|
+
|
|
677
1018
|
/**
|
|
678
1019
|
* What is waiting, as the render should describe it. Pure.
|
|
679
1020
|
*
|
|
@@ -695,6 +1036,11 @@ function describePending(pending) {
|
|
|
695
1036
|
// `approve`, and the operator must be offered the word that describes
|
|
696
1037
|
// what claiming it would do.
|
|
697
1038
|
verdict: readVerdict((p.blob && p.blob.verdict) || null) || null,
|
|
1039
|
+
// AN ACTION IS REPORTED AS AN ACTION. A pass carrying one has no verdict,
|
|
1040
|
+
// and listing it as `no verdict` would describe the instruction it does
|
|
1041
|
+
// carry as an absence — which is the reading that makes a reader claim it
|
|
1042
|
+
// expecting a conclusion.
|
|
1043
|
+
action: (p.blob && p.blob.action) || null,
|
|
698
1044
|
at: p.at || null,
|
|
699
1045
|
}))
|
|
700
1046
|
.sort((a, b) => String(a.at).localeCompare(String(b.at)) || a.code.localeCompare(b.code))
|
|
@@ -782,6 +1128,141 @@ function passesSince(pending, since) {
|
|
|
782
1128
|
return { codes, usable: true }
|
|
783
1129
|
}
|
|
784
1130
|
|
|
1131
|
+
/* ==========================================================================
|
|
1132
|
+
* The wait — one implementation, because improvised ones cannot be tested
|
|
1133
|
+
*
|
|
1134
|
+
* The skills used to say "watch the pending store and end your turn" and stop
|
|
1135
|
+
* there, so every run wrote its own watcher in shell. Three failed in two days,
|
|
1136
|
+
* each reaching the operator as "I pressed the button and nothing happened" —
|
|
1137
|
+
* and the worst of them was `until [ -f "$P" ] && [ "$x" \\> "$y" ]`, valid bash
|
|
1138
|
+
* and a syntax error in zsh, which spun for five minutes writing to a stderr
|
|
1139
|
+
* nobody reads.
|
|
1140
|
+
*
|
|
1141
|
+
* What made that expensive was not the typo. It is that SILENCE WAS THE SUCCESS
|
|
1142
|
+
* SIGNAL: from outside, a watcher that can never fire and one patiently working
|
|
1143
|
+
* look exactly alike. So the comparison lives here, in one place, where a test
|
|
1144
|
+
* can hand it a store that gains a pass mid-flight and watch it return.
|
|
1145
|
+
* ========================================================================== */
|
|
1146
|
+
|
|
1147
|
+
const WAIT_POLL_MS = 400
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Block until exactly one pass arrives inside a window. Impure only in that it
|
|
1151
|
+
* reads the store and sleeps; it writes nothing and claims nothing.
|
|
1152
|
+
*
|
|
1153
|
+
* Four outcomes, and they are distinct because the caller acts differently on
|
|
1154
|
+
* each:
|
|
1155
|
+
*
|
|
1156
|
+
* - `{ state: 'arrived', code }` — one pass, inside the window. Claim it.
|
|
1157
|
+
* - `{ state: 'ambiguous', count }` — more than one. NAMES NO CODE, because
|
|
1158
|
+
* choosing between two is exactly the guess `claimPending` refuses to make.
|
|
1159
|
+
* - `{ state: 'timeout' }` — only when a timeout was asked for.
|
|
1160
|
+
* - `{ state: 'unusable' }` — the window could not be parsed, so there is
|
|
1161
|
+
* nothing to wait inside. Returns AT ONCE rather than blocking forever on a
|
|
1162
|
+
* comparison that can never be satisfied — which is the failure this whole
|
|
1163
|
+
* function exists to stop.
|
|
1164
|
+
*
|
|
1165
|
+
* NO TIMEOUT UNLESS ASKED. `timeoutMs` omitted means wait as long as the
|
|
1166
|
+
* process lives, and that is the default the skills use: any fixed number is a
|
|
1167
|
+
* guess about how long someone reads, and a reader who walks away from a diff
|
|
1168
|
+
* is the normal case rather than the edge one. An hour was picked once and a
|
|
1169
|
+
* lunch break beat it.
|
|
1170
|
+
*
|
|
1171
|
+
* WHAT WOULD FOOL A LOOSER VERSION: treating an absent, empty, or older-only
|
|
1172
|
+
* store as a reason to return. None of those is a pass — they are the ordinary
|
|
1173
|
+
* state of a review nobody has answered yet — and a wait that ended on one
|
|
1174
|
+
* would report "no pass" as an outcome (`.claude/rules/negative-checks.md`
|
|
1175
|
+
* rule 3 has a test for each).
|
|
1176
|
+
*/
|
|
1177
|
+
async function waitForPass(readStore, since, { timeoutMs = null, pollMs = WAIT_POLL_MS, sleep } = {}) {
|
|
1178
|
+
if (!Number.isFinite(Date.parse(since))) return { state: 'unusable' }
|
|
1179
|
+
const nap = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
|
|
1180
|
+
const deadline = Number.isFinite(timeoutMs) && timeoutMs !== null ? Date.now() + timeoutMs : null
|
|
1181
|
+
|
|
1182
|
+
for (;;) {
|
|
1183
|
+
// A store that will not parse is not an empty one, and it is not a pass
|
|
1184
|
+
// either — keep waiting rather than reading someone's unreadable passes as
|
|
1185
|
+
// an answer. `readPending` already reports `corrupt` rather than throwing.
|
|
1186
|
+
const read = readStore()
|
|
1187
|
+
if (read && !read.corrupt) {
|
|
1188
|
+
const window = passesSince(read.pending, since)
|
|
1189
|
+
if (window.usable) {
|
|
1190
|
+
if (window.codes.length === 1) return { state: 'arrived', code: window.codes[0] }
|
|
1191
|
+
if (window.codes.length > 1) return { state: 'ambiguous', count: window.codes.length }
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (deadline !== null && Date.now() >= deadline) return { state: 'timeout' }
|
|
1195
|
+
// Never overshoot the deadline by a whole poll interval.
|
|
1196
|
+
await nap(deadline === null ? pollMs : Math.max(0, Math.min(pollMs, deadline - Date.now())))
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Every waiting pass, across every spec. Reads; writes, claims and refuses
|
|
1202
|
+
* nothing.
|
|
1203
|
+
*
|
|
1204
|
+
* A wait that never fired, a session cleared, a terminal closed overnight —
|
|
1205
|
+
* none of those is recoverable by any watcher, however good. This is what finds
|
|
1206
|
+
* the pass afterwards, so recovery stops depending on the operator happening to
|
|
1207
|
+
* suspect something.
|
|
1208
|
+
*
|
|
1209
|
+
* IT READS THE SIDECAR DIRECTORY, NEVER THE PROVISIONED LIST, and that is the
|
|
1210
|
+
* load-bearing decision rather than an implementation detail. `specEnvStatus`
|
|
1211
|
+
* walks specs that have a **worktree**; the ten passes that motivated this all
|
|
1212
|
+
* belonged to specs that had been completed and torn down. Scoping the scan to
|
|
1213
|
+
* provisioned specs would therefore make it blind to precisely the case that
|
|
1214
|
+
* produced it. A `<spec>.pending.json` is here because a pass was received —
|
|
1215
|
+
* a positive signal (`.claude/rules/negative-checks.md` rule 1), and one that
|
|
1216
|
+
* outlives the spec's worktree, its branch and its folder.
|
|
1217
|
+
*
|
|
1218
|
+
* WHAT WOULD FOOL THIS: a store that will not parse. It holds someone's passes
|
|
1219
|
+
* and reading it as empty would report "nothing waiting", which is the one
|
|
1220
|
+
* answer that is certainly wrong — so it comes back named in `unreadable`
|
|
1221
|
+
* rather than counted as zero (rule 4).
|
|
1222
|
+
*
|
|
1223
|
+
* Oldest first and stable, ties broken on the code, exactly as
|
|
1224
|
+
* `describePending` orders one spec's own.
|
|
1225
|
+
*/
|
|
1226
|
+
function waitingPasses(reviewsDirPath) {
|
|
1227
|
+
let entries
|
|
1228
|
+
try {
|
|
1229
|
+
entries = fs.readdirSync(reviewsDirPath)
|
|
1230
|
+
} catch {
|
|
1231
|
+
// No directory means no review has ever been rendered here. That is the
|
|
1232
|
+
// ordinary state of a fresh repo, not a failure to report.
|
|
1233
|
+
return { passes: [], unreadable: [] }
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const passes = []
|
|
1237
|
+
const unreadable = []
|
|
1238
|
+
for (const name of entries.sort()) {
|
|
1239
|
+
if (!name.endsWith(PENDING_SUFFIX)) continue
|
|
1240
|
+
const folder = name.slice(0, -PENDING_SUFFIX.length)
|
|
1241
|
+
// The engine's own reader, so "corrupt" means here what it means everywhere.
|
|
1242
|
+
const read = readPending(path.join(reviewsDirPath, `${folder}.html`), folder)
|
|
1243
|
+
if (read.corrupt) {
|
|
1244
|
+
unreadable.push(folder)
|
|
1245
|
+
continue
|
|
1246
|
+
}
|
|
1247
|
+
for (const pass of read.pending.passes || []) {
|
|
1248
|
+
if (pass && pass.code) {
|
|
1249
|
+
passes.push({
|
|
1250
|
+
spec: folder,
|
|
1251
|
+
code: pass.code,
|
|
1252
|
+
verdict: readVerdict(pass.blob && pass.blob.verdict),
|
|
1253
|
+
action: (pass.blob && pass.blob.action) || null,
|
|
1254
|
+
at: pass.at || null,
|
|
1255
|
+
})
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
passes.sort((a, b) => {
|
|
1260
|
+
const at = String(a.at || '').localeCompare(String(b.at || ''))
|
|
1261
|
+
return at !== 0 ? at : a.code.localeCompare(b.code)
|
|
1262
|
+
})
|
|
1263
|
+
return { passes, unreadable }
|
|
1264
|
+
}
|
|
1265
|
+
|
|
785
1266
|
/* ==========================================================================
|
|
786
1267
|
* The gate — a standing obligation to review, not a message in flight
|
|
787
1268
|
*
|
|
@@ -1005,7 +1486,20 @@ function validateNotesBlob(blob, specFolder) {
|
|
|
1005
1486
|
}
|
|
1006
1487
|
verdict = named
|
|
1007
1488
|
}
|
|
1008
|
-
|
|
1489
|
+
// AN ACTION, refused by name for the same reason a misspelt verdict is: a
|
|
1490
|
+
// dropped `action: "live-onn"` would read as a pass carrying no instruction
|
|
1491
|
+
// at all, reported as if the press had been honoured.
|
|
1492
|
+
let action = null
|
|
1493
|
+
if (blob.action !== undefined && blob.action !== null) {
|
|
1494
|
+
if (typeof blob.action !== 'string' || !ACTIONS.includes(blob.action)) {
|
|
1495
|
+
fail(`action ${JSON.stringify(blob.action)} is not one of ${ACTIONS.join(', ')}`)
|
|
1496
|
+
}
|
|
1497
|
+
action = blob.action
|
|
1498
|
+
}
|
|
1499
|
+
if (action && verdict) {
|
|
1500
|
+
fail(`carries both a verdict (${verdict}) and an action (${action}) — send one`)
|
|
1501
|
+
}
|
|
1502
|
+
return { accepted, unaccepted, comments, verdict, action }
|
|
1009
1503
|
}
|
|
1010
1504
|
|
|
1011
1505
|
/**
|
|
@@ -1032,7 +1526,15 @@ function validateNotesBlob(blob, specFolder) {
|
|
|
1032
1526
|
* failure we can afford.
|
|
1033
1527
|
*/
|
|
1034
1528
|
function judgeVerdict(verdict, notes) {
|
|
1035
|
-
const
|
|
1529
|
+
const read = readVerdict(verdict) || null
|
|
1530
|
+
// A WORD THIS ENGINE DOES NOT KNOW IS NOT HONOURED AS ONE. Every door into
|
|
1531
|
+
// here validates first — `validateNotesBlob` refuses an unknown verdict by
|
|
1532
|
+
// name and refuses an action in the verdict slot, and `--verdict` checks the
|
|
1533
|
+
// list — so nothing reaches this today. It used to pass such a word straight
|
|
1534
|
+
// through with `honoured: true`, which routes cannot-tell to the branch that
|
|
1535
|
+
// acts (`.claude/rules/negative-checks.md` rule 4 inverted). Closing it costs
|
|
1536
|
+
// nothing and means a fifth door added later cannot reopen it.
|
|
1537
|
+
const sent = read && VERDICTS.includes(read) ? read : null
|
|
1036
1538
|
const asked = sent || DEFAULT_VERDICT
|
|
1037
1539
|
const open = (notes.comments || []).filter((c) => !c.resolved)
|
|
1038
1540
|
const openFiles = [...new Set(open.map((c) => c.file))]
|
|
@@ -1060,9 +1562,18 @@ function judgeVerdict(verdict, notes) {
|
|
|
1060
1562
|
* commit something nobody read. What is kept is the account of what was
|
|
1061
1563
|
* decided, when, and eventually what it produced.
|
|
1062
1564
|
*/
|
|
1063
|
-
function appendDecision(notes, { verdict, at, note = null }) {
|
|
1565
|
+
function appendDecision(notes, { verdict, at, note = null, code = null }) {
|
|
1064
1566
|
const decisions = Array.isArray(notes.decisions) ? notes.decisions.slice() : []
|
|
1065
|
-
|
|
1567
|
+
// `code` NAMES THE PASS this decision came out of, and it is the only present
|
|
1568
|
+
// thing anyone can assert to conclude that a pass was picked up. Four
|
|
1569
|
+
// different things make a code vanish from the holding area and only one of
|
|
1570
|
+
// them is a claim — `--drop` is another, a moved store and a mistyped folder
|
|
1571
|
+
// are two more — so `passState` reads this, and never reads an absence.
|
|
1572
|
+
//
|
|
1573
|
+
// `null` for every other path in: a pasted blob and a `--verdict` word carry
|
|
1574
|
+
// no code, and inventing one would put a pass in the record that never
|
|
1575
|
+
// existed.
|
|
1576
|
+
decisions.push({ verdict, at, note: note === undefined ? null : note, code: code || null })
|
|
1066
1577
|
return { ...notes, updatedAt: at, decisions }
|
|
1067
1578
|
}
|
|
1068
1579
|
|
|
@@ -1501,6 +2012,11 @@ module.exports = {
|
|
|
1501
2012
|
WHOLE_FILE_CONTEXT,
|
|
1502
2013
|
NOTES_VERSION,
|
|
1503
2014
|
VERDICTS,
|
|
2015
|
+
ACTIONS,
|
|
2016
|
+
tierAction,
|
|
2017
|
+
surfacesFor,
|
|
2018
|
+
reviewTierStack,
|
|
2019
|
+
reviewTierLine,
|
|
1504
2020
|
COMMITTING,
|
|
1505
2021
|
BUTTON_SETS,
|
|
1506
2022
|
DEFAULT_BUTTON_SET,
|
|
@@ -1518,6 +2034,7 @@ module.exports = {
|
|
|
1518
2034
|
appendDecision,
|
|
1519
2035
|
annotateLastDecision,
|
|
1520
2036
|
reviewPendingPath,
|
|
2037
|
+
PENDING_SUFFIX,
|
|
1521
2038
|
emptyPending,
|
|
1522
2039
|
readPending,
|
|
1523
2040
|
writePending,
|
|
@@ -1525,6 +2042,10 @@ module.exports = {
|
|
|
1525
2042
|
addPending,
|
|
1526
2043
|
claimPending,
|
|
1527
2044
|
passesSince,
|
|
2045
|
+
waitingPasses,
|
|
2046
|
+
waitForPass,
|
|
2047
|
+
WAIT_POLL_MS,
|
|
2048
|
+
passState,
|
|
1528
2049
|
describePending,
|
|
1529
2050
|
pendingAge,
|
|
1530
2051
|
PENDING_CODE_LENGTH,
|