plumbbob 0.4.13 → 0.5.3
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 +168 -70
- package/dist/cli-core.js +24 -17
- package/dist/cli.js +1 -1
- package/dist/lib/buildlog.js +2 -2
- package/dist/lib/check.js +89 -20
- package/dist/lib/git.js +35 -9
- package/dist/lib/intent.js +21 -2
- package/dist/lib/orient.js +2 -2
- package/dist/lib/settings.js +71 -0
- package/dist/lib/sidecar.js +142 -32
- package/dist/verbs/build.js +7 -6
- package/dist/verbs/check.js +39 -5
- package/dist/verbs/checkpoint.js +145 -9
- package/dist/verbs/doctor.js +266 -26
- package/dist/verbs/finish.js +101 -0
- package/dist/verbs/revert.js +20 -14
- package/dist/verbs/spike.js +12 -11
- package/dist/verbs/start.js +45 -21
- package/dist/verbs/status.js +18 -7
- package/dist/verbs/use.js +44 -0
- package/hooks/post-edit.sh +9 -3
- package/package.json +12 -11
- package/skills/pb-build/SKILL.md +10 -4
- package/skills/{pb-wrap → pb-finish}/SKILL.md +20 -15
- package/skills/pb-plan/SKILL.md +9 -2
- package/skills/pb-verify/SKILL.md +24 -7
- package/templates/build-log.md +3 -3
- package/templates/intent.md +2 -2
- package/dist/lib/archive.js +0 -72
- package/dist/verbs/wrap.js +0 -55
package/dist/verbs/start.js
CHANGED
|
@@ -4,12 +4,18 @@ 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,
|
|
8
|
-
|
|
7
|
+
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, } from "../lib/sidecar.js";
|
|
8
|
+
import { settingsPath, setLocalSetting } from "../lib/settings.js";
|
|
9
|
+
// D24/D32: the gate is checkride unless a `check` setting overrides it, so
|
|
10
|
+
// settings.json seeds with no `check` key at all — absence IS the default. The
|
|
11
|
+
// templates' {{CHECK}} line is a human-readable echo of that.
|
|
12
|
+
const CHECK_ECHO = 'checkride (set a "check" key in .plumbbob/settings.json to override)';
|
|
9
13
|
export function start(cwd, args) {
|
|
10
14
|
const positionals = args.filter((a) => !a.startsWith('--'));
|
|
11
15
|
const allowDirty = args.includes('--allow-dirty');
|
|
16
|
+
const local = args.includes('--local');
|
|
12
17
|
const title = (positionals[0] ?? '').trim();
|
|
18
|
+
const slug = (flagValue(args, '--slug') ?? slugify(title)).trim();
|
|
13
19
|
if (title.length === 0) {
|
|
14
20
|
process.stderr.write('plumbbob: start needs a title. Try: plumbbob start "what you are building".\n');
|
|
15
21
|
return 1;
|
|
@@ -34,31 +40,49 @@ export function start(cwd, args) {
|
|
|
34
40
|
}
|
|
35
41
|
process.stderr.write('plumbbob: WARNING --allow-dirty: recording HEAD as baseline with a dirty tree. A later revert-to-baseline will DISCARD the uncommitted work.\n');
|
|
36
42
|
}
|
|
43
|
+
if (!local && slug.length === 0) {
|
|
44
|
+
process.stderr.write(`plumbbob: could not derive a build slug from "${title}". Pass --slug <name>, or retitle with letters or digits.\n`);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (!local && listBuilds(root).includes(slug)) {
|
|
48
|
+
process.stderr.write(`plumbbob: a build named "${slug}" already exists in .plumbbob/builds/. Retitle, or pass --slug <name> to choose another.\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
37
51
|
const sha = headSha(root);
|
|
38
|
-
const check = detectCheck(root);
|
|
39
52
|
mkdirSync(sidecarDir(root), { recursive: true });
|
|
40
53
|
beginSession(root);
|
|
41
|
-
writeFileSync(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
writeFileSync(settingsPath(root), `${JSON.stringify({ auto: false }, null, 2)}\n`);
|
|
55
|
+
// D26: `--local` keeps today's fully-untracked flat layout (whole `.plumbbob/`
|
|
56
|
+
// excluded); the default plants a tracked, PR-riding `builds/<slug>/` folder
|
|
57
|
+
// (D26) and points the per-worktree cursor at it (D28), excluding only control
|
|
58
|
+
// files (D17). The slug is validated unique above — the CLI refuses, never
|
|
59
|
+
// suffixes (D38).
|
|
60
|
+
let intentLocation;
|
|
61
|
+
if (local) {
|
|
62
|
+
writeFileSync(checkpointsPath(root), `baseline ${sha}\n`);
|
|
63
|
+
writeFileSync(intentPath(root), stamp(readTemplate('intent.md'), title, CHECK_ECHO));
|
|
64
|
+
writeFileSync(buildLogPath(root), stamp(readTemplate('build-log.md'), title, CHECK_ECHO));
|
|
65
|
+
excludeSidecar(root);
|
|
66
|
+
intentLocation = '.plumbbob/intent.md';
|
|
48
67
|
}
|
|
49
|
-
|
|
68
|
+
else {
|
|
69
|
+
const dir = buildDir(root, slug);
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
writeFileSync(join(dir, 'checkpoints'), `baseline ${sha}\n`);
|
|
72
|
+
writeFileSync(join(dir, 'intent.md'), stamp(readTemplate('intent.md'), title, CHECK_ECHO));
|
|
73
|
+
writeFileSync(join(dir, 'build-log.md'), stamp(readTemplate('build-log.md'), title, CHECK_ECHO));
|
|
74
|
+
setLocalSetting(root, 'activeBuild', slug);
|
|
75
|
+
excludeControl(root);
|
|
76
|
+
intentLocation = `.plumbbob/builds/${slug}/intent.md`;
|
|
77
|
+
}
|
|
78
|
+
process.stdout.write(`plumbbob: started "${title}" — baseline ${sha.slice(0, 9)}. Frame and decide in ${intentLocation}; \`build\` a step once the decisions are made.\n`);
|
|
50
79
|
return 0;
|
|
51
80
|
}
|
|
52
|
-
//
|
|
53
|
-
// when the
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return { command: DEFAULT_CHECK, warn: pkg.scripts?.check === undefined };
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return { command: DEFAULT_CHECK, warn: true };
|
|
61
|
-
}
|
|
81
|
+
// Read the value that follows a `--flag` in argv (e.g. `--slug my-build`), or
|
|
82
|
+
// undefined when the flag is absent or trails with no value.
|
|
83
|
+
function flagValue(args, flag) {
|
|
84
|
+
const i = args.indexOf(flag);
|
|
85
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
62
86
|
}
|
|
63
87
|
function readTemplate(name) {
|
|
64
88
|
return readFileSync(fileURLToPath(new URL(`../../templates/${name}`, import.meta.url)), 'utf8');
|
package/dist/verbs/status.js
CHANGED
|
@@ -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, inSpike, intentPath, stepPath } from "../lib/sidecar.js";
|
|
6
|
+
import { buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, listBuilds, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
7
7
|
import { formatOrientation, orient } from "../lib/orient.js";
|
|
8
8
|
function readOr(path) {
|
|
9
9
|
try {
|
|
@@ -13,19 +13,30 @@ function readOr(path) {
|
|
|
13
13
|
return '';
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
export function status(cwd) {
|
|
16
|
+
export function status(cwd, args = []) {
|
|
17
17
|
const root = findRepoRoot(cwd);
|
|
18
18
|
if (root === null || !hasSession(root)) {
|
|
19
19
|
process.stdout.write('NO ACTIVE SESSION\n');
|
|
20
20
|
return 0;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
// A session with builds but no resolvable cursor (finish cleared it, or the repo
|
|
23
|
+
// holds several builds and none is active) has no single dashboard to show, so
|
|
24
|
+
// list the builds and point at `use` instead of rendering a broken, empty one.
|
|
25
|
+
const { build: slug } = resolveBuild(root, args);
|
|
26
|
+
if (slug === null) {
|
|
27
|
+
const builds = listBuilds(root);
|
|
28
|
+
if (builds.length > 0) {
|
|
29
|
+
process.stdout.write(`NO ACTIVE BUILD — pick one with \`plumbbob use <slug>\`:\n${builds.map((b) => ` ${b}`).join('\n')}\n`);
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const inFlightRaw = readOr(stepPath(root, slug)).trim();
|
|
23
34
|
const orientation = orient({
|
|
24
|
-
intent: readOr(intentPath(root)),
|
|
25
|
-
buildLog: readOr(buildLogPath(root)),
|
|
26
|
-
checkpoints: readOr(checkpointsPath(root)),
|
|
35
|
+
intent: readOr(intentPath(root, slug)),
|
|
36
|
+
buildLog: readOr(buildLogPath(root, slug)),
|
|
37
|
+
checkpoints: readOr(checkpointsPath(root, slug)),
|
|
27
38
|
inFlight: /^\d+$/.test(inFlightRaw) ? Number(inFlightRaw) : null,
|
|
28
|
-
spiking: inSpike(root),
|
|
39
|
+
spiking: inSpike(root, slug),
|
|
29
40
|
});
|
|
30
41
|
process.stdout.write(`${formatOrientation(orientation)}\n`);
|
|
31
42
|
return 0;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// `plumbbob use <slug>` (D30) — re-point the per-worktree cursor at an existing
|
|
2
|
+
// build and resume it. Both switching and resuming are the same one word (Q10,
|
|
3
|
+
// `nvm use`-shaped): the cursor is a single scalar key in settings.local.json, so
|
|
4
|
+
// pointing it elsewhere IS the switch and one-active-per-worktree holds by
|
|
5
|
+
// construction (D28). It validates the target folder exists, and warns — but
|
|
6
|
+
// allows — leaving a build with a step in flight: that surviving in-flight state
|
|
7
|
+
// is D26's whole payoff, resumed the next time you `use` that build.
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { findRepoRoot } from "../lib/git.js";
|
|
10
|
+
import { activeBuild, hasSession, intentPath, listBuilds, stepPath } from "../lib/sidecar.js";
|
|
11
|
+
import { setLocalSetting } from "../lib/settings.js";
|
|
12
|
+
export function use(cwd, args) {
|
|
13
|
+
const root = findRepoRoot(cwd);
|
|
14
|
+
if (root === null || !hasSession(root)) {
|
|
15
|
+
process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
const builds = listBuilds(root);
|
|
19
|
+
const target = args.find((a) => !a.startsWith('--'));
|
|
20
|
+
if (target === undefined || target.length === 0) {
|
|
21
|
+
process.stderr.write(`plumbbob: use needs a build slug.${buildsHint(builds)}\n`);
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
// Validate by the folder's intent.md, not the dir alone: an empty `builds/<slug>/`
|
|
25
|
+
// is not a resumable build, and the flat `--local` layout has no builds/ at all.
|
|
26
|
+
if (!existsSync(intentPath(root, target))) {
|
|
27
|
+
process.stderr.write(`plumbbob: no build named "${target}" in .plumbbob/builds/.${buildsHint(builds)}\n`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
const leaving = activeBuild(root);
|
|
31
|
+
if (leaving !== null && leaving !== target && existsSync(stepPath(root, leaving))) {
|
|
32
|
+
process.stderr.write(`plumbbob: note — build "${leaving}" has a step in flight; its in-flight state is preserved ` +
|
|
33
|
+
`and resumes when you \`use ${leaving}\` again.\n`);
|
|
34
|
+
}
|
|
35
|
+
setLocalSetting(root, 'activeBuild', target);
|
|
36
|
+
const resuming = existsSync(stepPath(root, target)) ? ' (a step is in flight — `status` shows where)' : '';
|
|
37
|
+
process.stdout.write(`plumbbob: now on build "${target}"${resuming}. \`status\` to orient.\n`);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
// A trailing " Builds: a, b." hint when any exist, so a bad or missing slug points
|
|
41
|
+
// the user straight at the valid choices.
|
|
42
|
+
function buildsHint(builds) {
|
|
43
|
+
return builds.length > 0 ? ` Builds: ${builds.join(', ')}.` : '';
|
|
44
|
+
}
|
package/hooks/post-edit.sh
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# post-edit.sh — light feedback (D25). PostToolUse, non-blocking, ALWAYS exits 0.
|
|
3
3
|
# Runs file-scoped oxlint + ast-grep on the changed file and injects any failures
|
|
4
|
-
# into the model's context via additionalContext (verified API,
|
|
4
|
+
# into the model's context via additionalContext (verified API, D25). No-ops when
|
|
5
5
|
# the tools are absent or there is no session. tsc is deferred to the heavy tier
|
|
6
6
|
# (D25): it has no true single-file mode and would tax every keystroke.
|
|
7
7
|
|
|
8
|
+
# The repo root is the nearest ancestor whose .plumbbob/settings.local.json carries
|
|
9
|
+
# an `activeBuild` cursor — the per-worktree signal that a tracked build is live
|
|
10
|
+
# here (D28). Grep the same way the file_path read below does: a plain pattern
|
|
11
|
+
# match, no JSON parse. The cursor replaces the old .plumbbob/STATE probe as the
|
|
12
|
+
# "a build is active here" root marker.
|
|
8
13
|
find_root() {
|
|
9
14
|
d=$(pwd -P)
|
|
10
15
|
while [ -n "$d" ]; do
|
|
11
|
-
if [ -f "$d/.plumbbob/
|
|
16
|
+
if [ -f "$d/.plumbbob/settings.local.json" ] &&
|
|
17
|
+
grep -q '"activeBuild"' "$d/.plumbbob/settings.local.json" 2>/dev/null; then
|
|
12
18
|
printf '%s' "$d"
|
|
13
19
|
return 0
|
|
14
20
|
fi
|
|
@@ -18,7 +24,7 @@ find_root() {
|
|
|
18
24
|
return 1
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
root=$(find_root) || exit 0 # no
|
|
27
|
+
root=$(find_root) || exit 0 # no active build here: no feedback
|
|
22
28
|
|
|
23
29
|
input=$(cat)
|
|
24
30
|
path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty' 2>/dev/null)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plumbbob",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Attention-first build process:
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Attention-first build process: 12 skills + a CLI that keep you the decider — guidance, not enforcement.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"claude",
|
|
@@ -43,22 +43,23 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@ast-grep/cli": "^0.43.0",
|
|
46
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
47
|
+
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
46
48
|
"@types/node": "^24.0.0",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
50
|
+
"fallow": "^2.103.0",
|
|
51
|
+
"markdownlint-cli2": "^0.22.1",
|
|
49
52
|
"oxlint": "^1.69.0",
|
|
50
53
|
"typescript": "^6.0.3",
|
|
51
54
|
"vitest": "^4.1.8"
|
|
52
55
|
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"checkride": "0.1.6"
|
|
58
|
+
},
|
|
53
59
|
"scripts": {
|
|
54
60
|
"clean": "rm -rf dist",
|
|
55
61
|
"build": "tsc -p tsconfig.build.json",
|
|
56
|
-
"check": "
|
|
57
|
-
"
|
|
58
|
-
"check:oxlint": "oxlint",
|
|
59
|
-
"check:astgrep": "ast-grep scan",
|
|
60
|
-
"check:test": "vitest run",
|
|
61
|
-
"check:knip": "knip",
|
|
62
|
-
"check:md": "markdownlint --config .markdownlint.jsonc \"**/*.md\" --ignore node_modules --ignore research"
|
|
62
|
+
"check": "checkride",
|
|
63
|
+
"test": "vitest run"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/skills/pb-build/SKILL.md
CHANGED
|
@@ -27,7 +27,7 @@ at the pause for your approval. **Re-firing `/pb-build` is itself the clock tick
|
|
|
27
27
|
the next undone, planned step in `.plumbbob/intent.md`. If there is no planned step
|
|
28
28
|
to build, stop and tell the human to `/pb-step` first.
|
|
29
29
|
2. **Enter the step.** Run `plumbbob build <n>` (records the in-flight STEP +
|
|
30
|
-
SEAM so `/pb-status` shows the step in flight;
|
|
30
|
+
SEAM so `/pb-status` shows the step in flight; the seam is awareness, not a
|
|
31
31
|
lock).
|
|
32
32
|
3. **Read the plan.** Read the step's **done-when**, its **seam**, and the
|
|
33
33
|
**Decisions** and **Constraints** in `intent.md`. Build to *that* — the deciding
|
|
@@ -38,11 +38,17 @@ at the pause for your approval. **Re-firing `/pb-build` is itself the clock tick
|
|
|
38
38
|
touching more than the seam, that is scope drift: surface it to the human rather
|
|
39
39
|
than sprawling.
|
|
40
40
|
5. **Verify, through to the pause.** Run the verify tick exactly as `/pb-verify`
|
|
41
|
-
does: `plumbbob check`
|
|
41
|
+
does: `plumbbob check` (on red, read `.check/summary.json` and the failing slot's
|
|
42
|
+
raw output under `.check/` for the actual diagnostics; while iterating on a fix,
|
|
43
|
+
narrow the loop with `plumbbob check --bail --only <slots>` — the checkpoint gate
|
|
44
|
+
still runs everything) → self-review the diff against the done-when, the
|
|
42
45
|
Decisions, and the Constraints (a single structured read, D16) → validate → **PAUSE
|
|
43
46
|
for the human's approval** → only on approval, checkpoint with
|
|
44
|
-
`plumbbob checkpoint
|
|
45
|
-
|
|
47
|
+
`plumbbob checkpoint <n> --body <<'BODY' … BODY` — a commit body **proportional to the
|
|
48
|
+
step** (a line for a trivial change, a short paragraph for a meatier one; no TIL scan,
|
|
49
|
+
no separate commit skill). The CLI owns the subject and appends this step to the
|
|
50
|
+
build-log's `## Log`, so the history writes itself — you only supply the body (or omit
|
|
51
|
+
`--body` for the deterministic done-when + seam + diffstat fallback). Do **not** bump
|
|
46
52
|
the version or changelog — that is the human's `/version` call.
|
|
47
53
|
|
|
48
54
|
## `--auto` — let the agent be the clock (opt-in)
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: pb-
|
|
3
|
-
description:
|
|
2
|
+
name: pb-finish
|
|
3
|
+
description: Finish the build — write the report (what shipped, decisions, parked/harvested items, deferred tangents), then make the final commit that closes the session. The build folder rides the branch into the PR — no separate archive. Report by default, no gate.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
model: opus
|
|
6
|
-
allowed-tools: Read, Write, Bash(plumbbob status:*), Bash(plumbbob
|
|
6
|
+
allowed-tools: Read, Write, Bash(plumbbob status:*), Bash(plumbbob finish:*)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# PlumbBob —
|
|
9
|
+
# PlumbBob — finish the build (the close-out)
|
|
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
|
-
`/pb-
|
|
14
|
-
|
|
15
|
-
and no separate docs phase.
|
|
13
|
+
`/pb-finish` ends the build: it captures what happened, then makes the final commit
|
|
14
|
+
that closes the session. **Report by default** (D9) — no refuse-without-report gate,
|
|
15
|
+
and no separate docs phase. The build folder is tracked (D26/D29): it merges with the
|
|
16
|
+
branch and shows up in the PR, so there is nothing to archive — the folder *is* the
|
|
17
|
+
record.
|
|
16
18
|
|
|
17
19
|
## What this skill does, in order
|
|
18
20
|
|
|
19
|
-
1. **Write the report** to
|
|
21
|
+
1. **Write the report** to the active build's `report.md`
|
|
22
|
+
(`.plumbbob/builds/<slug>/report.md`), from `intent.md` + `build-log.md`.
|
|
20
23
|
The build-log's `## Log` is already the chronological history — `plumbbob checkpoint`
|
|
21
24
|
wrote a dated line for every step as it landed. **Read it as the spine; do not
|
|
22
25
|
re-narrate it.** The report adds only what the log does not already carry:
|
|
@@ -26,10 +29,11 @@ and no separate docs phase.
|
|
|
26
29
|
- **Final status** — done or partial, and what is left.
|
|
27
30
|
- **Deferred tangents** — the harvested items that become future work.
|
|
28
31
|
This is the "yeah, I did that" artifact. Write it by default; the human may edit it.
|
|
29
|
-
2. **
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
2. **Finish.** Run `plumbbob finish`, which appends the checkpoint SHAs to the
|
|
33
|
+
report and makes the final commit — subject `plumbbob: finish — <title>` (D34),
|
|
34
|
+
with an optional proportional body via `--body` (the D34 stdin heredoc) — then
|
|
35
|
+
clears the control state (markers, the `activeBuild` cursor, STATE last). The
|
|
36
|
+
build folder stays in place, committed, and rides the branch into the PR.
|
|
33
37
|
3. **Point at the next goal** — `/pb-plan` to frame the next one.
|
|
34
38
|
|
|
35
39
|
## The hard contracts
|
|
@@ -37,9 +41,10 @@ and no separate docs phase.
|
|
|
37
41
|
- **Report by default, never a gate.** Always offer the report; never wall the exit. A
|
|
38
42
|
bug fix's report can be three lines — size it to the work.
|
|
39
43
|
- **The log is the history; the report is the synthesis.** `checkpoint` already recorded
|
|
40
|
-
what shipped, step by step —
|
|
44
|
+
what shipped, step by step — finish applies the *unique additions* (the why, the deferred
|
|
41
45
|
tangents, the final status), it does not rewrite the timeline.
|
|
42
|
-
- **
|
|
43
|
-
|
|
46
|
+
- **The folder is the archive, never destroy** (C4/D29). `finish` keeps intent +
|
|
47
|
+
build-log + report in `builds/<slug>/` and commits them; nothing is copied out and
|
|
48
|
+
nothing is deleted but the untracked control markers.
|
|
44
49
|
- **No version bump, no docs phase.** Updating real docs is a separate, explicit ask;
|
|
45
50
|
cutting a release is the human's `/version`.
|
package/skills/pb-plan/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Frame a fresh goal and author the whole plan — Frame, Decisions,
|
|
|
4
4
|
argument-hint: "[spec-path | intent]"
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
model: opus
|
|
7
|
-
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob start:*)
|
|
7
|
+
allowed-tools: Read, Edit, Write, Bash(plumbbob status:*), Bash(plumbbob start:*), Bash(plumbbob checkpoint:*)
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# PlumbBob — plan a goal (the whole-goal move)
|
|
@@ -60,7 +60,14 @@ an agent can follow with `/pb-build`. The argument only seeds how you get there.
|
|
|
60
60
|
paths it touches). Later steps may be fuzzier than the first — that's fine; they get
|
|
61
61
|
sharpened just-in-time when you reach them with `/pb-step`. Keep each small enough to
|
|
62
62
|
verify in one review pass.
|
|
63
|
-
5. **
|
|
63
|
+
5. **Commit the plan.** Once the human approves the frame and steps, run
|
|
64
|
+
`plumbbob checkpoint --plan` to commit the scaffold on its own — subject
|
|
65
|
+
`plumbbob: plan — <title>`, only `.plumbbob/builds/<slug>/`, a `plan <sha>` line in
|
|
66
|
+
`checkpoints` (D36). This keeps the first step's diff clean, so history reads
|
|
67
|
+
baseline → plan → steps. Pass a proportional `--body` (the single-quoted stdin
|
|
68
|
+
heredoc) when the rationale is worth carrying; skip it for a small plan. Do this
|
|
69
|
+
only on the human's approval — the plan is their convergence.
|
|
70
|
+
6. **Offer to stress-test it.** Suggest `/pb-refine` to attack the frame for holes (or
|
|
64
71
|
to repair the plan as it drifts). Optional, the human's call.
|
|
65
72
|
|
|
66
73
|
## The interview (mode 1)
|
|
@@ -16,9 +16,14 @@ this skill verifies it the same way: **it reads the diff, not the author** (D3).
|
|
|
16
16
|
|
|
17
17
|
## What this skill does, in order
|
|
18
18
|
|
|
19
|
-
1. **Check.** Run `plumbbob check` (the heavy gate
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
1. **Check.** Run `plumbbob check` (the heavy gate — checkride unless the repo
|
|
20
|
+
configures a `check` override, D32). If it comes back **red**, stop here: the gate
|
|
21
|
+
names the failing slots and where each tool's raw output landed — read
|
|
22
|
+
`.check/summary.json`, then the failing slot's own file (`.check/<slot>.json` or
|
|
23
|
+
`.check/<slot>.stdout.txt`) for the actual diagnostics instead of scraping
|
|
24
|
+
scrollback. Report what failed and do **not** pause for approval — there is
|
|
25
|
+
nothing to approve yet. The human fixes it and re-invokes. (Exit 2 means the gate
|
|
26
|
+
itself broke — a harness problem to surface, not a code failure.)
|
|
22
27
|
2. **Self-review** *(a single structured read, D16)*. Read `git diff` and
|
|
23
28
|
`.plumbbob/intent.md`, then in one pass check the diff against:
|
|
24
29
|
- the current step's **done-when** criterion — is it actually met?
|
|
@@ -29,10 +34,22 @@ this skill verifies it the same way: **it reads the diff, not the author** (D3).
|
|
|
29
34
|
4. **PAUSE.** Present the check result, the self-review, and the validation, then
|
|
30
35
|
**stop and wait for the human's explicit approval.** This is the convergence beat;
|
|
31
36
|
the human is the clock. Never checkpoint without it.
|
|
32
|
-
5. **Checkpoint** *(only after approval)*.
|
|
33
|
-
commit
|
|
34
|
-
|
|
35
|
-
step
|
|
37
|
+
5. **Checkpoint** *(only after approval)*. Run `plumbbob checkpoint`: it makes the WIP
|
|
38
|
+
commit, records the SHA, flips the step to done, appends the step to the build-log's
|
|
39
|
+
`## Log`, and returns to DESIGN. The CLI owns the commit **subject**
|
|
40
|
+
(`plumbbob: step N — <title>`); you own the **body**. Compose a body *proportional to
|
|
41
|
+
the step* — a one-liner for a trivial change, a short paragraph on the what/why for a
|
|
42
|
+
meatier one — and pass it on stdin:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
plumbbob checkpoint <n> --body <<'BODY'
|
|
46
|
+
<your proportional body — what changed and why, no ceremony>
|
|
47
|
+
BODY
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Do **not** run a TIL scan or reach for a separate commit skill — the body is yours to
|
|
51
|
+
write in one breath. Omit `--body` entirely and the CLI writes a deterministic body
|
|
52
|
+
(done-when + seam + diffstat) on its own. Either way, do **not** bump the version or
|
|
36
53
|
touch the changelog — that is the human's `/version` call.
|
|
37
54
|
|
|
38
55
|
## The hard contracts
|
package/templates/build-log.md
CHANGED
|
@@ -6,7 +6,7 @@ step boundaries. The antidote to "my plan got lost in the noise."
|
|
|
6
6
|
Park list : where ideas go so you do not chase them. CAPTURE, never act inline.
|
|
7
7
|
Harvest : the boundary ritual that keeps you on one branch.
|
|
8
8
|
Log : the build's history. `plumbbob checkpoint` appends a line per step as it
|
|
9
|
-
lands; feeds the /pb-
|
|
9
|
+
lands; feeds the /pb-finish report, which rides the branch into the PR.
|
|
10
10
|
-->
|
|
11
11
|
|
|
12
12
|
# Build log — {{TITLE}}
|
|
@@ -52,5 +52,5 @@ Harvest results this boundary:
|
|
|
52
52
|
every time a step lands — via `/pb-build` or `/pb-verify` — so this
|
|
53
53
|
fills in as you go, not at the end. Add your own decision/event lines too: this is what
|
|
54
54
|
you point at to say "I did that — the LLM helped, but those were my calls."
|
|
55
|
-
`/pb-
|
|
56
|
-
|
|
55
|
+
`/pb-finish` reads this for the report; `plumbbob finish` commits it with the build
|
|
56
|
+
folder, so it rides the branch into the PR.)*
|
package/templates/intent.md
CHANGED
|
@@ -46,8 +46,8 @@ holes `/pb-refine` surfaces, and as blockers fold in during BUILD.)*
|
|
|
46
46
|
|
|
47
47
|
*(The build plan. `/pb-plan` authors the **whole list up front** — each step a small,
|
|
48
48
|
verifiable increment with its own **done-when** and **seam** (the paths it will touch,
|
|
49
|
-
which `/pb-build` records in
|
|
50
|
-
|
|
49
|
+
which `/pb-build` records in the build folder's `SEAM` for orientation — awareness, not a lock).
|
|
50
|
+
Then drive `/pb-build` until done. Later steps may be fuzzier than the first;
|
|
51
51
|
sharpen the next one just-in-time with `/pb-step` (empty input auto-syncs it), and use
|
|
52
52
|
`/pb-refine` to repair the whole plan when a blocker rewrites it.)*
|
|
53
53
|
|
package/dist/lib/archive.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// The finish-phase archive helper (D20: archive is local-only markdown in v1).
|
|
2
|
-
// `finish` copies the three active files into .plumbbob/archive/<date>-<slug>/
|
|
3
|
-
// before clearing the actives — "archive-then-clear, never destroy" (C4).
|
|
4
|
-
// Functional/procedural, node builtins only (C1/C2).
|
|
5
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { sidecarDir, intentPath, buildLogPath } from "./sidecar.js";
|
|
8
|
-
// report.md lives beside intent.md / build-log.md in the sidecar. It is written by
|
|
9
|
-
// /plumbbob-report and is the gate `finish` refuses without (D19). Derived from
|
|
10
|
-
// the exported sidecarDir so this module needs nothing new from sidecar.ts.
|
|
11
|
-
export function reportPath(root) {
|
|
12
|
-
return join(sidecarDir(root), 'report.md');
|
|
13
|
-
}
|
|
14
|
-
function archiveRoot(root) {
|
|
15
|
-
return join(sidecarDir(root), 'archive');
|
|
16
|
-
}
|
|
17
|
-
// A filesystem-safe slug from arbitrary title text: lowercased, runs of
|
|
18
|
-
// non-alphanumerics collapsed to a single hyphen, ends trimmed. Empty → `session`.
|
|
19
|
-
function slugify(title) {
|
|
20
|
-
const slug = title
|
|
21
|
-
.toLowerCase()
|
|
22
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
23
|
-
.replace(/^-+|-+$/g, '');
|
|
24
|
-
return slug.length === 0 ? 'session' : slug;
|
|
25
|
-
}
|
|
26
|
-
// The session title is intent.md's first `# ` heading (start stamps `# <title>`).
|
|
27
|
-
function sessionTitle(root) {
|
|
28
|
-
let content = '';
|
|
29
|
-
try {
|
|
30
|
-
content = readFileSync(intentPath(root), 'utf8');
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return 'session';
|
|
34
|
-
}
|
|
35
|
-
for (const line of content.split('\n')) {
|
|
36
|
-
const m = /^#\s+(.+)$/.exec(line);
|
|
37
|
-
if (m) {
|
|
38
|
-
return (m[1] ?? '').trim();
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return 'session';
|
|
42
|
-
}
|
|
43
|
-
// Today as YYYY-MM-DD (UTC); the archive directory is <date>-<slug>.
|
|
44
|
-
function today() {
|
|
45
|
-
return new Date().toISOString().slice(0, 10);
|
|
46
|
-
}
|
|
47
|
-
// Choose <date>-<slug>, disambiguating with -2, -3, … if a same-day, same-slug
|
|
48
|
-
// session was already archived — so a second session lands ALONGSIDE the first,
|
|
49
|
-
// never on top of it.
|
|
50
|
-
function uniqueArchiveDir(root, base) {
|
|
51
|
-
let candidate = join(archiveRoot(root), base);
|
|
52
|
-
let n = 2;
|
|
53
|
-
while (existsSync(candidate)) {
|
|
54
|
-
candidate = join(archiveRoot(root), `${base}-${n}`);
|
|
55
|
-
n += 1;
|
|
56
|
-
}
|
|
57
|
-
return candidate;
|
|
58
|
-
}
|
|
59
|
-
// Copy intent + build-log + report into archive/<date>-<slug>/ and return the
|
|
60
|
-
// directory created. Intent and build-log always exist in an active session; the
|
|
61
|
-
// report is copied only when present — v2 `wrap` does not gate on it (D9), so a
|
|
62
|
-
// close-out without a report still archives the rest.
|
|
63
|
-
export function archiveSession(root) {
|
|
64
|
-
const dir = uniqueArchiveDir(root, `${today()}-${slugify(sessionTitle(root))}`);
|
|
65
|
-
mkdirSync(dir, { recursive: true });
|
|
66
|
-
copyFileSync(intentPath(root), join(dir, 'intent.md'));
|
|
67
|
-
copyFileSync(buildLogPath(root), join(dir, 'build-log.md'));
|
|
68
|
-
if (existsSync(reportPath(root))) {
|
|
69
|
-
copyFileSync(reportPath(root), join(dir, 'report.md'));
|
|
70
|
-
}
|
|
71
|
-
return dir;
|
|
72
|
-
}
|
package/dist/verbs/wrap.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// `plumbbob wrap` (D9) — the v2 close-out, replacing the v1 four-verb
|
|
2
|
-
// finish ceremony. It archives intent + build-log + report
|
|
3
|
-
// (the `/plumbbob:pb-wrap` skill writes the report by default) under .plumbbob/archive/,
|
|
4
|
-
// clears the active files, and deletes the control state (STATE last). Unlike v1
|
|
5
|
-
// `finish` there is NO refuse-without-report gate — guidance offers the artifact, it
|
|
6
|
-
// does not wall the exit. Archive-then-clear, never destroy (C4); git untouched (C5).
|
|
7
|
-
import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
8
|
-
import { join, relative } from 'node:path';
|
|
9
|
-
import { findRepoRoot } from "../lib/git.js";
|
|
10
|
-
import { buildLogPath, checkpointsPath, hasSession, intentPath, seamPath, sidecarDir, spikePath, stepPath } from "../lib/sidecar.js";
|
|
11
|
-
import { archiveSession, reportPath } from "../lib/archive.js";
|
|
12
|
-
export function wrap(cwd) {
|
|
13
|
-
const root = findRepoRoot(cwd);
|
|
14
|
-
if (root === null || !hasSession(root)) {
|
|
15
|
-
process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n');
|
|
16
|
-
return 1;
|
|
17
|
-
}
|
|
18
|
-
if (existsSync(reportPath(root))) {
|
|
19
|
-
appendCheckpointShas(root);
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
process.stderr.write('plumbbob: note — no report.md found; archiving intent + build-log without one ' +
|
|
23
|
-
'(/plumbbob:pb-wrap normally writes the report first). No gate (D9).\n');
|
|
24
|
-
}
|
|
25
|
-
const archived = archiveSession(root);
|
|
26
|
-
// Clear the active files — now safely archived — then the control state, STATE
|
|
27
|
-
// last so "no session" flips exactly at the end.
|
|
28
|
-
rmSync(intentPath(root), { force: true });
|
|
29
|
-
rmSync(buildLogPath(root), { force: true });
|
|
30
|
-
rmSync(reportPath(root), { force: true });
|
|
31
|
-
rmSync(seamPath(root), { force: true });
|
|
32
|
-
rmSync(stepPath(root), { force: true });
|
|
33
|
-
rmSync(spikePath(root), { force: true });
|
|
34
|
-
rmSync(join(sidecarDir(root), 'STATE'), { force: true });
|
|
35
|
-
process.stdout.write(`plumbbob: wrap — archived to ${relative(root, archived)}. Sidecar cleared. ` +
|
|
36
|
-
'Run `/plumbbob:pb-plan` (or `plumbbob start "<title>"`) to frame the next goal.\n');
|
|
37
|
-
return 0;
|
|
38
|
-
}
|
|
39
|
-
// Append the recorded checkpoints (baseline + each `step n <sha>`) to the report so
|
|
40
|
-
// the archived report lists the SHAs.
|
|
41
|
-
function appendCheckpointShas(root) {
|
|
42
|
-
let raw = '';
|
|
43
|
-
try {
|
|
44
|
-
raw = readFileSync(checkpointsPath(root), 'utf8');
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
raw = '';
|
|
48
|
-
}
|
|
49
|
-
const lines = raw
|
|
50
|
-
.split('\n')
|
|
51
|
-
.map((l) => l.trim())
|
|
52
|
-
.filter((l) => l.length > 0)
|
|
53
|
-
.map((l) => `- ${l}`);
|
|
54
|
-
appendFileSync(reportPath(root), ['', '## Checkpoints', '', ...lines, ''].join('\n'));
|
|
55
|
-
}
|