plumbbob 0.8.18 → 0.9.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 +50 -44
- package/dist/cli-core.js +35 -15
- package/dist/lib/agents.js +230 -112
- package/dist/lib/buildlog.js +50 -30
- package/dist/lib/buildlogsync.js +21 -12
- package/dist/lib/check.js +69 -35
- package/dist/lib/commitmsg.js +53 -32
- package/dist/lib/git.js +86 -31
- package/dist/lib/intent.js +91 -31
- package/dist/lib/latch.js +70 -39
- package/dist/lib/orient.js +78 -39
- package/dist/lib/plugins.js +11 -8
- package/dist/lib/settings.js +65 -32
- package/dist/lib/sidecar.js +320 -150
- package/dist/lib/templates.js +17 -6
- package/dist/verbs/agent.js +116 -55
- package/dist/verbs/build.js +27 -12
- package/dist/verbs/check.js +33 -7
- package/dist/verbs/checkpoint.js +153 -78
- package/dist/verbs/doctor.js +192 -95
- package/dist/verbs/finish.js +86 -45
- package/dist/verbs/handoff.js +56 -20
- package/dist/verbs/init.js +37 -12
- package/dist/verbs/park.js +14 -3
- package/dist/verbs/revert.js +64 -24
- package/dist/verbs/spike.js +89 -30
- package/dist/verbs/start.js +72 -38
- package/dist/verbs/status.js +44 -17
- package/dist/verbs/turn.js +88 -45
- package/dist/verbs/use.js +20 -9
- package/package.json +1 -1
- package/skills/{pb-build → build}/SKILL.md +25 -25
- package/skills/{pb-doctor → doctor}/SKILL.md +1 -1
- package/skills/{pb-finish → finish}/SKILL.md +3 -3
- package/skills/{pb-harvest → harvest}/SKILL.md +4 -4
- package/skills/{pb-park → park}/SKILL.md +2 -2
- package/skills/{pb-plan → plan}/SKILL.md +11 -11
- package/skills/{pb-refine → refine}/SKILL.md +7 -7
- package/skills/{pb-revert → revert}/SKILL.md +2 -2
- package/skills/{pb-spike → spike}/SKILL.md +4 -4
- package/skills/{pb-status → status}/SKILL.md +3 -3
- package/skills/{pb-step → step}/SKILL.md +9 -9
- package/skills/{pb-verify → verify}/SKILL.md +8 -8
- package/templates/build-log.md +7 -7
- package/templates/intent.md +8 -8
- package/templates/spike-report.md +1 -1
package/dist/verbs/spike.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
// `plumbbob spike`
|
|
2
|
-
//
|
|
3
|
-
// `spike/<slug>-<opt>` branch per option OUTSIDE the repo root (default
|
|
4
|
-
// and drops the SPIKE marker
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// `plumbbob spike` — the spike lifecycle for a genuine fork the design phase
|
|
2
|
+
// couldn't settle. `spike "<slug>" [opt…]` creates a sibling git worktree and a
|
|
3
|
+
// `spike/<slug>-<opt>` branch per option OUTSIDE the repo root (default options
|
|
4
|
+
// a/b) and drops the SPIKE marker — one of the untracked per-build control files
|
|
5
|
+
// under `.plumbbob/` that record what's in flight. The main tree stays put while
|
|
6
|
+
// you experiment in the worktrees, which are hook-dormant by construction: the
|
|
7
|
+
// untracked control files don't exist in a fresh checkout, so the hooks find no
|
|
8
|
+
// STATE (the session sentinel) there. `spike done` removes every spike worktree
|
|
9
|
+
// and branch and clears the marker.
|
|
10
|
+
//
|
|
11
|
+
// Every spike also leaves a durable report — `spike-NN-<slug>.md` beside
|
|
12
|
+
// intent.md in the tracked `builds/<slug>/` folder — so the verdict rides the
|
|
13
|
+
// branch into the PR instead of evaporating with the throwaway worktrees.
|
|
8
14
|
//
|
|
9
15
|
// Worktree git calls run directly here rather than via lib/git.ts (which holds the
|
|
10
16
|
// shared additive read/commit helpers): worktree management is spike-local, and
|
|
@@ -15,10 +21,19 @@ import { basename, dirname, join, relative } from 'node:path';
|
|
|
15
21
|
import { findRepoRoot } from "../lib/git.js";
|
|
16
22
|
import { buildFolder, clearSpike, hasSession, inSpike, listSpikeReports, markSpike, nextSpikeReportPath, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
17
23
|
import { readTemplate, stampTemplate } from "../lib/templates.js";
|
|
24
|
+
// Worktree/branch names when the caller lists none — a fork defaults to two arms.
|
|
18
25
|
const DEFAULT_OPTIONS = ['a', 'b'];
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The verbatim Verdict placeholder from templates/spike-report.md — its presence
|
|
28
|
+
* means the spike's call was never recorded, which `spike done` nudges on.
|
|
29
|
+
*/
|
|
21
30
|
const VERDICT_PLACEHOLDER = '*(viable | not viable | partial';
|
|
31
|
+
/**
|
|
32
|
+
* Entry point for `plumbbob spike` — dispatches to open, `report`, or `done`.
|
|
33
|
+
*
|
|
34
|
+
* Requires an active session (the STATE sentinel) and resolves which build the
|
|
35
|
+
* spike belongs to before dispatching.
|
|
36
|
+
*/
|
|
22
37
|
export function spike(cwd, args) {
|
|
23
38
|
const root = findRepoRoot(cwd);
|
|
24
39
|
if (root === null || !hasSession(root)) {
|
|
@@ -35,11 +50,14 @@ export function spike(cwd, args) {
|
|
|
35
50
|
}
|
|
36
51
|
return spikeStart(root, buildSlug, positionals);
|
|
37
52
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
/**
|
|
54
|
+
* `spike report "<slug>"` — scaffold a spike report WITHOUT worktrees.
|
|
55
|
+
*
|
|
56
|
+
* Serves the spike-as-step case: a planned step titled `spike: …`, where the
|
|
57
|
+
* increment itself is the experiment. No boundary requirement and no SPIKE
|
|
58
|
+
* marker — a step in flight is exactly when this runs. Provenance is stamped
|
|
59
|
+
* `step <n>` when a step is in flight, else `/plumbbob:spike`.
|
|
60
|
+
*/
|
|
43
61
|
function spikeReport(root, buildSlug, positionals) {
|
|
44
62
|
const slug = sanitize(positionals[0] ?? '');
|
|
45
63
|
if (slug.length === 0) {
|
|
@@ -47,23 +65,32 @@ function spikeReport(root, buildSlug, positionals) {
|
|
|
47
65
|
return 1;
|
|
48
66
|
}
|
|
49
67
|
const inFlight = readInFlightStep(root, buildSlug);
|
|
50
|
-
const via = inFlight !== null ? `step ${inFlight}` : '/
|
|
68
|
+
const via = inFlight !== null ? `step ${inFlight}` : '/plumbbob:spike';
|
|
51
69
|
const path = scaffoldSpikeReport(root, buildSlug, slug, via);
|
|
52
70
|
process.stdout.write(`plumbbob: spike report scaffolded — ${relative(root, path)}. Fill Findings and the Verdict as you go; ` +
|
|
53
71
|
`a recorded Verdict is what closes a spike step.\n`);
|
|
54
72
|
return 0;
|
|
55
73
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Write a fresh spike report from the template at the next free
|
|
76
|
+
* `spike-NN-<slug>.md` in the build folder, and return its path.
|
|
77
|
+
*
|
|
78
|
+
* Shared by the worktree-opening spike and the spike-as-step `spike report` —
|
|
79
|
+
* one artifact, two entry points. The CLI owns the numbering; the human never
|
|
80
|
+
* creates or numbers a report.
|
|
81
|
+
*/
|
|
59
82
|
function scaffoldSpikeReport(root, buildSlug, spikeSlug, via) {
|
|
60
83
|
const path = nextSpikeReportPath(root, buildSlug, spikeSlug);
|
|
61
84
|
const date = new Date().toISOString().slice(0, 10);
|
|
62
85
|
writeFileSync(path, stampTemplate(readTemplate('spike-report.md'), { TITLE: spikeSlug, VIA: via, DATE: date }));
|
|
63
86
|
return path;
|
|
64
87
|
}
|
|
65
|
-
|
|
66
|
-
|
|
88
|
+
/**
|
|
89
|
+
* The build's in-flight step number, or null — reads the STEP marker `build`
|
|
90
|
+
* writes.
|
|
91
|
+
*
|
|
92
|
+
* Used to stamp a spike-as-step report's provenance as `step <n>`.
|
|
93
|
+
*/
|
|
67
94
|
function readInFlightStep(root, buildSlug) {
|
|
68
95
|
try {
|
|
69
96
|
const raw = readFileSync(stepPath(root, buildSlug), 'utf8').trim();
|
|
@@ -73,6 +100,14 @@ function readInFlightStep(root, buildSlug) {
|
|
|
73
100
|
return null;
|
|
74
101
|
}
|
|
75
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Open a spike: one throwaway worktree + `spike/<slug>-<opt>` branch per option.
|
|
105
|
+
*
|
|
106
|
+
* Refuses when a spike is already open or a step is in flight — a spike is a
|
|
107
|
+
* deliberate fork from a settled boundary, so the current step must checkpoint
|
|
108
|
+
* or revert first. Marks the SPIKE control file and scaffolds the report before
|
|
109
|
+
* returning.
|
|
110
|
+
*/
|
|
76
111
|
function spikeStart(root, buildSlug, positionals) {
|
|
77
112
|
if (inSpike(root, buildSlug)) {
|
|
78
113
|
process.stderr.write('plumbbob: already in a spike. Run `plumbbob spike done` to close it first.\n');
|
|
@@ -101,20 +136,26 @@ function spikeStart(root, buildSlug, positionals) {
|
|
|
101
136
|
created.push(path);
|
|
102
137
|
}
|
|
103
138
|
markSpike(root, buildSlug);
|
|
104
|
-
// Scaffold the report NOW, while the worktrees live
|
|
139
|
+
// Scaffold the report NOW, while the worktrees live: findings accrue during the
|
|
105
140
|
// experiment, not from memory after the teardown. Provenance names the worktrees.
|
|
106
|
-
const report = scaffoldSpikeReport(root, buildSlug, slug, `/
|
|
141
|
+
const report = scaffoldSpikeReport(root, buildSlug, slug, `/plumbbob:spike — worktrees (${options.join(', ')})`);
|
|
107
142
|
process.stdout.write(`plumbbob: spiking — the main tree stays put. Experiment in the throwaway worktrees:\n${created
|
|
108
143
|
.map((p) => ` ${p}`)
|
|
109
144
|
.join('\n')}\nRecord findings and the Verdict in ${relative(root, report)} as you go, then run \`plumbbob spike done\`.\n`);
|
|
110
145
|
return 0;
|
|
111
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Close the spike: remove every spike worktree and branch, clear the marker.
|
|
149
|
+
*
|
|
150
|
+
* Nudges — but never refuses — when a report's Verdict is still the template
|
|
151
|
+
* placeholder: guidance, not a gate.
|
|
152
|
+
*/
|
|
112
153
|
function spikeDone(root, buildSlug) {
|
|
113
154
|
if (!inSpike(root, buildSlug)) {
|
|
114
155
|
process.stderr.write('plumbbob: no active spike to close.\n');
|
|
115
156
|
return 1;
|
|
116
157
|
}
|
|
117
|
-
// Check for an unrecorded verdict BEFORE teardown
|
|
158
|
+
// Check for an unrecorded verdict BEFORE teardown. The reports live in the
|
|
118
159
|
// build folder, not the spike worktrees, so they survive the removal — but the
|
|
119
160
|
// learning that fills them does not, so this is the moment to nudge.
|
|
120
161
|
const unfilled = spikeReportsMissingVerdict(root, buildSlug);
|
|
@@ -126,8 +167,8 @@ function spikeDone(root, buildSlug) {
|
|
|
126
167
|
git(root, ['branch', '-D', branch]);
|
|
127
168
|
}
|
|
128
169
|
clearSpike(root, buildSlug);
|
|
129
|
-
// Guidance, not a gate
|
|
130
|
-
//
|
|
170
|
+
// Guidance, not a gate: a missing verdict is a nudge, and the spike still
|
|
171
|
+
// closes. Name the reports so the human knows where to write it.
|
|
131
172
|
if (unfilled.length > 0) {
|
|
132
173
|
process.stderr.write(`plumbbob: heads-up — no verdict recorded in ${unfilled.join(', ')}. ` +
|
|
133
174
|
`The worktrees are gone; capture the call there before it fades.\n`);
|
|
@@ -138,9 +179,13 @@ function spikeDone(root, buildSlug) {
|
|
|
138
179
|
`Record the verdict (which option won, and why) in ${where} before you \`build\`.\n`);
|
|
139
180
|
return 0;
|
|
140
181
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
182
|
+
/**
|
|
183
|
+
* The spike-report filenames in the build folder whose Verdict is still the
|
|
184
|
+
* template placeholder — the ones `spike done` nudges on.
|
|
185
|
+
*
|
|
186
|
+
* Best-effort per file: an unreadable report is skipped rather than blocking
|
|
187
|
+
* the close.
|
|
188
|
+
*/
|
|
144
189
|
function spikeReportsMissingVerdict(root, buildSlug) {
|
|
145
190
|
const dir = buildFolder(root, buildSlug);
|
|
146
191
|
return listSpikeReports(root, buildSlug).filter((name) => {
|
|
@@ -152,8 +197,11 @@ function spikeReportsMissingVerdict(root, buildSlug) {
|
|
|
152
197
|
}
|
|
153
198
|
});
|
|
154
199
|
}
|
|
155
|
-
|
|
156
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Worktree paths whose checked-out branch is under spike/ — parsed from the
|
|
202
|
+
* porcelain output (blank-line-separated `worktree <path>` / `branch <ref>`
|
|
203
|
+
* blocks).
|
|
204
|
+
*/
|
|
157
205
|
function spikeWorktrees(root) {
|
|
158
206
|
const out = git(root, ['worktree', 'list', '--porcelain']);
|
|
159
207
|
const paths = [];
|
|
@@ -168,16 +216,27 @@ function spikeWorktrees(root) {
|
|
|
168
216
|
}
|
|
169
217
|
return paths;
|
|
170
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* List the local `spike/<slug>` branch names.
|
|
221
|
+
*/
|
|
171
222
|
function spikeBranches(root) {
|
|
172
223
|
const out = git(root, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/spike/']);
|
|
173
224
|
return out.length === 0 ? [] : out.split('\n').filter((b) => b.length > 0);
|
|
174
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Slugify a raw spike name — lowercase, non-alphanumerics collapsed to dashes.
|
|
228
|
+
*/
|
|
175
229
|
function sanitize(raw) {
|
|
176
230
|
return raw
|
|
177
231
|
.toLowerCase()
|
|
178
232
|
.replace(/[^a-z0-9]+/g, '-')
|
|
179
233
|
.replace(/^-+|-+$/g, '');
|
|
180
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Run a git command at `root` and return its trimmed stdout.
|
|
237
|
+
*
|
|
238
|
+
* Stderr passes through to the terminal; a non-zero exit throws.
|
|
239
|
+
*/
|
|
181
240
|
function git(root, args) {
|
|
182
241
|
return execFileSync('git', ['-C', root, ...args], {
|
|
183
242
|
encoding: 'utf8',
|
package/dist/verbs/start.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
// `plumbbob start "<title>"` — scaffold the sidecar
|
|
2
|
-
//
|
|
1
|
+
// `plumbbob start "<title>"` — open a session: scaffold the sidecar (the
|
|
2
|
+
// `.plumbbob/` tree of build artifacts and control files), record the baseline
|
|
3
|
+
// commit, and point the session at a new build. Refuses on a dirty tree (a
|
|
4
|
+
// clean baseline is the contract; `--allow-dirty` overrides), an existing
|
|
5
|
+
// session, or a non-git directory.
|
|
3
6
|
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4
7
|
import { join } from 'node:path';
|
|
5
8
|
import { detectGate } from "../lib/check.js";
|
|
@@ -7,10 +10,27 @@ import { readTemplate, stampTemplate } from "../lib/templates.js";
|
|
|
7
10
|
import { findRepoRoot, hasCommit, headSha, isDirty } from "../lib/git.js";
|
|
8
11
|
import { sidecarDir, buildDir, listBuilds, slugify, checkpointsPath, intentPath, buildLogPath, beginSession, hasSession, excludeControl, excludeSidecar, setGrant, stampTick, } from "../lib/sidecar.js";
|
|
9
12
|
import { settingsPath } from "../lib/settings.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Human-readable echo of the default check gate, stamped into the templates'
|
|
15
|
+
* {{CHECK}} line.
|
|
16
|
+
*
|
|
17
|
+
* The gate is checkride unless a `check` setting overrides it, so settings.json
|
|
18
|
+
* seeds with no `check` key at all — absence IS the default; this line says
|
|
19
|
+
* that in prose where the templates need it.
|
|
20
|
+
*/
|
|
13
21
|
const CHECK_ECHO = 'checkride (set a "check" key in .plumbbob/settings.json to override)';
|
|
22
|
+
/**
|
|
23
|
+
* Open a session: validate the repo, scaffold the build folder, record the
|
|
24
|
+
* baseline commit.
|
|
25
|
+
*
|
|
26
|
+
* Refuses without a title, outside a git repo, on a commitless repo, when a
|
|
27
|
+
* session is already active, and on a dirty tree (`--allow-dirty` records the
|
|
28
|
+
* current HEAD anyway, with a data-loss warning). The default layout is a
|
|
29
|
+
* tracked `.plumbbob/builds/<slug>/` folder that rides the branch into the PR;
|
|
30
|
+
* `--local` keeps the whole sidecar untracked for repos that won't carry tool
|
|
31
|
+
* folders. Ends with a plan-time probe of the check gate so an unconfigured
|
|
32
|
+
* repo hears about it while the human is still deciding.
|
|
33
|
+
*/
|
|
14
34
|
export async function start(cwd, args) {
|
|
15
35
|
const positionals = args.filter((a) => !a.startsWith('--'));
|
|
16
36
|
const allowDirty = args.includes('--allow-dirty');
|
|
@@ -51,25 +71,28 @@ export async function start(cwd, args) {
|
|
|
51
71
|
}
|
|
52
72
|
const sha = headSha(root);
|
|
53
73
|
mkdirSync(sidecarDir(root), { recursive: true });
|
|
54
|
-
// beginSession writes STATE —
|
|
55
|
-
//
|
|
56
|
-
//
|
|
74
|
+
// beginSession writes STATE — one file that is both the session sentinel (its
|
|
75
|
+
// existence means a session is live) and the active-build cursor (its single
|
|
76
|
+
// line names the build every verb acts on). Point it at this build — null
|
|
77
|
+
// under --local, which has no cursor. The cursor lives in STATE, not
|
|
78
|
+
// settings.local.json, so that personal overlay stays purely human-owned.
|
|
57
79
|
beginSession(root, local ? null : slug);
|
|
58
80
|
// Scaffold the tracked settings.json ONLY when absent, and seed it EMPTY — the
|
|
59
81
|
// human owns this file once it exists (their `check` gate lives here), so a
|
|
60
|
-
// re-start must never touch it.
|
|
61
|
-
// absence of `check` already means checkride and absence of `auto` already
|
|
62
|
-
// false (see settings.ts), so `{}` is exactly "all defaults". `auto` in
|
|
63
|
-
// particular is a personal preference that belongs in settings.local.json,
|
|
64
|
-
// this tracked file
|
|
82
|
+
// re-start must never touch it. Inject no opinions even on first create:
|
|
83
|
+
// absence of `check` already means checkride and absence of `auto` already
|
|
84
|
+
// means false (see settings.ts), so `{}` is exactly "all defaults". `auto` in
|
|
85
|
+
// particular is a personal preference that belongs in settings.local.json,
|
|
86
|
+
// never this tracked file.
|
|
65
87
|
if (!existsSync(settingsPath(root))) {
|
|
66
88
|
writeFileSync(settingsPath(root), `${JSON.stringify({}, null, 2)}\n`);
|
|
67
89
|
}
|
|
68
|
-
//
|
|
69
|
-
// excluded); the default plants a tracked
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
90
|
+
// `--local` keeps a fully-untracked flat layout (the whole `.plumbbob/`
|
|
91
|
+
// git-excluded); the default plants a tracked `builds/<slug>/` folder — the
|
|
92
|
+
// build's record (intent, build-log, checkpoints) rides its branch into the
|
|
93
|
+
// PR — and git-excludes only the per-worktree control files. The slug is
|
|
94
|
+
// date-prefixed when derived (see datedSlug) and validated unique above —
|
|
95
|
+
// the CLI refuses a collision, never auto-suffixes.
|
|
73
96
|
let intentLocation;
|
|
74
97
|
if (local) {
|
|
75
98
|
writeFileSync(checkpointsPath(root), `baseline ${sha}\n`);
|
|
@@ -87,20 +110,23 @@ export async function start(cwd, args) {
|
|
|
87
110
|
excludeControl(root);
|
|
88
111
|
intentLocation = `.plumbbob/builds/${slug}/intent.md`;
|
|
89
112
|
}
|
|
90
|
-
// The plan's entry stamp
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
113
|
+
// The plan's entry stamp for the checkpoint latch: set TICK to the current
|
|
114
|
+
// TURN, so `checkpoint --plan` only lands once the harness ledger records a
|
|
115
|
+
// human turn after this moment. Skipped when TURN is absent — a hookless host
|
|
116
|
+
// grows no ledger and the latch stays dormant, which also covers a first
|
|
117
|
+
// session where the hook has never ticked; that one plan commit stays
|
|
118
|
+
// guidance-governed. Any GRANT file lying around predates this session (a
|
|
119
|
+
// legitimate one is minted by a `/build` prompt, which never runs `start`)
|
|
120
|
+
// — clear it so a stale grant can't self-approve this session's plan; a
|
|
121
|
+
// grant's lifetime is one turn.
|
|
96
122
|
setGrant(root, null);
|
|
97
123
|
stampTick(root, local ? null : slug);
|
|
98
124
|
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`);
|
|
99
|
-
// The plan-time gate probe
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// code
|
|
125
|
+
// The plan-time gate probe: if checkride sees no code checks here, say so
|
|
126
|
+
// NOW — while the human is still deciding — instead of at the first
|
|
127
|
+
// checkpoint, where the gate either refuses a vacuous run or, worse, greens
|
|
128
|
+
// on the always-on repo checks alone. Guidance only: it never changes the
|
|
129
|
+
// exit code, and a configured `check` or a probe hiccup stays silent.
|
|
104
130
|
const gate = await detectGate(root);
|
|
105
131
|
if (gate.configured === null && !gate.detected) {
|
|
106
132
|
process.stderr.write('plumbbob: heads-up — the check gate sees no code checks in this repo (checkride detected no tools).\n' +
|
|
@@ -109,11 +135,15 @@ export async function start(cwd, args) {
|
|
|
109
135
|
}
|
|
110
136
|
return 0;
|
|
111
137
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Derive a `YYYY-MM-DD-<slug>` build slug from the title (local time).
|
|
140
|
+
*
|
|
141
|
+
* The date prefix makes `builds/` sort chronologically under `listBuilds`'
|
|
142
|
+
* plain lexical sort — ordering by construction, not by titling convention. An
|
|
143
|
+
* explicit `--slug` bypasses this and stays verbatim — the CLI never rewrites
|
|
144
|
+
* what the caller chose. An untitleable title yields `''` so the empty-slug
|
|
145
|
+
* guard fires instead of minting a date-only slug.
|
|
146
|
+
*/
|
|
117
147
|
function datedSlug(title) {
|
|
118
148
|
const base = slugify(title);
|
|
119
149
|
if (base.length === 0)
|
|
@@ -122,14 +152,18 @@ function datedSlug(title) {
|
|
|
122
152
|
const pad = (n) => String(n).padStart(2, '0');
|
|
123
153
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${base}`;
|
|
124
154
|
}
|
|
125
|
-
|
|
126
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Read the value that follows a `--flag` in argv (e.g. `--slug my-build`), or
|
|
157
|
+
* undefined when the flag is absent or trails with no value.
|
|
158
|
+
*/
|
|
127
159
|
function flagValue(args, flag) {
|
|
128
160
|
const i = args.indexOf(flag);
|
|
129
161
|
return i >= 0 ? args[i + 1] : undefined;
|
|
130
162
|
}
|
|
131
|
-
|
|
132
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Stamp a build template's `{{TITLE}}`/`{{CHECK}}` — the check is always the
|
|
165
|
+
* echo constant, since an absent `check` setting IS the checkride default.
|
|
166
|
+
*/
|
|
133
167
|
function stampBuild(template, title) {
|
|
134
168
|
return stampTemplate(template, { TITLE: title, CHECK: CHECK_ECHO });
|
|
135
169
|
}
|
package/dist/verbs/status.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// `plumbbob status` — the orientation dashboard
|
|
1
|
+
// `plumbbob status` — the orientation dashboard, or NO ACTIVE SESSION. It
|
|
2
|
+
// parses the live session — intent.md, the build-log, the checkpoints ledger,
|
|
3
|
+
// the in-flight STEP marker — into a where-am-I view that suggests one primary
|
|
4
|
+
// next move while printing the full picture, so you can always override.
|
|
2
5
|
// Read-only, always exits 0. Skills pre-inject this output to gate their own
|
|
3
6
|
// behavior, so the `NO ACTIVE SESSION` sentinel is kept exact.
|
|
4
7
|
import { readFileSync } from 'node:fs';
|
|
@@ -7,6 +10,9 @@ import { commitsSince, findRepoRoot } from "../lib/git.js";
|
|
|
7
10
|
import { SLOTS, readHarnessFile, resolveAgent, } from "../lib/agents.js";
|
|
8
11
|
import { buildFolder, buildLogPath, checkpointsPath, hasSession, inSpike, intentPath, listBuilds, resolveBuild, stepPath, } from "../lib/sidecar.js";
|
|
9
12
|
import { formatOrientation, lastLedgerSha, orient } from "../lib/orient.js";
|
|
13
|
+
/**
|
|
14
|
+
* Read a file as utf8, or '' when it is missing or unreadable.
|
|
15
|
+
*/
|
|
10
16
|
function readOr(path) {
|
|
11
17
|
try {
|
|
12
18
|
return readFileSync(path, 'utf8');
|
|
@@ -15,6 +21,13 @@ function readOr(path) {
|
|
|
15
21
|
return '';
|
|
16
22
|
}
|
|
17
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Print the orientation dashboard for the active build, plus its harness bindings.
|
|
26
|
+
*
|
|
27
|
+
* No session prints the exact `NO ACTIVE SESSION` sentinel; a session with
|
|
28
|
+
* builds but no active one lists them and points at `use`. Always exits 0 —
|
|
29
|
+
* status is a report, never a gate.
|
|
30
|
+
*/
|
|
18
31
|
export function status(cwd, args = []) {
|
|
19
32
|
const root = findRepoRoot(cwd);
|
|
20
33
|
if (root === null || !hasSession(root)) {
|
|
@@ -33,10 +46,11 @@ export function status(cwd, args = []) {
|
|
|
33
46
|
}
|
|
34
47
|
}
|
|
35
48
|
const inFlightRaw = readOr(stepPath(root, slug)).trim();
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
49
|
+
// Out-of-band commits — ones that landed outside the checkpoints ledger — are
|
|
50
|
+
// surfaced here, never blocked. Counting them needs git (orient is pure), so
|
|
51
|
+
// status measures it: commits on HEAD since the last ledger line's SHA —
|
|
52
|
+
// baseline, plan, or step, so a commit landing before the first step
|
|
53
|
+
// checkpoint still surfaces. No ledger yet ⇒ nothing to reconcile against ⇒ 0.
|
|
40
54
|
const checkpoints = readOr(checkpointsPath(root, slug));
|
|
41
55
|
const anchor = lastLedgerSha(checkpoints);
|
|
42
56
|
const orientation = orient({
|
|
@@ -51,12 +65,18 @@ export function status(cwd, args = []) {
|
|
|
51
65
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
52
66
|
return 0;
|
|
53
67
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
/**
|
|
69
|
+
* The active build's harness bindings: the agents bound to each slot, per
|
|
70
|
+
* `defaults` and per step.
|
|
71
|
+
*
|
|
72
|
+
* Bindings live in `builds/<slug>/harness.json` and status is the surface that
|
|
73
|
+
* reports them — no separate `agent check` verb. Any bound agent that does not
|
|
74
|
+
* resolve gets a warning, not an error: a teammate can lack a personal agent
|
|
75
|
+
* and the loop still works. Returns [] when the build has no harness.json (a
|
|
76
|
+
* clean no-op) or the file binds nothing, so the dashboard stays uncluttered
|
|
77
|
+
* for builds that use no agents. A broken harness.json surfaces its parse
|
|
78
|
+
* error rather than hiding.
|
|
79
|
+
*/
|
|
60
80
|
function harnessSection(root, slug) {
|
|
61
81
|
const parsed = readHarnessFile(join(buildFolder(root, slug), 'harness.json'));
|
|
62
82
|
if (parsed === null)
|
|
@@ -77,9 +97,12 @@ function harnessSection(root, slug) {
|
|
|
77
97
|
return [];
|
|
78
98
|
return ['', 'harness bindings:', ...rows, ...bindingWarnings(root, harness)];
|
|
79
99
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Render a SlotBindings as `slot: name1, name2` fragments, in slot order.
|
|
102
|
+
*
|
|
103
|
+
* A slot bound to no agents (an explicit "override to none") is omitted —
|
|
104
|
+
* there is nothing to run and nothing to name.
|
|
105
|
+
*/
|
|
83
106
|
function slotFragments(bindings) {
|
|
84
107
|
const out = [];
|
|
85
108
|
for (const slot of SLOTS) {
|
|
@@ -89,9 +112,13 @@ function slotFragments(bindings) {
|
|
|
89
112
|
}
|
|
90
113
|
return out;
|
|
91
114
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
/**
|
|
116
|
+
* A warning line for every distinct agent bound anywhere in the harness that
|
|
117
|
+
* does not resolve.
|
|
118
|
+
*
|
|
119
|
+
* Deduped and sorted so the report is stable; a name bound in several slots
|
|
120
|
+
* warns once.
|
|
121
|
+
*/
|
|
95
122
|
function bindingWarnings(root, harness) {
|
|
96
123
|
const names = new Set();
|
|
97
124
|
for (const slot of SLOTS)
|