plumbbob 0.4.4 → 0.4.11

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.
@@ -1,16 +1,18 @@
1
1
  // `plumbbob checkpoint [<n>] [-m <msg>]` — the executor-agnostic commit tick (D3).
2
- // Unlike v1 `done`, it does NOT require BUILD state or a STEP file: the step is
3
- // whatever you pass, else the in-flight STEP, else the next undone step in intent.
4
- // It gates on a green check, then commits any pending work (or records the existing
5
- // HEAD when the tree is already clean — the human's commit skill may have committed
6
- // first), records the SHA, flips the intent checkbox to `[x]`, clears any STEP/SEAM,
7
- // and returns to DESIGN. The diff's author is irrelevant: `/plumbbob:build`, your hands,
8
- // a vibe session, or another harness all checkpoint the same way.
2
+ // Unlike v1 `done`, it does NOT require a STEP file: the step is whatever you pass,
3
+ // else the in-flight STEP, else the next undone step in intent. It gates on a green
4
+ // check, then commits any pending work (or records the existing HEAD when the tree
5
+ // is already clean — the human's commit skill may have committed first), records the
6
+ // SHA, flips the intent checkbox to `[x]`, and clears any STEP/SEAM (which returns
7
+ // the dashboard to the DESIGN boundary). The diff's author is irrelevant:
8
+ // `/plumbbob:pb-build`, your hands, a vibe session, or another harness all checkpoint
9
+ // the same way.
9
10
  import { appendFileSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
11
  import { commit, findRepoRoot, headSha, isDirty, stageAll } from "../lib/git.js";
11
- import { checkpointsPath, hasSession, intentPath, seamPath, stepPath, writeState } from "../lib/sidecar.js";
12
+ import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, stepPath } from "../lib/sidecar.js";
12
13
  import { runCheck } from "../lib/check.js";
13
14
  import { markStepDone, parseSteps } from "../lib/orient.js";
15
+ import { appendToSection, checkpointLogLine } from "../lib/buildlog.js";
14
16
  export function checkpoint(cwd, args) {
15
17
  const root = findRepoRoot(cwd);
16
18
  if (root === null || !hasSession(root)) {
@@ -36,10 +38,10 @@ export function checkpoint(cwd, args) {
36
38
  }
37
39
  appendFileSync(checkpointsPath(root), `step ${step} ${sha}\n`);
38
40
  flipIntent(root, step);
41
+ logCheckpoint(root, step, sha);
39
42
  rmSync(seamPath(root), { force: true });
40
43
  rmSync(stepPath(root), { force: true });
41
- writeState(root, 'DESIGN');
42
- process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. STATE=DESIGN.\n`);
44
+ process.stdout.write(`plumbbob: step ${step} checkpointed — ${sha.slice(0, 9)}. Back at the boundary.\n`);
43
45
  return 0;
44
46
  }
45
47
  // Step resolution (D3): explicit arg > in-flight STEP file > first undone step in
@@ -68,6 +70,32 @@ function flipIntent(root, step) {
68
70
  // best-effort bookkeeping; the checkpoint SHA is the source of truth.
69
71
  }
70
72
  }
73
+ // Append a dated line to the build-log's `## Log` so the build's history accrues at
74
+ // each checkpoint instead of being reconstructed at wrap. The step's title is lifted
75
+ // from intent.md when still present. Best-effort: a missing/odd build-log never blocks
76
+ // a checkpoint — the `checkpoints` SHA is the source of truth.
77
+ function logCheckpoint(root, step, sha) {
78
+ try {
79
+ const path = buildLogPath(root);
80
+ const date = new Date().toISOString().slice(0, 10);
81
+ const line = checkpointLogLine(date, step, sha, titleForStep(root, step));
82
+ const updated = appendToSection(readFileSync(path, 'utf8'), 'Log', line);
83
+ if (updated !== null) {
84
+ writeFileSync(path, updated);
85
+ }
86
+ }
87
+ catch {
88
+ // best-effort ledger; never fail a checkpoint over the build-log.
89
+ }
90
+ }
91
+ function titleForStep(root, step) {
92
+ try {
93
+ return parseSteps(readFileSync(intentPath(root), 'utf8')).find((s) => s.n === step)?.title ?? null;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
71
99
  function messageArg(args) {
72
100
  const i = args.indexOf('-m');
73
101
  return i !== -1 && i + 1 < args.length ? (args[i + 1] ?? null) : null;
@@ -4,12 +4,13 @@
4
4
  // in place as a plugin (skills `/plumbbob:*`, the post-edit hook from hooks.json).
5
5
  // doctor verifies the link resolves to a package carrying the manifest, the skills,
6
6
  // and the hook — and names the fix for anything missing. The failure class it
7
- // exists for is SILENT (a `/plumbbob:status` that opens an empty dashboard because the
7
+ // exists for is SILENT (a `/plumbbob:pb-status` that opens an empty dashboard because the
8
8
  // plugin never linked). Functional, node builtins only (C1/C2).
9
9
  import { existsSync, lstatSync, readdirSync, readlinkSync } from 'node:fs';
10
10
  import { homedir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
+ import { marketplacePlumbbob } from "../lib/plugins.js";
13
14
  // The plumbbob package's own skills/ dir (the canonical set), off this module's
14
15
  // URL so it resolves the same from src/ (dev) and dist/ (published).
15
16
  function packageDir(name) {
@@ -58,8 +59,25 @@ export function doctor() {
58
59
  const link = join(home, '.claude', 'skills', 'plumbbob');
59
60
  const shipped = listSkills(packageDir('skills'));
60
61
  const pkg = linkedPackage(link);
61
- const checks = pkg === null ? [{ ok: false, label: `not linked — no plugin at ${link}`, fix: 'run: plumbbob init' }] : buildChecks(link, pkg, shipped);
62
- const out = ['plumbbob doctor — global plugin install'];
62
+ const market = marketplacePlumbbob(home);
63
+ let checks;
64
+ if (pkg === null) {
65
+ checks =
66
+ market.length > 0
67
+ ? [{ ok: true, label: `installed via marketplace (${market.join(', ')}) — skills load as /plumbbob:*, the CLI is on PATH from the plugin bin/. No init needed.` }]
68
+ : [{ ok: false, label: `not linked — no plugin at ${link}`, fix: 'install the marketplace plugin (/plugin install plumbbob@<marketplace>) or run: plumbbob init' }];
69
+ }
70
+ else {
71
+ checks = buildChecks(link, pkg, shipped);
72
+ if (market.length > 0) {
73
+ checks.unshift({
74
+ ok: false,
75
+ label: `collision — also installed via marketplace (${market.join(', ')}); two plugins named plumbbob fight over /plumbbob:* and skills can drop to flat names`,
76
+ fix: 'keep one — `plumbbob init --uninstall` to use the marketplace plugin, or `/plugin uninstall` the marketplace one to keep this link',
77
+ });
78
+ }
79
+ }
80
+ const out = ['plumbbob doctor — plugin install'];
63
81
  for (const c of checks) {
64
82
  out.push(c.ok ? ` ✓ ${c.label}` : ` ✗ ${c.label}\n → ${c.fix}`);
65
83
  }
@@ -68,7 +86,7 @@ export function doctor() {
68
86
  out.push(failed === 0
69
87
  ? 'plumbbob: all checks passed. If a skill still misbehaves, restart Claude Code (or /reload-plugins).'
70
88
  : `plumbbob: ${failed} problem(s) — apply the → fixes, then restart Claude Code.`);
71
- out.push('plumbbob: skills shell a bare `plumbbob` ensure it is on PATH (`npm i -g plumbbob`). Sessions are per-project via `plumbbob start`.');
89
+ out.push('plumbbob: skills shell a bare `plumbbob`. The marketplace plugin puts it on PATH only inside a Claude Code session (via bin/) — there is no terminal `plumbbob`; for one (or the skills-dir install) run `npm i -g plumbbob`. In-session you can also run this as `/plumbbob:pb-doctor`. Sessions are per-project via `plumbbob start`.');
72
90
  process.stdout.write(`${out.join('\n')}\n`);
73
91
  return failed === 0 ? 0 : 1;
74
92
  }
@@ -1,16 +1,24 @@
1
- // `plumbbob init [--uninstall]` — link plumbbob into Claude Code as an in-place
2
- // skills-directory plugin. This is the whole install: it symlinks the installed
3
- // package into ~/.claude/skills/plumbbob, where Claude Code discovers it as
4
- // `plumbbob@skills-dir` — the skills load namespaced (`/plumbbob:*`) and the
5
- // post-edit hook auto-registers from hooks/hooks.json. Global-only by design:
6
- // plumbbob is a personal tool (like firecrawl/gh), and install scope is NOT
7
- // session scope sessions stay per-project via `plumbbob start`. Idempotent +
8
- // reversible (`--uninstall` drops the link); it NEVER writes settings.json.
9
- // Functional, node builtins only (C1/C2).
1
+ // `plumbbob init [--uninstall] [--force]` — link plumbbob into Claude Code as an
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
+ // — the skills load namespaced (`/plumbbob:*`) and the post-edit hook auto-registers
5
+ // from hooks/hooks.json. This is the deliberate non-marketplace install path — kept
6
+ // first-class, not deprecated: it serves an `npm i -g` global, local dev, and clients
7
+ // predating plugins, and keeps plumbbob usable as a standalone CLI (and, later, under
8
+ // other agents) rather than Claude-marketplace-only. The marketplace plugin is the
9
+ // co-equal self-contained alternative (it ships skills AND the `plumbbob` CLI on PATH
10
+ // via bin/, so it needs neither `npm i -g` nor `init`). The two are mutually
11
+ // exclusive: both register a plugin named `plumbbob`, and a double-install collides
12
+ // over the /plumbbob:* namespace (skills can drop to flat `/pb-status` names). So init
13
+ // REFUSES when a marketplace plumbbob is already installed — `--force` overrides
14
+ // (the dev-install path uses it). Global-only by design: install scope is NOT session
15
+ // scope — sessions stay per-project via `plumbbob start`. Idempotent + reversible
16
+ // (`--uninstall` drops the link); it NEVER writes settings.json. Node builtins only.
10
17
  import { lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from 'node:fs';
11
18
  import { homedir } from 'node:os';
12
19
  import { join } from 'node:path';
13
20
  import { fileURLToPath } from 'node:url';
21
+ import { marketplacePlumbbob } from "../lib/plugins.js";
14
22
  // The installed package root (parent of .claude-plugin/, skills/, hooks/, dist/),
15
23
  // off this module's URL — the global install when run as the published bin, the
16
24
  // checkout in dev. No trailing slash, so it compares clean against a readlink.
@@ -47,6 +55,14 @@ export function init(args) {
47
55
  if (args.includes('--uninstall')) {
48
56
  return uninstall(link);
49
57
  }
58
+ const market = marketplacePlumbbob(home);
59
+ if (market.length > 0 && !args.includes('--force')) {
60
+ process.stderr.write(`plumbbob: a marketplace plumbbob plugin is already installed (${market.join(', ')}).\n` +
61
+ 'plumbbob: it already provides the skills (`/plumbbob:*`) and the `plumbbob` CLI on PATH (the plugin bin/) — no `plumbbob init` needed.\n' +
62
+ `plumbbob: linking ${link} would register a SECOND plugin named \`plumbbob\`; the two collide over the /plumbbob:* namespace and skills can drop to flat names (\`/pb-status\`).\n` +
63
+ 'plumbbob: to use the skills-dir link instead, first remove the marketplace one (`/plugin uninstall plumbbob@<marketplace>`), or re-run with `--force` if you know what you are doing.\n');
64
+ return 1;
65
+ }
50
66
  mkdirSync(join(home, '.claude', 'skills'), { recursive: true });
51
67
  if (present(link)) {
52
68
  const current = symlinkTarget(link);
@@ -1,9 +1,10 @@
1
1
  // `plumbbob park "<text>"` — the dumb capture path: append one raw line under
2
- // the build-log's Park list. No model turn, no composition (that is /park's job).
2
+ // the build-log's Park list. No model turn, no composition (that is /plumbbob:pb-park's job).
3
3
  // Capture is not a transition, so it runs in any context (terminal or in-session).
4
4
  import { readFileSync, writeFileSync } from 'node:fs';
5
5
  import { findRepoRoot } from "../lib/git.js";
6
6
  import { hasSession, buildLogPath } from "../lib/sidecar.js";
7
+ import { appendToSection } from "../lib/buildlog.js";
7
8
  export function park(cwd, args) {
8
9
  const root = findRepoRoot(cwd);
9
10
  if (root === null || !hasSession(root)) {
@@ -19,7 +20,7 @@ export function park(cwd, args) {
19
20
  return 1;
20
21
  }
21
22
  const path = buildLogPath(root);
22
- const updated = insertParkItem(readFileSync(path, 'utf8'), text);
23
+ const updated = appendToSection(readFileSync(path, 'utf8'), 'Park list', `- [ ] ${text}`);
23
24
  if (updated === null) {
24
25
  process.stderr.write('plumbbob: could not find a "## Park list" section in build-log.md.\n');
25
26
  return 1;
@@ -28,24 +29,3 @@ export function park(cwd, args) {
28
29
  process.stdout.write(`parked: ${text}\n`);
29
30
  return 0;
30
31
  }
31
- // Append after the last non-blank line of the Park list section (i.e. just before
32
- // the next `## ` heading). Returns null if there is no Park list to append to.
33
- function insertParkItem(content, text) {
34
- const lines = content.split('\n');
35
- const headingIdx = lines.findIndex((line) => line.trim() === '## Park list');
36
- if (headingIdx === -1) {
37
- return null;
38
- }
39
- let nextSection = lines.findIndex((line, i) => i > headingIdx && line.startsWith('## '));
40
- if (nextSection === -1) {
41
- nextSection = lines.length;
42
- }
43
- let insertAt = headingIdx + 1;
44
- for (let i = headingIdx + 1; i < nextSection; i++) {
45
- if ((lines[i] ?? '').trim() !== '') {
46
- insertAt = i + 1;
47
- }
48
- }
49
- lines.splice(insertAt, 0, `- [ ] ${text}`);
50
- return lines.join('\n');
51
- }
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os';
13
13
  import { join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { findRepoRoot, resetHard, untrackedPaths } from "../lib/git.js";
16
- import { checkpointsPath, hasSession, seamPath, sidecarDir, stepPath, writeState } from "../lib/sidecar.js";
16
+ import { checkpointsPath, hasSession, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
17
17
  import { matchesSeam } from "../lib/intent.js";
18
18
  export function revert(cwd, args) {
19
19
  const root = findRepoRoot(cwd);
@@ -53,8 +53,7 @@ export function revert(cwd, args) {
53
53
  }
54
54
  rmSync(seamPath(root), { force: true });
55
55
  rmSync(stepPath(root), { force: true });
56
- writeState(root, 'DESIGN');
57
- process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — STATE=DESIGN. Park lines and intent edits were preserved.\n`);
56
+ process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — back at the boundary. Park lines and intent edits were preserved.\n`);
58
57
  return 0;
59
58
  }
60
59
  // The repo paths that belong to plumbbob, not to the work being reverted: the
@@ -1,10 +1,10 @@
1
1
  // `plumbbob spike` (D18) — the spike lifecycle for a genuine fork the design
2
2
  // phase couldn't settle. `spike "<slug>" [opt…]` creates a sibling git worktree +
3
3
  // `spike/<slug>-<opt>` branch per option OUTSIDE the repo root (default opts a/b)
4
- // and sets STATE=SPIKE; the main tree stays DESIGN-locked while you experiment in
5
- // the worktrees, which are hook-dormant by construction — the untracked sidecar
6
- // (D17) doesn't exist in a fresh checkout, so the hooks find no STATE there.
7
- // `spike done` removes every spike worktree + branch and returns to DESIGN.
4
+ // and drops the SPIKE marker; the main tree stays put while you experiment in the
5
+ // worktrees, which are hook-dormant by construction — the untracked sidecar (D17)
6
+ // doesn't exist in a fresh checkout, so the hooks find no STATE there.
7
+ // `spike done` removes every spike worktree + branch and clears the marker.
8
8
  //
9
9
  // Worktree git calls run directly here rather than via lib/git.ts (which holds the
10
10
  // shared additive read/commit helpers): worktree management is spike-local, and
@@ -13,7 +13,7 @@ import { execFileSync } from 'node:child_process';
13
13
  import { existsSync } from 'node:fs';
14
14
  import { basename, dirname, join } from 'node:path';
15
15
  import { findRepoRoot } from "../lib/git.js";
16
- import { hasSession, readState, writeState } from "../lib/sidecar.js";
16
+ import { hasSession, inSpike, markSpike, clearSpike, stepPath } from "../lib/sidecar.js";
17
17
  const DEFAULT_OPTIONS = ['a', 'b'];
18
18
  export function spike(cwd, args) {
19
19
  const root = findRepoRoot(cwd);
@@ -28,14 +28,13 @@ export function spike(cwd, args) {
28
28
  return spikeStart(root, positionals);
29
29
  }
30
30
  function spikeStart(root, positionals) {
31
- const state = readState(root);
32
- if (state === 'SPIKE') {
31
+ if (inSpike(root)) {
33
32
  process.stderr.write('plumbbob: already in a spike. Run `plumbbob spike done` to close it first.\n');
34
33
  return 1;
35
34
  }
36
- if (state !== 'DESIGN') {
37
- process.stderr.write(`plumbbob: spike starts from DESIGN (current state is ${state ?? 'UNKNOWN'}). ` +
38
- 'A spike is a deliberate fork — close the current step first with `done`.\n');
35
+ if (existsSync(stepPath(root))) {
36
+ process.stderr.write('plumbbob: spike starts from a settled boundary, but a step is in flight. ' +
37
+ 'A spike is a deliberate fork — checkpoint or revert the current step first.\n');
39
38
  return 1;
40
39
  }
41
40
  const slug = sanitize(positionals[0] ?? '');
@@ -55,16 +54,15 @@ function spikeStart(root, positionals) {
55
54
  git(root, ['worktree', 'add', '-b', `spike/${slug}-${opt}`, path, 'HEAD']);
56
55
  created.push(path);
57
56
  }
58
- writeState(root, 'SPIKE');
59
- process.stdout.write(`plumbbob: STATE=SPIKE — the main tree stays DESIGN-locked. Experiment in the throwaway worktrees:\n${created
57
+ markSpike(root);
58
+ process.stdout.write(`plumbbob: spiking — the main tree stays put. Experiment in the throwaway worktrees:\n${created
60
59
  .map((p) => ` ${p}`)
61
60
  .join('\n')}\nWhen you've decided, record the verdict in intent.md and run \`plumbbob spike done\`.\n`);
62
61
  return 0;
63
62
  }
64
63
  function spikeDone(root) {
65
- const state = readState(root);
66
- if (state !== 'SPIKE') {
67
- process.stderr.write(`plumbbob: no active spike (state is ${state ?? 'UNKNOWN'}).\n`);
64
+ if (!inSpike(root)) {
65
+ process.stderr.write('plumbbob: no active spike to close.\n');
68
66
  return 1;
69
67
  }
70
68
  for (const path of spikeWorktrees(root)) {
@@ -74,8 +72,8 @@ function spikeDone(root) {
74
72
  for (const branch of spikeBranches(root)) {
75
73
  git(root, ['branch', '-D', branch]);
76
74
  }
77
- writeState(root, 'DESIGN');
78
- process.stdout.write('plumbbob: spike closed — STATE=DESIGN, worktrees and branches removed. ' +
75
+ clearSpike(root);
76
+ process.stdout.write('plumbbob: spike closed — worktrees and branches removed, back at the boundary. ' +
79
77
  'Record the verdict (which option won, and why) in intent.md before you `build`.\n');
80
78
  return 0;
81
79
  }
@@ -1,10 +1,10 @@
1
- // `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, enter
2
- // DESIGN. Refuses on a dirty tree (D22), an existing session, or a non-git dir.
1
+ // `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, open
2
+ // the session. Refuses on a dirty tree (D22), an existing session, or a non-git dir.
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
7
- import { sidecarDir, checkpointsPath, configPath, intentPath, buildLogPath, writeState, hasSession, excludeSidecar, } from "../lib/sidecar.js";
7
+ import { sidecarDir, checkpointsPath, configPath, intentPath, buildLogPath, beginSession, hasSession, excludeSidecar, } from "../lib/sidecar.js";
8
8
  const DEFAULT_CHECK = 'pnpm run check';
9
9
  export function start(cwd, args) {
10
10
  const positionals = args.filter((a) => !a.startsWith('--'));
@@ -37,7 +37,7 @@ export function start(cwd, args) {
37
37
  const sha = headSha(root);
38
38
  const check = detectCheck(root);
39
39
  mkdirSync(sidecarDir(root), { recursive: true });
40
- writeState(root, 'DESIGN');
40
+ beginSession(root);
41
41
  writeFileSync(checkpointsPath(root), `baseline ${sha}\n`);
42
42
  writeFileSync(configPath(root), `check=${check.command}\n`);
43
43
  writeFileSync(intentPath(root), stamp(readTemplate('intent.md'), title, check.command));
@@ -46,7 +46,7 @@ export function start(cwd, args) {
46
46
  if (check.warn) {
47
47
  process.stderr.write(`plumbbob: WARNING the heavy check '${check.command}' is not defined in this repo's package.json. Edit .plumbbob/config (check=...) to set the real gate before \`review\`/\`done\`.\n`);
48
48
  }
49
- process.stdout.write(`plumbbob: started "${title}" — STATE=DESIGN, baseline ${sha.slice(0, 9)}. Frame and decide in .plumbbob/intent.md; flip to BUILD only once the decisions are made.\n`);
49
+ process.stdout.write(`plumbbob: started "${title}" — baseline ${sha.slice(0, 9)}. Frame and decide in .plumbbob/intent.md; \`build\` a step once the decisions are made.\n`);
50
50
  return 0;
51
51
  }
52
52
  // D24: default the heavy check to `pnpm run check`; warn (but still record it)
@@ -3,7 +3,7 @@
3
3
  // behavior, so the `NO ACTIVE SESSION` sentinel is kept exact.
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { findRepoRoot } from "../lib/git.js";
6
- import { buildLogPath, checkpointsPath, hasSession, intentPath, readState, stepPath } from "../lib/sidecar.js";
6
+ import { buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, stepPath } from "../lib/sidecar.js";
7
7
  import { formatOrientation, orient } from "../lib/orient.js";
8
8
  function readOr(path) {
9
9
  try {
@@ -21,11 +21,11 @@ export function status(cwd) {
21
21
  }
22
22
  const inFlightRaw = readOr(stepPath(root)).trim();
23
23
  const orientation = orient({
24
- state: readState(root) ?? 'UNKNOWN',
25
24
  intent: readOr(intentPath(root)),
26
25
  buildLog: readOr(buildLogPath(root)),
27
26
  checkpoints: readOr(checkpointsPath(root)),
28
27
  inFlight: /^\d+$/.test(inFlightRaw) ? Number(inFlightRaw) : null,
28
+ spiking: inSpike(root),
29
29
  });
30
30
  process.stdout.write(`${formatOrientation(orientation)}\n`);
31
31
  return 0;
@@ -1,13 +1,13 @@
1
1
  // `plumbbob wrap` (D9) — the v2 close-out, replacing the v1 four-verb
2
2
  // finish ceremony. It archives intent + build-log + report
3
- // (the `/plumbbob:wrap` skill writes the report by default) under .plumbbob/archive/,
3
+ // (the `/plumbbob:pb-wrap` skill writes the report by default) under .plumbbob/archive/,
4
4
  // clears the active files, and deletes the control state (STATE last). Unlike v1
5
5
  // `finish` there is NO refuse-without-report gate — guidance offers the artifact, it
6
6
  // does not wall the exit. Archive-then-clear, never destroy (C4); git untouched (C5).
7
7
  import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
8
8
  import { join, relative } from 'node:path';
9
9
  import { findRepoRoot } from "../lib/git.js";
10
- import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, stepPath } from "../lib/sidecar.js";
10
+ import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, spikePath, stepPath } from "../lib/sidecar.js";
11
11
  import { archiveSession, reportPath } from "../lib/archive.js";
12
12
  export function wrap(cwd) {
13
13
  const root = findRepoRoot(cwd);
@@ -20,7 +20,7 @@ export function wrap(cwd) {
20
20
  }
21
21
  else {
22
22
  process.stderr.write('plumbbob: note — no report.md found; archiving intent + build-log without one ' +
23
- '(/plumbbob:wrap normally writes the report first). No gate (D9).\n');
23
+ '(/plumbbob:pb-wrap normally writes the report first). No gate (D9).\n');
24
24
  }
25
25
  const archived = archiveSession(root);
26
26
  // Clear the active files — now safely archived — then the control state, STATE
@@ -30,9 +30,10 @@ export function wrap(cwd) {
30
30
  rmSync(reportPath(root), { force: true });
31
31
  rmSync(seamPath(root), { force: true });
32
32
  rmSync(stepPath(root), { force: true });
33
+ rmSync(spikePath(root), { force: true });
33
34
  rmSync(join(sidecarDir(root), 'STATE'), { force: true });
34
35
  process.stdout.write(`plumbbob: wrap — archived to ${relative(root, archived)}. Sidecar cleared. ` +
35
- 'Run `/plumbbob:plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
36
+ 'Run `/plumbbob:pb-plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
36
37
  return 0;
37
38
  }
38
39
  // Append the recorded checkpoints (baseline + each `step n <sha>`) to the report so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumbbob",
3
- "version": "0.4.4",
3
+ "version": "0.4.11",
4
4
  "description": "Attention-first build process: 11 skills + a CLI that keep you the decider — guidance, not enforcement.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -20,11 +20,12 @@
20
20
  "license": "Apache-2.0",
21
21
  "author": "Rob McLarty <hello@robmclarty.com>",
22
22
  "bin": {
23
- "plumbbob": "dist/cli.js",
24
- "pb": "dist/cli.js"
23
+ "plumbbob": "bin/plumbbob",
24
+ "pb": "bin/pb"
25
25
  },
26
26
  "files": [
27
27
  ".claude-plugin",
28
+ "bin",
28
29
  "dist",
29
30
  "hooks",
30
31
  "skills",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: build
2
+ name: pb-build
3
3
  description: The optional engine — read the next planned step from intent, implement it (its done-when, seam, Decisions, Constraints), then verify it through to the approval pause. Skip it to build by hand/vibed/another harness. `--auto` self-approves and chains to done.
4
4
  disable-model-invocation: true
5
5
  model: opus
@@ -12,40 +12,41 @@ Current session state (injected when this skill runs): !`plumbbob status 2>/dev/
12
12
 
13
13
  This is the **bundled executor** — one way to turn a planned step into code. It is
14
14
  **optional** (D3): you can implement any step by hand, in a vibe session, or with
15
- another harness and go straight to `/plumbbob:verify` instead — plumbbob does not care how
15
+ another harness and go straight to `/plumbbob:pb-verify` instead — plumbbob does not care how
16
16
  the diff appeared. When you do run it, it reads the plan, writes the step, and
17
17
  carries straight through to the verify pause.
18
18
 
19
- Since `/plumbbob:plan` lays down the whole step list up front, the happy path is to fire
20
- `/plumbbob:build` once per step until done — each run builds the next undone step and stops
21
- at the pause for your approval. **Re-firing `/plumbbob:build` is itself the clock tick.**
19
+ Since `/plumbbob:pb-plan` lays down the whole step list up front, the happy path is to fire
20
+ `/plumbbob:pb-build` once per step until done — each run builds the next undone step and stops
21
+ at the pause for your approval. **Re-firing `/plumbbob:pb-build` is itself the clock tick.**
22
22
 
23
23
  ## What this skill does, in order
24
24
 
25
- 1. **Pick the step.** Use the number you were invoked with (e.g. `/plumbbob:build 4`), else
25
+ 1. **Pick the step.** Use the number you were invoked with (e.g. `/plumbbob:pb-build 4`), else
26
26
  the next undone, planned step in `.plumbbob/intent.md`. If there is no planned step
27
- to build, stop and tell the human to `/plumbbob:step` first.
27
+ to build, stop and tell the human to `/plumbbob:pb-step` first.
28
28
  2. **Enter the step.** Run `plumbbob build <n>` (records the in-flight STEP +
29
- SEAM so `/plumbbob:status` shows the step in flight; in v2 the seam is awareness, not a
29
+ SEAM so `/plumbbob:pb-status` shows the step in flight; in v2 the seam is awareness, not a
30
30
  lock).
31
31
  3. **Read the plan.** Read the step's **done-when**, its **seam**, and the
32
32
  **Decisions** and **Constraints** in `intent.md`. Build to *that* — the deciding
33
33
  already happened, off the chat.
34
34
  4. **Implement** the step, and only that step, staying within the declared seam. A
35
- new problem or "ooh what if" that surfaces mid-build is a `/plumbbob:park`, **not** an
35
+ new problem or "ooh what if" that surfaces mid-build is a `/plumbbob:pb-park`, **not** an
36
36
  edit — capture it and stay on the step. If you genuinely cannot finish without
37
37
  touching more than the seam, that is scope drift: surface it to the human rather
38
38
  than sprawling.
39
- 5. **Verify, through to the pause.** Run the verify tick exactly as `/plumbbob:verify`
39
+ 5. **Verify, through to the pause.** Run the verify tick exactly as `/plumbbob:pb-verify`
40
40
  does: `plumbbob check` → self-review the diff against the done-when, the
41
41
  Decisions, and the Constraints (a single structured read, D16) → validate → **PAUSE
42
42
  for the human's approval** → only on approval, checkpoint with
43
- `plumbbob checkpoint`. Do **not** bump the version or changelogthat is
44
- the human's `/version` call.
43
+ `plumbbob checkpoint` (which also appends this step to the build-log's `## Log`the
44
+ history writes itself at each checkpoint, so you don't hand-log it). Do **not** bump
45
+ the version or changelog — that is the human's `/version` call.
45
46
 
46
47
  ## `--auto` — let the agent be the clock (opt-in)
47
48
 
48
- `/plumbbob:build --auto` is the explicit escape hatch when the human wants unattended
49
+ `/plumbbob:pb-build --auto` is the explicit escape hatch when the human wants unattended
49
50
  progress instead of approving each step. It does the same work, but **the agent reviews
50
51
  and approves in the human's place**, and it **chains**:
51
52
 
@@ -61,10 +62,10 @@ human asked for it by name. The default — no flag — always ends at the pause
61
62
 
62
63
  ## The hard contracts
63
64
 
64
- - **Optional, never required.** The loop works without this skill; `/plumbbob:verify`
65
+ - **Optional, never required.** The loop works without this skill; `/plumbbob:pb-verify`
65
66
  checkpoints a hand-built or vibed diff just the same (D3).
66
67
  - **Build the decided step, not a new one.** Implement what `intent.md` settled. A
67
- new idea mid-build is a `/plumbbob:park`, not an edit.
68
+ new idea mid-build is a `/plumbbob:pb-park`, not an edit.
68
69
  - **Default ends at the pause.** Implement → verify → wait for approval; never
69
70
  checkpoint without it. Only an explicit `--auto` lets the agent approve in your place,
70
71
  and it still halts on a red check or any mismatch.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: pb-doctor
3
+ description: Diagnose the plugin install from inside a session — is plumbbob linked, are the skills and hook present, is there a collision. A thin trigger for `plumbbob doctor`. Matters most for a marketplace install, where the CLI is on PATH only inside Claude Code.
4
+ disable-model-invocation: true
5
+ model: haiku
6
+ allowed-tools: Bash(plumbbob doctor:*)
7
+ ---
8
+
9
+ # Plumbbob — doctor (the is-it-installed-right move)
10
+
11
+ Install diagnostic (injected when this skill runs): !`if command -v plumbbob >/dev/null 2>&1; then plumbbob doctor 2>&1; else echo "plumbbob CLI not on PATH in this session. Marketplace install: confirm the plugin is enabled in /plugin, then /reload-plugins. Skills-dir/global install: npm i -g plumbbob && plumbbob init."; fi`
12
+
13
+ This is a **thin driver** for `plumbbob doctor`. The report above is read-only — `doctor`
14
+ inspects, it never writes. This skill carries **no Edit and no Write tool**: never edit a file
15
+ to change what the report says. If a check failed, surface its `→ fix` verbatim and apply
16
+ *that* fix, then have the human **restart Claude Code (or `/reload-plugins`)**. **Never retry.**
17
+
18
+ ## Why a skill (and not just the terminal)
19
+
20
+ The CLI's reach differs by install path, and that is the whole point of running `doctor` here:
21
+
22
+ - **Marketplace plugin** — `plumbbob` is on PATH **only inside a Claude Code session** (its
23
+ `bin/` shims are injected into the Bash tool while the plugin is enabled). There is **no
24
+ terminal `plumbbob`** to run. So this skill — or letting me run `plumbbob doctor` in-session
25
+ — is the *only* way to reach `doctor`. The fact that this skill loaded at all already proves
26
+ the plugin is enabled and the shim is on PATH.
27
+ - **Skills-dir / global install** (`npm i -g plumbbob` + `plumbbob init`) — `plumbbob` is on
28
+ PATH everywhere, so terminal `plumbbob doctor` works too; this skill is just convenience.
29
+
30
+ The one failure `doctor` **cannot** diagnose: a plugin that never loaded at all (no
31
+ `/plumbbob:*` skill appears). That is a `/plugin` / `/reload-plugins` problem, not doctor's —
32
+ and if you are reading this, that is not your situation.
33
+
34
+ ## What it does
35
+
36
+ 1. Surface the injected `doctor` report verbatim — every `✓` and any `✗` with its `→ fix`.
37
+ 2. If all checks passed, say so; if a skill still misbehaves, the fix is a restart, not a re-run.
38
+ 3. If a check failed, apply its named `→ fix`, then restart Claude Code (or `/reload-plugins`).
39
+ 4. If the line above says the CLI is not on PATH, follow the install-path branch it printed.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: harvest
2
+ name: pb-harvest
3
3
  description: Triage the park list at a step boundary — propose one class (blocker/tangent/pivot) per parked item, write only after the human confirms each, record under ## Harvest, and fold a confirmed blocker into intent.
4
4
  disable-model-invocation: true
5
5
  model: opus
@@ -10,18 +10,19 @@ allowed-tools: Read, Edit, Bash(plumbbob status:*)
10
10
 
11
11
  Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
12
12
 
13
- `/plumbbob:harvest` is the complement of `/plumbbob:park` (D7): you parked ideas as seeds during a
13
+ `/plumbbob:pb-harvest` is the complement of `/plumbbob:pb-park` (D7): you parked ideas as seeds during a
14
14
  build; now, at a boundary, you harvest them — decide what each one is.
15
15
 
16
- ## When to run it — wrong-state refusal
16
+ ## When to run it — boundary only
17
17
 
18
- Harvest at a **step boundary**: after a step is checkpointed and you are back in
19
- DESIGN, not mid-step. Read the state injected above:
18
+ Harvest at a **step boundary**: after a step is checkpointed and you are back at the
19
+ DESIGN boundary, not mid-step. Read the dashboard injected above:
20
20
 
21
21
  - `NO ACTIVE SESSION` — **refuse**; tell the human to `plumbbob start "<title>"` first.
22
- - `BUILD` — a step is in flight; **refuse** and suggest finishing it with `/plumbbob:verify`
23
- before harvesting. Chasing parked items mid-step is the disease parking prevents.
24
- - `DESIGN` (and other settled states) go ahead.
22
+ - A step in flight (the dashboard reads `[BUILD]`, and `next →` points at finishing the
23
+ step) **refuse** and suggest finishing it with `/plumbbob:pb-verify` before harvesting.
24
+ Chasing parked items mid-step is the disease parking prevents.
25
+ - At the boundary (the dashboard reads `[DESIGN]`) — go ahead.
25
26
 
26
27
  ## What this skill does
27
28
 
@@ -40,7 +41,7 @@ one line of reasoning, then **wait for the human to confirm or override**. Write
40
41
  **only after** per-item confirmation:
41
42
 
42
43
  - Record each confirmed class in `build-log.md`'s `## Harvest` section.
43
- - **Flip the harvested item** from `- [ ]` to `- [x]` in the Park list, so `/plumbbob:status`
44
+ - **Flip the harvested item** from `- [ ]` to `- [x]` in the Park list, so `/plumbbob:pb-status`
44
45
  stops counting it as open.
45
46
  - A confirmed **blocker** also folds its decision into `intent.md`.
46
47
  - Never reclassify or resolve an item the human hasn't confirmed, and default every
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: park
2
+ name: pb-park
3
3
  description: Compose one tidy tagged park line, get the human's OK in-turn, then capture it by shelling `plumbbob park` — never by editing a file. The capture half of the park/harvest loop.
4
4
  disable-model-invocation: true
5
5
  model: haiku
@@ -10,16 +10,16 @@ allowed-tools: Bash(plumbbob status:*), Bash(plumbbob park:*)
10
10
 
11
11
  Current session state (injected when this skill runs): !`plumbbob status 2>/dev/null || echo "plumbbob CLI not found - install the dep and re-run: npm i -g plumbbob && plumbbob init"`
12
12
 
13
- `/plumbbob:park` is the **capture** half of the loop; `/plumbbob:harvest` is where parked items
13
+ `/plumbbob:pb-park` is the **capture** half of the loop; `/plumbbob:pb-harvest` is where parked items
14
14
  get triaged later (D7). Capturing the instant an idea arrives — instead of acting on
15
15
  it — is the whole point: it protects the step in flight.
16
16
 
17
17
  ## Wrong-state refusal
18
18
 
19
- Parking needs an **active session** to capture into. Read the state injected above: if
19
+ Parking needs an **active session** to capture into. Read the dashboard injected above: if
20
20
  it is `NO ACTIVE SESSION`, **refuse** and tell the human to run `plumbbob start
21
- "<title>"` first. Every active state (`DESIGN`, `BUILD`, `SPIKE`, `FINISH`) is fine
22
- capture is always available, which is the whole point of parking.
21
+ "<title>"` first. Any time the session is live at the boundary, mid-step, or in a spike
22
+ is fine; capture is always available, which is the whole point of parking.
23
23
 
24
24
  ## What this skill does
25
25