plumbbob 0.9.5 → 0.10.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +89 -87
- package/dist/cli-core.js +18 -8
- package/dist/cli.js +1 -1
- package/dist/lib/agents.js +26 -26
- package/dist/lib/buildlog.js +21 -11
- package/dist/lib/buildlogsync.js +8 -8
- package/dist/lib/check.js +14 -14
- package/dist/lib/commitbody.js +72 -0
- package/dist/lib/commitmsg.js +7 -7
- package/dist/lib/git.js +44 -15
- package/dist/lib/intent.js +20 -19
- package/dist/lib/latch.js +16 -16
- package/dist/lib/orient.js +36 -17
- package/dist/lib/reentry.js +3 -3
- package/dist/lib/settings.js +16 -16
- package/dist/lib/sidecar.js +70 -54
- package/dist/lib/templates.js +1 -1
- package/dist/verbs/abandon.js +122 -0
- package/dist/verbs/agent.js +25 -25
- package/dist/verbs/build.js +9 -9
- package/dist/verbs/check.js +6 -6
- package/dist/verbs/checkpoint.js +48 -47
- package/dist/verbs/doctor.js +35 -29
- package/dist/verbs/finish.js +27 -40
- package/dist/verbs/handoff.js +9 -8
- package/dist/verbs/init.js +9 -9
- package/dist/verbs/park.js +3 -3
- package/dist/verbs/recover.js +12 -12
- package/dist/verbs/revert.js +12 -12
- package/dist/verbs/spike.js +21 -21
- package/dist/verbs/start.js +49 -19
- package/dist/verbs/status.js +8 -8
- package/dist/verbs/turn.js +16 -16
- package/dist/verbs/use.js +4 -4
- package/node_modules/checkride/.claude-plugin/plugin.json +1 -1
- package/node_modules/checkride/CHANGELOG.md +401 -0
- package/node_modules/checkride/README.md +27 -9
- package/node_modules/checkride/dist/adapters.d.ts +12 -4
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +21 -1
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts +9 -1
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/cursor.js +59 -1
- package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.d.ts +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.js +10 -1
- package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts +23 -0
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.js +387 -82
- package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.js +2 -2
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts +14 -5
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.js +58 -5
- package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
- package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
- package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/history.js +232 -0
- package/node_modules/checkride/dist/baseline/history.js.map +1 -0
- package/node_modules/checkride/dist/baseline/index.d.ts +4 -2
- package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/index.js +2 -1
- package/node_modules/checkride/dist/baseline/index.js.map +1 -1
- package/node_modules/checkride/dist/baseline/store.d.ts +30 -7
- package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/store.js +50 -16
- package/node_modules/checkride/dist/baseline/store.js.map +1 -1
- package/node_modules/checkride/dist/cli.d.ts +5 -1
- package/node_modules/checkride/dist/cli.d.ts.map +1 -1
- package/node_modules/checkride/dist/cli.js +98 -25
- package/node_modules/checkride/dist/cli.js.map +1 -1
- package/node_modules/checkride/dist/config.d.ts +33 -4
- package/node_modules/checkride/dist/config.d.ts.map +1 -1
- package/node_modules/checkride/dist/config.js +21 -6
- package/node_modules/checkride/dist/config.js.map +1 -1
- package/node_modules/checkride/dist/digest/digest.js +1 -1
- package/node_modules/checkride/dist/doctor.d.ts +2 -0
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
- package/node_modules/checkride/dist/doctor.js +39 -1
- package/node_modules/checkride/dist/doctor.js.map +1 -1
- package/node_modules/checkride/dist/gate.d.ts +49 -4
- package/node_modules/checkride/dist/gate.d.ts.map +1 -1
- package/node_modules/checkride/dist/gate.js +143 -16
- package/node_modules/checkride/dist/gate.js.map +1 -1
- package/node_modules/checkride/dist/init.d.ts +8 -1
- package/node_modules/checkride/dist/init.d.ts.map +1 -1
- package/node_modules/checkride/dist/init.js +68 -7
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/node-pin.d.ts +1 -1
- package/node_modules/checkride/dist/node-pin.js +1 -1
- package/node_modules/checkride/dist/orchestrator.d.ts +13 -0
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +118 -13
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pm/index.d.ts +1 -0
- package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/index.js.map +1 -1
- package/node_modules/checkride/dist/pm/launch.d.ts +23 -2
- package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/launch.js +16 -0
- package/node_modules/checkride/dist/pm/launch.js.map +1 -1
- package/node_modules/checkride/dist/pm/translate.js +1 -1
- package/node_modules/checkride/dist/pm/translate.js.map +1 -1
- package/node_modules/checkride/dist/qa/mutation.d.ts +4 -4
- package/node_modules/checkride/dist/qa/mutation.js +4 -4
- package/node_modules/checkride/dist/recover.d.ts +48 -0
- package/node_modules/checkride/dist/recover.d.ts.map +1 -0
- package/node_modules/checkride/dist/recover.js +220 -0
- package/node_modules/checkride/dist/recover.js.map +1 -0
- package/node_modules/checkride/dist/smoke.d.ts +1 -1
- package/node_modules/checkride/dist/smoke.js +1 -1
- package/node_modules/checkride/dist/snippets.d.ts +1 -1
- package/node_modules/checkride/package.json +3 -2
- package/node_modules/checkride/schema/checkride.config.schema.json +9 -1
- package/node_modules/checkride/skills/check/SKILL.md +9 -1
- package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
- package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
- package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
- package/node_modules/checkride/templates/shared/vale.ini +34 -0
- package/package.json +13 -11
- package/skills/abandon/SKILL.md +20 -0
- package/skills/build/SKILL.md +69 -64
- package/skills/doctor/SKILL.md +11 -11
- package/skills/finish/SKILL.md +20 -19
- package/skills/harvest/SKILL.md +13 -12
- package/skills/park/SKILL.md +10 -10
- package/skills/plan/SKILL.md +63 -54
- package/skills/recover/SKILL.md +7 -7
- package/skills/refine/SKILL.md +18 -16
- package/skills/revert/SKILL.md +6 -6
- package/skills/spike/SKILL.md +9 -9
- package/skills/status/SKILL.md +5 -5
- package/skills/step/SKILL.md +17 -12
- package/skills/verify/SKILL.md +45 -40
- package/templates/build-log.md +7 -7
- package/templates/intent.md +49 -39
- package/templates/spike-report.md +10 -10
package/dist/verbs/doctor.js
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
// `plumbbob doctor
|
|
1
|
+
// `plumbbob doctor`: the health report, five sections under one verb.
|
|
2
2
|
//
|
|
3
3
|
// 1. The global plugin link (read-only). After `plumbbob init`, plumbbob lives as a
|
|
4
4
|
// symlink at ~/.claude/skills/plumbbob pointing at the installed package; Claude
|
|
5
5
|
// Code loads it in place as a plugin (skills `/plumbbob:*`, the post-edit hook from
|
|
6
6
|
// hooks.json). doctor verifies the link resolves to a package carrying the manifest,
|
|
7
|
-
// the skills, and the hook
|
|
7
|
+
// the skills, and the hook, and names the fix for anything missing. The failure
|
|
8
8
|
// class it exists for is SILENT (a `/plumbbob:status` that opens an empty dashboard
|
|
9
9
|
// because the plugin never linked).
|
|
10
10
|
//
|
|
11
11
|
// 2. The repo sidecar layout. plumbbob keeps its per-repo state in a `.plumbbob/`
|
|
12
12
|
// sidecar: a tracked artifact plane (settings.json plus one `builds/<slug>/` folder
|
|
13
|
-
// per build
|
|
13
|
+
// per build (intent, build-log, checkpoints, report) that rides its branch into
|
|
14
14
|
// the PR) and a git-excluded control plane of per-worktree files. A repo scaffolded
|
|
15
15
|
// by an older plumbbob carries the legacy FLAT sidecar instead (`.plumbbob/intent.md`,
|
|
16
16
|
// `config`, `archive/`), fully git-excluded, whose record dies with
|
|
17
17
|
// `git worktree remove`. doctor detects it and, with `--migrate`, moves it into the
|
|
18
18
|
// tracked `builds/<slug>/` layout: archive entries and the active session become
|
|
19
19
|
// build folders, `config` becomes `settings.json`, and the whole move is STAGED but
|
|
20
|
-
// never committed
|
|
20
|
+
// never committed: the human owns that commit.
|
|
21
21
|
//
|
|
22
22
|
// 3. Agent validation. Every resolvable user-authored agent gets its manifest and its
|
|
23
23
|
// `command` statically checked, so a broken agent surfaces here rather than mid-build.
|
|
24
24
|
//
|
|
25
25
|
// 4. The check gate. When a `check` setting overrides checkride (the default heavy
|
|
26
26
|
// check), doctor names the command; otherwise it runs checkride's own doctor and
|
|
27
|
-
// prints the slot/adapter table
|
|
27
|
+
// prints the slot/adapter table: "detected but tool missing" is the footgun this
|
|
28
28
|
// exists for.
|
|
29
29
|
//
|
|
30
30
|
// 5. The approval latch. Reports whether the harness's turn ledger is live (checkpoint
|
|
31
|
-
// can latch on a recorded human turn) or dormant (guidance only)
|
|
31
|
+
// can latch on a recorded human turn) or dormant (guidance only): dormant is a
|
|
32
32
|
// state, never a failure.
|
|
33
33
|
//
|
|
34
34
|
// Functional, node builtins plus checkride.
|
|
@@ -44,7 +44,7 @@ import { findRepoRoot, gitPath, stagePath } from "../lib/git.js";
|
|
|
44
44
|
import { buildDir, excludeControl, listBuilds, readTurn, setActiveBuild, sidecarDir, slugify } from "../lib/sidecar.js";
|
|
45
45
|
import { resolveBoolean, resolveString, settingsPath } from "../lib/settings.js";
|
|
46
46
|
/**
|
|
47
|
-
* Resolve a directory shipped inside the plumbbob package (
|
|
47
|
+
* Resolve a directory shipped inside the plumbbob package (for example its skills/).
|
|
48
48
|
*
|
|
49
49
|
* Resolved off this module's URL so it lands the same from src/ (dev) and
|
|
50
50
|
* dist/ (published).
|
|
@@ -103,10 +103,10 @@ function buildChecks(link, pkg, shipped) {
|
|
|
103
103
|
/**
|
|
104
104
|
* The plugin-link diagnostic: how plumbbob is installed into Claude Code, as checks.
|
|
105
105
|
*
|
|
106
|
-
* Two install kinds are recognized: the marketplace plugin (one green line
|
|
106
|
+
* Two install kinds are recognized: the marketplace plugin (one green line: skills
|
|
107
107
|
* load and the CLI rides the plugin's bin/, nothing to verify piecemeal) and the
|
|
108
108
|
* init-style link at ~/.claude/skills/plumbbob (verified piece by piece). Both at
|
|
109
|
-
* once is a collision
|
|
109
|
+
* once is a collision (two plugins named plumbbob fight over /plumbbob:*), reported
|
|
110
110
|
* first, with the keep-one fix.
|
|
111
111
|
*/
|
|
112
112
|
function pluginChecks() {
|
|
@@ -148,7 +148,7 @@ function subdirs(dir) {
|
|
|
148
148
|
* Detect a legacy flat sidecar, or null when the repo is on the current layout
|
|
149
149
|
* (or has no sidecar).
|
|
150
150
|
*
|
|
151
|
-
* `config` and `archive/` are unambiguous legacy markers
|
|
151
|
+
* `config` and `archive/` are unambiguous legacy markers: the builds/ layout has
|
|
152
152
|
* neither. A flat `intent.md` alone would also match today's `--local` layout, so it
|
|
153
153
|
* only counts as legacy when the repo is NOT already migrated (no `builds/`, no
|
|
154
154
|
* `settings.json`): that guard is what keeps `--local` untouched.
|
|
@@ -166,7 +166,7 @@ export function inspectLegacy(root) {
|
|
|
166
166
|
return { config, archive, session };
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
|
-
* The first `# Heading` in a flat intent.md
|
|
169
|
+
* The first `# Heading` in a flat intent.md: the build title the slug derives from.
|
|
170
170
|
*/
|
|
171
171
|
function titleFromIntent(path) {
|
|
172
172
|
try {
|
|
@@ -200,7 +200,7 @@ function configCheck(path) {
|
|
|
200
200
|
/**
|
|
201
201
|
* A slug not already claimed by `taken`, suffixing `-2`, `-3`, … only on collision.
|
|
202
202
|
*
|
|
203
|
-
* `start` refuses a colliding slug outright
|
|
203
|
+
* `start` refuses a colliding slug outright (the human picks a new title), but
|
|
204
204
|
* migration is mechanically moving folders that already exist, so it disambiguates
|
|
205
205
|
* rather than aborting mid-move.
|
|
206
206
|
*/
|
|
@@ -245,15 +245,18 @@ function narrowExcludes(root) {
|
|
|
245
245
|
writeFileSync(exclude, kept.join('\n'));
|
|
246
246
|
}
|
|
247
247
|
catch {
|
|
248
|
-
/* no exclude yet
|
|
248
|
+
/* no exclude yet: excludeControl creates it */
|
|
249
249
|
}
|
|
250
250
|
excludeControl(root);
|
|
251
251
|
}
|
|
252
252
|
/**
|
|
253
253
|
* Perform the migration and return a human-readable list of what moved.
|
|
254
254
|
*
|
|
255
|
-
* STAGES the result but never commits
|
|
256
|
-
*
|
|
255
|
+
* STAGES the result but never commits: the human owns that commit. In a repo
|
|
256
|
+
* that gitignores the sidecar the stage is skipped (stagePath's record-only
|
|
257
|
+
* guard) and the report says so: the files move but stay untracked. Call only
|
|
258
|
+
* when `inspectLegacy` reported a legacy layout; doctor prints the returned
|
|
259
|
+
* actions.
|
|
257
260
|
*/
|
|
258
261
|
export function migrateSidecar(root) {
|
|
259
262
|
const dir = sidecarDir(root);
|
|
@@ -263,7 +266,7 @@ export function migrateSidecar(root) {
|
|
|
263
266
|
const configPath = join(dir, 'config');
|
|
264
267
|
if (existsSync(configPath)) {
|
|
265
268
|
if (!existsSync(settingsPath(root))) {
|
|
266
|
-
// Carry forward ONLY what the legacy config actually held
|
|
269
|
+
// Carry forward ONLY what the legacy config actually held: a `check` line,
|
|
267
270
|
// nothing else. No invented `auto` (the config never had one, it defaults to
|
|
268
271
|
// false, and it belongs in settings.local.json): migration yields exactly what
|
|
269
272
|
// a fresh `start` would, plus the check. An empty config → an empty `{}`.
|
|
@@ -297,8 +300,10 @@ export function migrateSidecar(root) {
|
|
|
297
300
|
}
|
|
298
301
|
rmSync(archiveDir, { recursive: true, force: true });
|
|
299
302
|
narrowExcludes(root);
|
|
300
|
-
stagePath(root, '.plumbbob');
|
|
301
|
-
actions.push(
|
|
303
|
+
const staged = stagePath(root, '.plumbbob');
|
|
304
|
+
actions.push(staged
|
|
305
|
+
? 'staged the move (builds/ + settings.json) — commit it yourself'
|
|
306
|
+
: 'moved but not staged (the sidecar is gitignored) — the files stay untracked');
|
|
302
307
|
return actions;
|
|
303
308
|
}
|
|
304
309
|
/**
|
|
@@ -336,7 +341,7 @@ function sidecarReport(cwd, migrate) {
|
|
|
336
341
|
/**
|
|
337
342
|
* Interpreters recognized as the program of a manifest `command`: when the command
|
|
338
343
|
* is `sh <script>` / `node <script>` / …, the checkable artifact is the script
|
|
339
|
-
* argument (the interpreter reads it
|
|
344
|
+
* argument (the interpreter reads it: the file need not carry +x), not the
|
|
340
345
|
* interpreter, which we trust to be on PATH.
|
|
341
346
|
*/
|
|
342
347
|
const INTERPRETERS = new Set(['sh', 'bash', 'zsh', 'dash', 'node', 'deno', 'bun', 'python', 'python3', 'ruby', 'perl']);
|
|
@@ -344,7 +349,7 @@ const INTERPRETERS = new Set(['sh', 'bash', 'zsh', 'dash', 'node', 'deno', 'bun'
|
|
|
344
349
|
* Expand PLUMBBOB_AGENT_DIR in a command and split it into bare tokens
|
|
345
350
|
* (surrounding quotes stripped).
|
|
346
351
|
*
|
|
347
|
-
* A deliberately shallow split
|
|
352
|
+
* A deliberately shallow split: enough for the static command check below, not a
|
|
348
353
|
* shell parser (which is neither possible nor wanted for a best-effort diagnostic).
|
|
349
354
|
*/
|
|
350
355
|
function commandTokens(command, agentDir) {
|
|
@@ -356,11 +361,11 @@ function commandTokens(command, agentDir) {
|
|
|
356
361
|
}
|
|
357
362
|
/**
|
|
358
363
|
* Statically check a manifest `command` for the silent footguns: a script file that
|
|
359
|
-
* does not exist, or a directly
|
|
364
|
+
* does not exist, or a directly invoked one missing its +x bit.
|
|
360
365
|
*
|
|
361
366
|
* The command is a shell string spawned via `sh -c` with the repo root as cwd, so a
|
|
362
367
|
* relative path resolves against `root` and PLUMBBOB_AGENT_DIR points at the agent's
|
|
363
|
-
* own dir. Best-effort by design
|
|
368
|
+
* own dir. Best-effort by design: a path-shaped program or an interpreter's script
|
|
364
369
|
* argument is checked; a bare command (a PATH binary, an inline `node -e` program)
|
|
365
370
|
* is trusted, because we cannot prove a shell line broken without running it.
|
|
366
371
|
* Returns the problem string, or null when nothing checkable is wrong.
|
|
@@ -443,7 +448,7 @@ function agentReport(cwd) {
|
|
|
443
448
|
*
|
|
444
449
|
* A configured `check` setting names the spawn override and asks nothing more;
|
|
445
450
|
* otherwise checkride's own doctor reports the slot/adapter table. Only rows
|
|
446
|
-
* checkride marks `required` count as problems
|
|
451
|
+
* checkride marks `required` count as problems; an empty slot ("no tool detected")
|
|
447
452
|
* is informational, not a failure, because the runtime gate already refuses a
|
|
448
453
|
* vacuous run.
|
|
449
454
|
*/
|
|
@@ -474,7 +479,7 @@ async function gateReport(cwd, env) {
|
|
|
474
479
|
failed += 1;
|
|
475
480
|
}
|
|
476
481
|
// Called out where the human can see it coming: no CODE checks detected means
|
|
477
|
-
// checkpoints would be gated only by checkride's always-on repo checks
|
|
482
|
+
// checkpoints would be gated only by checkride's always-on repo checks: either
|
|
478
483
|
// a vacuous refusal or, worse, a green that tested nothing. Informational, with
|
|
479
484
|
// the exact fix; `start` surfaces the same probe at plan time.
|
|
480
485
|
if (!gateDetectsTools(report.checks)) {
|
|
@@ -490,7 +495,8 @@ async function gateReport(cwd, env) {
|
|
|
490
495
|
}
|
|
491
496
|
/**
|
|
492
497
|
* One slot line: `✓ types ← tsc (6.0.3)`, `✗ dead ← fallow` with its install
|
|
493
|
-
* hint, or `○ spell
|
|
498
|
+
* hint, or `○ spell` joined by an em dash to "no tool detected" for an empty
|
|
499
|
+
* (skipping) slot.
|
|
494
500
|
*/
|
|
495
501
|
function toolRow(c) {
|
|
496
502
|
if (c.adapter === null || c.adapter === undefined) {
|
|
@@ -507,8 +513,8 @@ function toolRow(c) {
|
|
|
507
513
|
* The latch is `checkpoint`'s refusal to land a step until the harness has recorded
|
|
508
514
|
* a human turn since the step was entered. `.plumbbob/TURN` holds a count once the
|
|
509
515
|
* UserPromptSubmit hook has ticked at least once, so a present count means the latch
|
|
510
|
-
* is armed
|
|
511
|
-
* state (a host with no hooks simply has no ledger), never a failure
|
|
516
|
+
* is armed; its absence means the tick is guidance-only. Dormant is a legitimate
|
|
517
|
+
* state (a host with no hooks simply has no ledger), never a failure, so this
|
|
512
518
|
* section adds nothing to the problem count. The dormant hint names the hook because
|
|
513
519
|
* a missing ledger almost always means it never wired, and that hook rides in either
|
|
514
520
|
* install kind (the marketplace plugin's hooks/hooks.json or an init-style link).
|
|
@@ -525,9 +531,9 @@ function latchReport(cwd) {
|
|
|
525
531
|
: ` ✓ latch: live (turn ${turn})`;
|
|
526
532
|
const lines = ['', 'plumbbob doctor — approval latch — D64 (approval-latch)', line];
|
|
527
533
|
// The latch ignores a settings `auto`: a model can write that file, and a grant
|
|
528
|
-
// the model can forge is no grant
|
|
534
|
+
// the model can forge is no grant: self-approval comes only from the human's
|
|
529
535
|
// typed `/build --auto`. Surface a set value so a human relying on it isn't
|
|
530
|
-
// silently changed
|
|
536
|
+
// silently changed: informational, never a problem.
|
|
531
537
|
if (resolveBoolean(root, 'auto', false)) {
|
|
532
538
|
lines.push(' ○ auto: set in settings but not a grant since D67 (auto-not-a-grant) — self-approve per run with `/plumbbob:build --auto`');
|
|
533
539
|
}
|
package/dist/verbs/finish.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// `plumbbob finish
|
|
1
|
+
// `plumbbob finish`: the close-out. It appends the checkpoint SHAs and per-step
|
|
2
2
|
// stats to report.md, makes the final commit, and clears the control state (the
|
|
3
3
|
// untracked session and step markers under `.plumbbob/`). The build folder is NOT
|
|
4
|
-
// deleted
|
|
4
|
+
// deleted; the tracked `.plumbbob/builds/<slug>/` folder IS the archive: it
|
|
5
5
|
// merges with the branch and shows up in the PR, so nothing is copied into a
|
|
6
|
-
// separate local archive. A missing report never blocks the exit
|
|
6
|
+
// separate local archive. A missing report never blocks the exit: guidance
|
|
7
7
|
// offers the artifact, it does not wall the door. The git footprint stays
|
|
8
8
|
// additive: one forward commit under the Conventional `chore(<scope>): finish`
|
|
9
9
|
// subject, with the `plumbbob finish` identifier riding a marker line at the head
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { commit, findRepoRoot, isDirty, stageAll } from "../lib/git.js";
|
|
14
|
+
import { readCommitBody } from "../lib/commitbody.js";
|
|
14
15
|
import { buildScope, checkpointsPath, clearTick, hasSession, intentPath, readStats, refreshExcludes, reportPath, resolveBuild, seamPath, setGrant, sidecarDir, spikePath, stepPath, } from "../lib/sidecar.js";
|
|
15
16
|
import { conventionalSubject, withMarker } from "../lib/commitmsg.js";
|
|
16
17
|
import { parseBuildScope } from "../lib/intent.js";
|
|
@@ -19,7 +20,7 @@ import { parseBuildScope } from "../lib/intent.js";
|
|
|
19
20
|
*
|
|
20
21
|
* Requires an active session (the `.plumbbob/STATE` sentinel). When report.md
|
|
21
22
|
* exists it gains a `## Checkpoints` SHA list and a `## Stats` table first; when
|
|
22
|
-
* it doesn't, finish notes the absence and proceeds
|
|
23
|
+
* it doesn't, finish notes the absence and proceeds; the exit is never gated on
|
|
23
24
|
* a report.
|
|
24
25
|
*/
|
|
25
26
|
export function finish(cwd, args = []) {
|
|
@@ -28,6 +29,15 @@ export function finish(cwd, args = []) {
|
|
|
28
29
|
process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
|
|
29
30
|
return 1;
|
|
30
31
|
}
|
|
32
|
+
// Checked before any of finish's writes: appendCheckpointShas/appendStats
|
|
33
|
+
// are plain appends, not idempotent, so a refusal after they ran would
|
|
34
|
+
// duplicate those sections on retry. A socket stdin can never deliver the
|
|
35
|
+
// requested body, so there is nothing to do first.
|
|
36
|
+
const bodyResult = readCommitBody(args);
|
|
37
|
+
if (!bodyResult.ok) {
|
|
38
|
+
process.stderr.write(bodyResult.message);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
31
41
|
const { build: slug } = resolveBuild(root, args);
|
|
32
42
|
if (existsSync(reportPath(root, slug))) {
|
|
33
43
|
appendCheckpointShas(root, slug);
|
|
@@ -37,9 +47,9 @@ export function finish(cwd, args = []) {
|
|
|
37
47
|
process.stderr.write('plumbbob: note — no report.md found; finishing without one ' +
|
|
38
48
|
'(/plumbbob:finish normally writes the report first). No gate — D9 (finish-no-gate).\n');
|
|
39
49
|
}
|
|
40
|
-
// The final commit: stage the report just written plus the build folder's tail
|
|
41
|
-
// a step's commit sweeps its own bookkeeping along with the work, so the last
|
|
42
|
-
// step's `checkpoints` line lands one commit late and finish absorbs it
|
|
50
|
+
// The final commit: stage the report just written plus the build folder's tail
|
|
51
|
+
// (a step's commit sweeps its own bookkeeping along with the work, so the last
|
|
52
|
+
// step's `checkpoints` line lands one commit late and finish absorbs it) and
|
|
43
53
|
// commit under the `finish` subject. `--allow-empty` (via `commit`) still marks
|
|
44
54
|
// the narrative endpoint when the tree is already clean, or under `--local`,
|
|
45
55
|
// where the whole sidecar is git-excluded and there is nothing tracked to stage.
|
|
@@ -47,18 +57,18 @@ export function finish(cwd, args = []) {
|
|
|
47
57
|
if (isDirty(root)) {
|
|
48
58
|
stageAll(root);
|
|
49
59
|
}
|
|
50
|
-
const sha = commit(root, subject(root, slug), withMarker('plumbbob finish',
|
|
60
|
+
const sha = commit(root, subject(root, slug), withMarker('plumbbob finish', bodyResult.body ?? undefined));
|
|
51
61
|
// Clear the control state: the in-flight markers first, then the session
|
|
52
62
|
// sentinel last (so "no session" flips exactly at the end). Deleting STATE also
|
|
53
|
-
// drops the active-build cursor
|
|
54
|
-
// single delete does both. The tracked artifacts stay in place
|
|
63
|
+
// drops the active-build cursor: cursor and session share that one file, so a
|
|
64
|
+
// single delete does both. The tracked artifacts stay in place; only the
|
|
55
65
|
// ephemera go.
|
|
56
66
|
rmSync(seamPath(root, slug), { force: true });
|
|
57
67
|
rmSync(stepPath(root, slug), { force: true });
|
|
58
68
|
rmSync(spikePath(root, slug), { force: true });
|
|
59
69
|
// The checkpoint latch's per-build entry stamp and the one-turn self-approval
|
|
60
70
|
// grant go with the session: a grant lives one turn by construction, but only
|
|
61
|
-
// because every tick rewrites it
|
|
71
|
+
// because every tick rewrites it: the session's last tick was the last rewrite,
|
|
62
72
|
// so left behind, a stale `auto` could self-approve the next session's first
|
|
63
73
|
// landing.
|
|
64
74
|
clearTick(root, slug);
|
|
@@ -74,7 +84,7 @@ export function finish(cwd, args = []) {
|
|
|
74
84
|
*
|
|
75
85
|
* The scope resolves through the same build-default fallback chain as the plan
|
|
76
86
|
* and step subjects: the intent.md `**Scope:**` header field, else the build
|
|
77
|
-
* slug, else bare (
|
|
87
|
+
* slug, else bare (for example `--local`). The `plumbbob finish` identifier rides the
|
|
78
88
|
* body marker, not the subject.
|
|
79
89
|
*/
|
|
80
90
|
function subject(root, slug) {
|
|
@@ -83,7 +93,7 @@ function subject(root, slug) {
|
|
|
83
93
|
/**
|
|
84
94
|
* Resolve the commit scope: the intent.md `**Scope:**` header, else the slug.
|
|
85
95
|
*
|
|
86
|
-
* Twin of checkpoint.ts's helper
|
|
96
|
+
* Twin of checkpoint.ts's helper: same fallback chain, but with this verb's own
|
|
87
97
|
* `slug` resolution instead of the active-build cursor.
|
|
88
98
|
*/
|
|
89
99
|
function buildDefaultScope(root, slug) {
|
|
@@ -94,38 +104,15 @@ function buildDefaultScope(root, slug) {
|
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
catch {
|
|
97
|
-
// no intent.md
|
|
107
|
+
// no intent.md: fall through to the slug rung.
|
|
98
108
|
}
|
|
99
109
|
return buildScope(slug);
|
|
100
110
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Read the `--body` final-commit body from stdin, or null.
|
|
103
|
-
*
|
|
104
|
-
* The body arrives as a single-quoted stdin heredoc — the CLI owns every commit
|
|
105
|
-
* subject, but the finish skill composes a proportional close-out body this
|
|
106
|
-
* way. Returns null when the flag is absent or stdin is empty; the commit then
|
|
107
|
-
* carries subject only. Reading fd 0 blocks until EOF, which the heredoc
|
|
108
|
-
* supplies; a read error (no stdin attached) degrades to null, and an
|
|
109
|
-
* interactive TTY — which would never send EOF — skips the read instead of
|
|
110
|
-
* hanging (twin of checkpoint.ts's guard).
|
|
111
|
-
*/
|
|
112
|
-
function bodyArg(args) {
|
|
113
|
-
if (!args.includes('--body') || process.stdin.isTTY === true) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
const raw = readFileSync(0, 'utf8').trimEnd();
|
|
118
|
-
return raw.length > 0 ? raw : null;
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
111
|
/**
|
|
125
112
|
* Roll the per-step receipts into report.md as a `## Stats` table.
|
|
126
113
|
*
|
|
127
|
-
* One row per step plus totals
|
|
128
|
-
* wall-clock
|
|
114
|
+
* One row per step plus totals (red checks, drift warnings, reverts,
|
|
115
|
+
* wall-clock) so "is the loop worth it?" is a table, not a feeling. Silently
|
|
129
116
|
* skipped when nothing accrued (a build with no red/revert/drift and no
|
|
130
117
|
* `build <n>` stamps has nothing to say).
|
|
131
118
|
*/
|
|
@@ -173,7 +160,7 @@ function wallClockMs(s) {
|
|
|
173
160
|
/**
|
|
174
161
|
* Format a wall-clock duration for the stats table.
|
|
175
162
|
*
|
|
176
|
-
*
|
|
163
|
+
* An em dash when unknown (a hand-built step never ran `build <n>`, so it has no
|
|
177
164
|
* startedAt), `<1m` under a minute, whole minutes otherwise.
|
|
178
165
|
*/
|
|
179
166
|
function formatWall(ms) {
|
package/dist/verbs/handoff.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// `plumbbob handoff
|
|
1
|
+
// `plumbbob handoff`: print the standardized build hand-off block: the
|
|
2
2
|
// "state / choice / what's next" the human sees at each step boundary. Read-only,
|
|
3
|
-
// no state change. It derives the moment from the session
|
|
3
|
+
// no state change. It derives the moment from the session: a step in flight ⇒ the
|
|
4
4
|
// pause block (built → looks-good/needs-work → next), none ⇒ the post-checkpoint
|
|
5
|
-
// boundary block
|
|
5
|
+
// boundary block, and renders the next undone step plus its advisory `- model:`
|
|
6
6
|
// recommendation straight from intent.md, the build's plan document. Owning the
|
|
7
7
|
// block here, rather than as prose in the build skill, keeps the skill from
|
|
8
8
|
// drifting out of sync with `status`, which renders the same next-step detail
|
|
@@ -75,7 +75,7 @@ function boundaryBlock(step, title, nextUp) {
|
|
|
75
75
|
* The forward pointer, shared by both blocks: the next undone step, its title,
|
|
76
76
|
* and its advisory `- model:` recommendation.
|
|
77
77
|
*
|
|
78
|
-
* The recommendation renders as just the model token for the `/model` call
|
|
78
|
+
* The recommendation renders as just the model token for the `/model` call;
|
|
79
79
|
* the full rationale lives on the `status` dashboard. No next step ⇒ the
|
|
80
80
|
* finish/step nudge instead.
|
|
81
81
|
*/
|
|
@@ -101,8 +101,9 @@ function titleOf(steps, n) {
|
|
|
101
101
|
return steps.find((s) => s.n === n)?.title ?? null;
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
* The model token from a `- model:` recommendation
|
|
105
|
-
* `model: opus
|
|
104
|
+
* The model token from a `- model:` recommendation: the first word, so a
|
|
105
|
+
* `model: opus` line followed by an em dash and rationale yields `opus` for
|
|
106
|
+
* the `/model` call.
|
|
106
107
|
*
|
|
107
108
|
* Null when there is no recommendation, or it degraded to whitespace (folding
|
|
108
109
|
* the null model into the same guard keeps both branches live).
|
|
@@ -112,7 +113,7 @@ function modelToken(model) {
|
|
|
112
113
|
return first !== undefined && first.length > 0 ? first : null;
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
115
|
-
* Read a file as UTF-8, or return '' when it is missing or unreadable
|
|
116
|
+
* Read a file as UTF-8, or return '' when it is missing or unreadable: an
|
|
116
117
|
* absent sidecar file reads as empty rather than throwing.
|
|
117
118
|
*/
|
|
118
119
|
function readOr(path) {
|
|
@@ -124,7 +125,7 @@ function readOr(path) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
/**
|
|
127
|
-
* The in-flight step number from the STEP marker
|
|
128
|
+
* The in-flight step number from the STEP marker: the flat one-line file in
|
|
128
129
|
* the build folder that records which step is open. Null when no step is in
|
|
129
130
|
* flight or the file holds anything but a bare number.
|
|
130
131
|
*/
|
package/dist/verbs/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// `plumbbob init [--uninstall] [--force]
|
|
1
|
+
// `plumbbob init [--uninstall] [--force]`: link plumbbob into Claude Code as an
|
|
2
2
|
// in-place skills-directory plugin. It symlinks the installed package into
|
|
3
|
-
// ~/.claude/skills/plumbbob, where Claude Code discovers it as `plumbbob@skills-dir
|
|
4
|
-
//
|
|
3
|
+
// ~/.claude/skills/plumbbob, where Claude Code discovers it as `plumbbob@skills-dir`:
|
|
4
|
+
// the skills load namespaced (`/plumbbob:*`) and the post-edit hook auto-registers
|
|
5
5
|
// from hooks/hooks.json. This non-marketplace install path is first-class by design:
|
|
6
6
|
// it serves an `npm i -g` global, local dev, and clients predating plugins, and it
|
|
7
7
|
// keeps plumbbob usable as a standalone CLI outside any one host rather than
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// via bin/, so it needs neither `npm i -g` nor `init`). The two are mutually
|
|
11
11
|
// exclusive: both register a plugin named `plumbbob`, and a double-install collides
|
|
12
12
|
// over the /plumbbob:* namespace (skills can drop to flat `/status` names). So init
|
|
13
|
-
// REFUSES when a marketplace plumbbob is already installed
|
|
13
|
+
// REFUSES when a marketplace plumbbob is already installed: `--force` overrides
|
|
14
14
|
// (the dev-install path uses it). Global-only by design: install scope is NOT session
|
|
15
|
-
// scope
|
|
15
|
+
// scope: sessions stay per-project via `plumbbob start`. Idempotent + reversible
|
|
16
16
|
// (`--uninstall` drops the link); it NEVER writes settings.json. Node builtins only.
|
|
17
17
|
import { lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from 'node:fs';
|
|
18
18
|
import { homedir } from 'node:os';
|
|
@@ -20,7 +20,7 @@ import { join } from 'node:path';
|
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { marketplacePlumbbob } from "../lib/plugins.js";
|
|
22
22
|
/**
|
|
23
|
-
* The installed package root
|
|
23
|
+
* The installed package root: parent of .claude-plugin/, skills/, hooks/, dist/.
|
|
24
24
|
*
|
|
25
25
|
* Derived from this module's URL: the global install when run as the published
|
|
26
26
|
* bin, the checkout in dev. No trailing slash, so it compares clean against a
|
|
@@ -64,8 +64,8 @@ function symlinkTarget(path) {
|
|
|
64
64
|
/**
|
|
65
65
|
* Link the installed package into ~/.claude/skills/plumbbob, or `--uninstall` it.
|
|
66
66
|
*
|
|
67
|
-
* Refuses when a marketplace plumbbob plugin is already installed
|
|
68
|
-
* named `plumbbob` collide over the /plumbbob:* namespace
|
|
67
|
+
* Refuses when a marketplace plumbbob plugin is already installed (two plugins
|
|
68
|
+
* named `plumbbob` collide over the /plumbbob:* namespace) unless `--force`.
|
|
69
69
|
* Repoints a stale link from an earlier install location; already-linked is a
|
|
70
70
|
* clean exit 0.
|
|
71
71
|
*/
|
|
@@ -95,7 +95,7 @@ export function init(args) {
|
|
|
95
95
|
process.stdout.write(`plumbbob: already linked (${link} → ${target}). Nothing to do.\n`);
|
|
96
96
|
return 0;
|
|
97
97
|
}
|
|
98
|
-
rmSync(link); // a stale link from an earlier install location
|
|
98
|
+
rmSync(link); // a stale link from an earlier install location: repoint it
|
|
99
99
|
}
|
|
100
100
|
symlinkSync(target, link);
|
|
101
101
|
process.stdout.write(`plumbbob: linked ${link} → ${target}.\n` +
|
package/dist/verbs/park.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// `plumbbob park "<text>"
|
|
1
|
+
// `plumbbob park "<text>"`: the raw capture path for a thought you don't want
|
|
2
2
|
// to chase mid-step: append one flat line under the "## Park list" section of
|
|
3
|
-
// build-log.md (the build's human-facing ledger). Capture stays dumb by design
|
|
3
|
+
// build-log.md (the build's human-facing ledger). Capture stays dumb by design:
|
|
4
4
|
// a grep-readable append, no markdown parsing, no model turn (wording the line
|
|
5
5
|
// well is /plumbbob:park's job); triage waits for a step boundary, via harvest.
|
|
6
6
|
// Capture is not a state transition, so it runs in any context (terminal or
|
|
@@ -14,7 +14,7 @@ import { appendToSection } from "../lib/buildlog.js";
|
|
|
14
14
|
*
|
|
15
15
|
* All non-flag args join into a single line. Refuses when there is no active
|
|
16
16
|
* session, when the text is empty, or when build-log.md lacks a "## Park list"
|
|
17
|
-
* section
|
|
17
|
+
* section: a park line must never vanish silently.
|
|
18
18
|
*/
|
|
19
19
|
export function park(cwd, args) {
|
|
20
20
|
const root = findRepoRoot(cwd);
|
package/dist/verbs/recover.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// `plumbbob recover
|
|
1
|
+
// `plumbbob recover`: read the control plane and say, plainly, whether it is
|
|
2
2
|
// telling the truth. Nothing else reads these files as a set: `doctor` checks
|
|
3
3
|
// the install and the gate, `status` reports what the markers claim without
|
|
4
4
|
// questioning them. So a session that crashed, lost its context window, or was
|
|
5
|
-
// switched away mid-step can sit in a state no verb detects
|
|
5
|
+
// switched away mid-step can sit in a state no verb detects: the worst being a
|
|
6
6
|
// cursor pointing at a build that no longer exists, which renders a perfectly
|
|
7
7
|
// plausible empty dashboard instead of an error.
|
|
8
8
|
//
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// `doctor --migrate` takes. What `--fix` may touch is deliberately narrow: only
|
|
11
11
|
// the untracked control files this tool owns and can rewrite from what it
|
|
12
12
|
// already knows. It never touches a tracked artifact (intent, build-log,
|
|
13
|
-
// checkpoints, reports), never touches git, and never advances the loop
|
|
13
|
+
// checkpoints, reports), never touches git, and never advances the loop: a
|
|
14
14
|
// recovery verb that could land a step would be a second, quieter checkpoint.
|
|
15
15
|
//
|
|
16
16
|
// Spike leftovers are reported and never removed: those worktrees live outside
|
|
@@ -26,7 +26,7 @@ import { spikeBranches, spikeWorktrees } from "./spike.js";
|
|
|
26
26
|
* ones that can be repaired without judgment.
|
|
27
27
|
*
|
|
28
28
|
* Exits 0 when the control plane is consistent (or when every problem found was
|
|
29
|
-
* fixed), 1 while any problem is still standing
|
|
29
|
+
* fixed), 1 while any problem is still standing, so a scripted caller can tell
|
|
30
30
|
* "clean" from "you still have to look at this".
|
|
31
31
|
*/
|
|
32
32
|
export function recover(cwd, args) {
|
|
@@ -75,7 +75,7 @@ function summary(standing, fixable, fix) {
|
|
|
75
75
|
* Everything that depends on there being a session: the cursor, the phase
|
|
76
76
|
* markers, and the per-build control files.
|
|
77
77
|
*
|
|
78
|
-
* With no session there is no control plane to be wrong about
|
|
78
|
+
* With no session there is no control plane to be wrong about: only the spike
|
|
79
79
|
* leftovers, which outlive `finish` and are checked separately.
|
|
80
80
|
*/
|
|
81
81
|
function sessionFindings(root) {
|
|
@@ -101,7 +101,7 @@ function sessionFindings(root) {
|
|
|
101
101
|
*
|
|
102
102
|
* This is the quiet one: `status` takes the cursor at its word, so every read
|
|
103
103
|
* comes back empty and the dashboard renders as a fresh, untouched build rather
|
|
104
|
-
* than an error. Repairable only when exactly one real build remains
|
|
104
|
+
* than an error. Repairable only when exactly one real build remains; with
|
|
105
105
|
* several, which one you meant is a judgment call.
|
|
106
106
|
*/
|
|
107
107
|
function cursorFinding(root, slug) {
|
|
@@ -128,7 +128,7 @@ function cursorFinding(root, slug) {
|
|
|
128
128
|
* Contradictions in what phase the build claims to be in.
|
|
129
129
|
*
|
|
130
130
|
* Phase is derived from the markers rather than stored, so two markers at once
|
|
131
|
-
* is not an impossible state
|
|
131
|
+
* is not an impossible state; it is an unreadable one, and `status` resolves
|
|
132
132
|
* it by showing the spike and hiding the step entirely.
|
|
133
133
|
*/
|
|
134
134
|
function phaseFindings(root, slug, stepInFlight) {
|
|
@@ -159,7 +159,7 @@ function phaseFindings(root, slug, stepInFlight) {
|
|
|
159
159
|
name: 'step marker',
|
|
160
160
|
ok: false,
|
|
161
161
|
detail: `step ${marker} is in flight but the plan has no step ${marker} (planned: ${planned.join(', ')})`,
|
|
162
|
-
hint: 'the plan was rewritten under the step — sharpen it with /plumbbob:step, or plumbbob
|
|
162
|
+
hint: 'the plan was rewritten under the step — sharpen it with /plumbbob:step, plumbbob revert to drop the step and its work, or plumbbob abandon to drop the step but keep the work',
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
}
|
|
@@ -175,7 +175,7 @@ function phaseFindings(root, slug, stepInFlight) {
|
|
|
175
175
|
*/
|
|
176
176
|
function leftoverFindings(root, slug, stepInFlight) {
|
|
177
177
|
const findings = [];
|
|
178
|
-
// The handoff ledger is step-scoped and cleared by `checkpoint` alone
|
|
178
|
+
// The handoff ledger is step-scoped and cleared by `checkpoint` alone; a
|
|
179
179
|
// revert or an abandoned step leaves it for the next step to inherit.
|
|
180
180
|
const handoffs = handoffCount(root, slug);
|
|
181
181
|
if (!stepInFlight && handoffs > 0) {
|
|
@@ -209,7 +209,7 @@ function leftoverFindings(root, slug, stepInFlight) {
|
|
|
209
209
|
else {
|
|
210
210
|
findings.push({ name: 'latch tick', ok: true, detail: stepInFlight ? 'stamped for the step in flight' : 'clear' });
|
|
211
211
|
}
|
|
212
|
-
// GRANT is rewritten on every turn tick, so it self-clears
|
|
212
|
+
// GRANT is rewritten on every turn tick, so it self-clears, but only while
|
|
213
213
|
// the hook runs. Without a TURN ledger nothing will ever clear it, and the
|
|
214
214
|
// latch reads a standing grant as self-approval.
|
|
215
215
|
if (existsSync(grantPath(root)) && readTurn(root) === null) {
|
|
@@ -276,7 +276,7 @@ function handoffCount(root, slug) {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
/**
|
|
279
|
-
* A control marker's trimmed content, or '?' when it cannot be read
|
|
279
|
+
* A control marker's trimmed content, or '?' when it cannot be read: a marker
|
|
280
280
|
* exists to be reported on, so an unreadable one must not throw mid-report.
|
|
281
281
|
*/
|
|
282
282
|
function readMarker(path) {
|
|
@@ -284,7 +284,7 @@ function readMarker(path) {
|
|
|
284
284
|
return raw.length > 0 ? raw : '?';
|
|
285
285
|
}
|
|
286
286
|
/**
|
|
287
|
-
* File content, or '' when absent
|
|
287
|
+
* File content, or '' when absent: recover reads state that may be missing by
|
|
288
288
|
* definition, so every read degrades rather than throws.
|
|
289
289
|
*/
|
|
290
290
|
function readOr(path) {
|