plumbbob 0.9.5 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +87 -86
- package/dist/cli-core.js +8 -8
- package/dist/cli.js +1 -1
- package/dist/lib/agents.js +26 -26
- package/dist/lib/buildlog.js +11 -11
- package/dist/lib/buildlogsync.js +8 -8
- package/dist/lib/check.js +14 -14
- package/dist/lib/commitbody.js +72 -0
- package/dist/lib/commitmsg.js +7 -7
- package/dist/lib/git.js +44 -15
- package/dist/lib/intent.js +20 -19
- package/dist/lib/latch.js +16 -16
- package/dist/lib/orient.js +36 -17
- package/dist/lib/reentry.js +3 -3
- package/dist/lib/settings.js +16 -16
- package/dist/lib/sidecar.js +54 -54
- package/dist/lib/templates.js +1 -1
- package/dist/verbs/agent.js +25 -25
- package/dist/verbs/build.js +9 -9
- package/dist/verbs/check.js +6 -6
- package/dist/verbs/checkpoint.js +48 -47
- package/dist/verbs/doctor.js +27 -26
- package/dist/verbs/finish.js +27 -40
- package/dist/verbs/handoff.js +9 -8
- package/dist/verbs/init.js +9 -9
- package/dist/verbs/park.js +3 -3
- package/dist/verbs/recover.js +11 -11
- package/dist/verbs/revert.js +12 -12
- package/dist/verbs/spike.js +17 -17
- package/dist/verbs/start.js +49 -19
- package/dist/verbs/status.js +8 -8
- package/dist/verbs/turn.js +16 -16
- package/dist/verbs/use.js +4 -4
- package/node_modules/checkride/.claude-plugin/plugin.json +1 -1
- package/node_modules/checkride/CHANGELOG.md +401 -0
- package/node_modules/checkride/README.md +27 -9
- package/node_modules/checkride/dist/adapters.d.ts +12 -4
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +21 -1
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts +9 -1
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/cursor.js +59 -1
- package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.d.ts +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.js +10 -1
- package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts +23 -0
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.js +387 -82
- package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.js +2 -2
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts +14 -5
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.js +58 -5
- package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
- package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
- package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/history.js +232 -0
- package/node_modules/checkride/dist/baseline/history.js.map +1 -0
- package/node_modules/checkride/dist/baseline/index.d.ts +4 -2
- package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/index.js +2 -1
- package/node_modules/checkride/dist/baseline/index.js.map +1 -1
- package/node_modules/checkride/dist/baseline/store.d.ts +30 -7
- package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/store.js +50 -16
- package/node_modules/checkride/dist/baseline/store.js.map +1 -1
- package/node_modules/checkride/dist/cli.d.ts +5 -1
- package/node_modules/checkride/dist/cli.d.ts.map +1 -1
- package/node_modules/checkride/dist/cli.js +98 -25
- package/node_modules/checkride/dist/cli.js.map +1 -1
- package/node_modules/checkride/dist/config.d.ts +33 -4
- package/node_modules/checkride/dist/config.d.ts.map +1 -1
- package/node_modules/checkride/dist/config.js +21 -6
- package/node_modules/checkride/dist/config.js.map +1 -1
- package/node_modules/checkride/dist/digest/digest.js +1 -1
- package/node_modules/checkride/dist/doctor.d.ts +2 -0
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
- package/node_modules/checkride/dist/doctor.js +39 -1
- package/node_modules/checkride/dist/doctor.js.map +1 -1
- package/node_modules/checkride/dist/gate.d.ts +49 -4
- package/node_modules/checkride/dist/gate.d.ts.map +1 -1
- package/node_modules/checkride/dist/gate.js +143 -16
- package/node_modules/checkride/dist/gate.js.map +1 -1
- package/node_modules/checkride/dist/init.d.ts +8 -1
- package/node_modules/checkride/dist/init.d.ts.map +1 -1
- package/node_modules/checkride/dist/init.js +68 -7
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/node-pin.d.ts +1 -1
- package/node_modules/checkride/dist/node-pin.js +1 -1
- package/node_modules/checkride/dist/orchestrator.d.ts +13 -0
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +118 -13
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pm/index.d.ts +1 -0
- package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/index.js.map +1 -1
- package/node_modules/checkride/dist/pm/launch.d.ts +23 -2
- package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/launch.js +16 -0
- package/node_modules/checkride/dist/pm/launch.js.map +1 -1
- package/node_modules/checkride/dist/pm/translate.js +1 -1
- package/node_modules/checkride/dist/pm/translate.js.map +1 -1
- package/node_modules/checkride/dist/qa/mutation.d.ts +4 -4
- package/node_modules/checkride/dist/qa/mutation.js +4 -4
- package/node_modules/checkride/dist/recover.d.ts +48 -0
- package/node_modules/checkride/dist/recover.d.ts.map +1 -0
- package/node_modules/checkride/dist/recover.js +220 -0
- package/node_modules/checkride/dist/recover.js.map +1 -0
- package/node_modules/checkride/dist/smoke.d.ts +1 -1
- package/node_modules/checkride/dist/smoke.js +1 -1
- package/node_modules/checkride/dist/snippets.d.ts +1 -1
- package/node_modules/checkride/package.json +3 -2
- package/node_modules/checkride/schema/checkride.config.schema.json +9 -1
- package/node_modules/checkride/skills/check/SKILL.md +9 -1
- package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
- package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
- package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
- package/node_modules/checkride/templates/shared/vale.ini +34 -0
- package/package.json +10 -9
- package/skills/build/SKILL.md +69 -64
- package/skills/doctor/SKILL.md +11 -11
- package/skills/finish/SKILL.md +20 -19
- package/skills/harvest/SKILL.md +13 -12
- package/skills/park/SKILL.md +10 -10
- package/skills/plan/SKILL.md +63 -54
- package/skills/recover/SKILL.md +6 -6
- package/skills/refine/SKILL.md +18 -16
- package/skills/revert/SKILL.md +6 -6
- package/skills/spike/SKILL.md +9 -9
- package/skills/status/SKILL.md +5 -5
- package/skills/step/SKILL.md +17 -12
- package/skills/verify/SKILL.md +45 -40
- package/templates/build-log.md +7 -7
- package/templates/intent.md +49 -39
- package/templates/spike-report.md +10 -10
package/dist/lib/buildlog.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// Mutations to the active build-log.md
|
|
1
|
+
// Mutations to the active build-log.md: the human-facing ledger inside the
|
|
2
2
|
// build's tracked `builds/<slug>/` folder. `park` appends to its Park list;
|
|
3
3
|
// `checkpoint` appends to its Log so the build's history accrues line-by-line
|
|
4
4
|
// *as it happens* rather than being reconstructed at finish. The `## Steps`
|
|
5
5
|
// mirror and `**Current step:**` line are CLI-owned: `build`, `checkpoint`,
|
|
6
6
|
// and `revert` re-render them from intent.md so the ledger's top half tracks
|
|
7
|
-
// reality without a model turn. Pure string transforms
|
|
7
|
+
// reality without a model turn. Pure string transforms: no fs, no classes;
|
|
8
8
|
// callers own the IO so these stay trivially testable.
|
|
9
9
|
/**
|
|
10
|
-
* Append `line` after the last non-blank line of the `## <heading>` section
|
|
11
|
-
*
|
|
10
|
+
* Append `line` after the last non-blank line of the `## <heading>` section:
|
|
11
|
+
* that is, just before the next `## ` heading or EOF.
|
|
12
12
|
*
|
|
13
13
|
* Returns null when the section is absent so the caller can report rather than
|
|
14
14
|
* silently corrupt the doc.
|
|
@@ -33,9 +33,9 @@ export function appendToSection(content, heading, line) {
|
|
|
33
33
|
return lines.join('\n');
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* Replace the `**Current step:**` line with `**Current step:** <label
|
|
37
|
-
* `build` sets it to
|
|
38
|
-
* reset it to `none (at the boundary)`.
|
|
36
|
+
* Replace the `**Current step:**` line with `**Current step:** <label>`:
|
|
37
|
+
* `build` sets it to the step's number and title joined by an em dash when a
|
|
38
|
+
* step opens; `checkpoint`/`revert` reset it to `none (at the boundary)`.
|
|
39
39
|
*
|
|
40
40
|
* Returns null when the line is absent so the caller skips rather than
|
|
41
41
|
* corrupting the doc.
|
|
@@ -50,14 +50,14 @@ export function setCurrentStep(content, label) {
|
|
|
50
50
|
return lines.join('\n');
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Regenerate the `## Steps` mirror from intent.md's parsed steps
|
|
53
|
+
* Regenerate the `## Steps` mirror from intent.md's parsed steps: one
|
|
54
54
|
* `- ☑/☐ N. <title>` line each, in order.
|
|
55
55
|
*
|
|
56
56
|
* Only the mirror's own `- ☑/☐` list lines are owned; the italic instructions
|
|
57
57
|
* paragraph (and any other prose) is preserved, and exactly one blank line
|
|
58
58
|
* separates it from the list. Returns null when the section is absent so the
|
|
59
59
|
* caller skips rather than corrupting the doc. With no steps the mirror
|
|
60
|
-
* renders empty
|
|
60
|
+
* renders empty (the plan is not authored yet) rather than inventing a
|
|
61
61
|
* placeholder.
|
|
62
62
|
*/
|
|
63
63
|
export function syncStepsSection(content, steps) {
|
|
@@ -92,9 +92,9 @@ function isMirrorLine(line) {
|
|
|
92
92
|
* The Log line `checkpoint` writes for a landed step: dated, names the step
|
|
93
93
|
* (its title when intent.md still carries it), and carries the short SHA.
|
|
94
94
|
*
|
|
95
|
-
* One line of the build's history
|
|
95
|
+
* One line of the build's history: `/plumbbob:finish` reads these instead of
|
|
96
96
|
* re-narrating the build. `stats` is the optional compact receipt of what the
|
|
97
|
-
* step cost (
|
|
97
|
+
* step cost (for example `2 red, 34m`): null when nothing accrued, so a clean
|
|
98
98
|
* first-try step reads as a bare line.
|
|
99
99
|
*/
|
|
100
100
|
export function checkpointLogLine(date, step, sha, title, stats = null) {
|
package/dist/lib/buildlogsync.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
// Keep the build-log's top half
|
|
2
|
-
// mirror
|
|
1
|
+
// Keep the build-log's top half (the `**Current step:**` line and the `## Steps`
|
|
2
|
+
// mirror) in sync with intent.md, the build's plan document. The build-log is the
|
|
3
3
|
// human-facing ledger in the build's tracked `builds/<slug>/` folder; its top half
|
|
4
4
|
// is CLI-owned so it never lies: `build` sets the current-step label when a step
|
|
5
5
|
// opens, `checkpoint` and `revert` reset it to the boundary, and all three
|
|
6
6
|
// re-render the mirror from intent.md's parsed steps so it tracks reality without
|
|
7
7
|
// a model turn. The write is best-effort (a missing or hand-edited build-log never
|
|
8
|
-
// fails a verb
|
|
8
|
+
// fails a verb: the checkpoints ledger and intent.md stay the source of truth),
|
|
9
9
|
// matching how `checkpoint`'s Log append behaves.
|
|
10
10
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { buildLogPath, intentPath } from "./sidecar.js";
|
|
12
12
|
import { parseSteps } from "./orient.js";
|
|
13
13
|
import { setCurrentStep, syncStepsSection } from "./buildlog.js";
|
|
14
|
-
/** The Current-step label for no step in flight
|
|
14
|
+
/** The Current-step label for no step in flight: the loop is at the step boundary. */
|
|
15
15
|
export const AT_BOUNDARY = 'none (at the boundary)';
|
|
16
16
|
/**
|
|
17
|
-
* The current-step label for an open step:
|
|
18
|
-
* when the step carries no title. `build` passes
|
|
19
|
-
* AT_BOUNDARY.
|
|
17
|
+
* The current-step label for an open step: the number and title joined by an
|
|
18
|
+
* em dash, or a bare `<n>` when the step carries no title. `build` passes
|
|
19
|
+
* this; the boundary verbs pass AT_BOUNDARY.
|
|
20
20
|
*/
|
|
21
21
|
export function stepLabel(n, title) {
|
|
22
22
|
return title !== null && title.trim().length > 0 ? `${n} — ${title.trim()}` : String(n);
|
|
@@ -26,7 +26,7 @@ export function stepLabel(n, title) {
|
|
|
26
26
|
* `label` and re-render the `## Steps` mirror from intent.md.
|
|
27
27
|
*
|
|
28
28
|
* Each transform is skipped when its target is absent (null), and the whole
|
|
29
|
-
* thing swallows any IO error
|
|
29
|
+
* thing swallows any IO error: the ledger must never block the verb.
|
|
30
30
|
*/
|
|
31
31
|
export function syncBuildLogState(root, slug, label) {
|
|
32
32
|
try {
|
package/dist/lib/check.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// pointers, and an all-slots-skipped run REFUSES rather than green-lighting
|
|
12
12
|
// a repo checkride can't see (a vacuous pass is not a pass).
|
|
13
13
|
// Checkride's stream discipline holds: human progress goes to stderr. Exit 0 is
|
|
14
|
-
// green, 1 is red, 2 means the harness itself broke
|
|
14
|
+
// green, 1 is red, 2 means the harness itself broke: reported distinctly,
|
|
15
15
|
// because a misconfigured gate must not read as broken code (both still block).
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
17
|
import { runChecks, runDoctor } from 'checkride';
|
|
@@ -21,7 +21,7 @@ import { resolveString } from "./settings.js";
|
|
|
21
21
|
* Probe whether the check gate would have anything to run in `root`.
|
|
22
22
|
*
|
|
23
23
|
* A configured `check` command answers yes on its own; otherwise checkride's
|
|
24
|
-
* doctor detection runs in-process
|
|
24
|
+
* doctor detection runs in-process: the same pass `plumbbob doctor` reports
|
|
25
25
|
* as a table. `start` reads this to surface "nothing to check" at plan time,
|
|
26
26
|
* while the human is still deciding, instead of at the first refused
|
|
27
27
|
* checkpoint.
|
|
@@ -37,19 +37,19 @@ export async function detectGate(root) {
|
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
39
39
|
// A broken checkride harness is a different problem with its own reporters
|
|
40
|
-
// (doctor's ✗ row, runCheck's exit 2)
|
|
40
|
+
// (doctor's ✗ row, runCheck's exit 2); the plan-time probe stays quiet
|
|
41
41
|
// rather than mislabeling it "nothing to check".
|
|
42
42
|
return { configured: null, detected: true };
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
46
|
* Checkride's always-on repo checks: the adapters that resolve with no tool
|
|
47
|
-
* config of their own
|
|
47
|
+
* config of their own: the built-in `links` slot, the pnpm-audit security
|
|
48
48
|
* scan, the publint/attw package-shape probes, and the pack/smoke/snippets
|
|
49
49
|
* publish-bundle slots. Several are named for their shape rather than because
|
|
50
50
|
* a bare directory reports them: `publint`/`attw` resolve off a devDependency,
|
|
51
|
-
* `build` off a `scripts.build` entry
|
|
52
|
-
* not that checkride can see the code that goes into it
|
|
51
|
+
* `build` off a `scripts.build` entry (which says the package can be built,
|
|
52
|
+
* not that checkride can see the code that goes into it), and `snippets`
|
|
53
53
|
* carries two adapters since checkride 0.10.2, so the dist-facing
|
|
54
54
|
* `snippets-dist` is named beside it (this list matches ADAPTER names, not
|
|
55
55
|
* slot names). Every one of them but `links` is an opt-in slot, so a default
|
|
@@ -72,7 +72,7 @@ const ALWAYS_ON_ADAPTERS = [
|
|
|
72
72
|
];
|
|
73
73
|
/**
|
|
74
74
|
* The detection rule, shared by `start`'s probe and doctor's callout: some
|
|
75
|
-
* tool slot beyond the always-on family has an adapter
|
|
75
|
+
* tool slot beyond the always-on family has an adapter: that is, checkride can
|
|
76
76
|
* see the CODE (a tsconfig, a test runner, a linter…), not just the repo.
|
|
77
77
|
*/
|
|
78
78
|
export function gateDetectsTools(checks) {
|
|
@@ -90,7 +90,7 @@ export function gateDetectsTools(checks) {
|
|
|
90
90
|
*/
|
|
91
91
|
export async function runCheck(root, flags = {}, commandFlag) {
|
|
92
92
|
if (gateIsRunningFor(root)) {
|
|
93
|
-
// This repo re-entering its OWN gate is never a slow gate
|
|
93
|
+
// This repo re-entering its OWN gate is never a slow gate: it is a
|
|
94
94
|
// recursion whose next generation forks wider than this one. Exit 2 (the
|
|
95
95
|
// harness broke), not 1: nothing has been learned about the code. A gate on
|
|
96
96
|
// a DIFFERENT root is ordinary nested work (the test suite gating its
|
|
@@ -105,7 +105,7 @@ export async function runCheck(root, flags = {}, commandFlag) {
|
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
107
|
* The spawn override: stream the configured command's own output to the
|
|
108
|
-
* terminal and return its exit code
|
|
108
|
+
* terminal and return its exit code: the command is trusted verbatim, no
|
|
109
109
|
* interpretation.
|
|
110
110
|
*/
|
|
111
111
|
function runCommand(root, command, flags) {
|
|
@@ -133,7 +133,7 @@ async function runCheckride(root, flags) {
|
|
|
133
133
|
include: flags.include !== undefined ? [...flags.include] : null,
|
|
134
134
|
// Checkride's own vacuous-green refusal (0.10.2): zero checks executed is
|
|
135
135
|
// an error, not a pass. It overlaps the refusal below and never reaches
|
|
136
|
-
// the human
|
|
136
|
+
// the human: our predicate is the broader one (it also catches a
|
|
137
137
|
// links-only run, where a check DID execute) and fires first, so
|
|
138
138
|
// plumbbob's message and its exit 1 are what a reader sees. Kept anyway:
|
|
139
139
|
// it is the contract checkride asks a gate to run under, so a future
|
|
@@ -145,7 +145,7 @@ async function runCheckride(root, flags) {
|
|
|
145
145
|
}
|
|
146
146
|
catch (err) {
|
|
147
147
|
// The harness broke, not the code: a malformed checkride.config.json or
|
|
148
|
-
// the like. Checkride's own CLI maps this to exit 2; so do we
|
|
148
|
+
// the like. Checkride's own CLI maps this to exit 2; so do we: a
|
|
149
149
|
// misconfigured gate must report distinctly from red.
|
|
150
150
|
const message = err instanceof Error ? err.message : String(err);
|
|
151
151
|
process.stderr.write(`plumbbob: the check gate itself failed — ${message}\n` +
|
|
@@ -156,7 +156,7 @@ async function runCheckride(root, flags) {
|
|
|
156
156
|
// Zero-config checkride skips slots with no detected tool; a repo it can't
|
|
157
157
|
// see must refuse, not vacuously green-light the checkpoint. The built-in
|
|
158
158
|
// `links` slot is always-on (empty detect list), so it alone proves
|
|
159
|
-
// nothing about the code
|
|
159
|
+
// nothing about the code: a links-only run is still vacuous.
|
|
160
160
|
process.stderr.write('plumbbob: checkride found nothing to check in this repo — refusing to call that green.\n' +
|
|
161
161
|
' Add tool configs (tsconfig, vitest, …) or a checkride.config.json custom check,\n' +
|
|
162
162
|
' or set the "check" key in .plumbbob/settings.json to gate through another command.\n');
|
|
@@ -171,7 +171,7 @@ async function runCheckride(root, flags) {
|
|
|
171
171
|
* Name the failing slots and where their raw diagnostics landed, so the agent
|
|
172
172
|
* (verify) reads the tool's own JSON instead of scraping scrollback.
|
|
173
173
|
*
|
|
174
|
-
* The summary is the canonical pointer
|
|
174
|
+
* The summary is the canonical pointer: `output_file` is the adapter's
|
|
175
175
|
* preferred name, but non-JSON output falls back to `.check/<slot>.stdout.txt`.
|
|
176
176
|
*/
|
|
177
177
|
function reportFailingSlots(summary) {
|
|
@@ -184,7 +184,7 @@ function reportFailingSlots(summary) {
|
|
|
184
184
|
process.stderr.write(`plumbbob: failing slots:\n${lines.join('\n')}\n Full report: .check/summary.json\n`);
|
|
185
185
|
}
|
|
186
186
|
/**
|
|
187
|
-
* True when any narrowing flag was passed
|
|
187
|
+
* True when any narrowing flag was passed: the trigger for the spawn path's
|
|
188
188
|
* flags-are-ignored warning.
|
|
189
189
|
*/
|
|
190
190
|
function hasFlags(flags) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The `--body` stdin reader shared by `checkpoint` and `finish`: both verbs
|
|
2
|
+
// take a commit body on fd 0 via a single-quoted heredoc (`--body <<'BODY'`),
|
|
3
|
+
// never as an argument value. `evaluateCommitBody` is the pure decision over
|
|
4
|
+
// an already-known fd-0 shape, so every shape is a direct unit test with no
|
|
5
|
+
// fd tricks; `readCommitBody` gathers the real one: the mirror of latch.ts's
|
|
6
|
+
// evaluateLatch/checkLatch split.
|
|
7
|
+
//
|
|
8
|
+
// Four fd-0 shapes reach this in practice: a heredoc is a regular file, a
|
|
9
|
+
// pipe is a FIFO, an interactive terminal is a TTY, and an agent harness
|
|
10
|
+
// hands the CLI a socket. The first three already degrade safely: a TTY
|
|
11
|
+
// skips the read (it would never send EOF), a regular file/FIFO/`/dev/null`
|
|
12
|
+
// read to EOF and fall back to the caller's own default body when empty. A
|
|
13
|
+
// socket also never sends EOF on its own, so the same blind `readFileSync(0)`
|
|
14
|
+
// blocks forever and, worse, drops the requested body silently. Refuse
|
|
15
|
+
// instead: a silently dropped body reads as success.
|
|
16
|
+
import { fstatSync, readFileSync } from 'node:fs';
|
|
17
|
+
/**
|
|
18
|
+
* The refusal shown when `--body` is asked to read a socket: names the form
|
|
19
|
+
* that works so the retry succeeds instead of guessing again.
|
|
20
|
+
*/
|
|
21
|
+
export const SOCKET_REFUSAL = "plumbbob: --body refuses — stdin is a socket, and a socket never sends EOF the way a heredoc does, so the read would block forever and the body would be lost. Redirect it in with the `--body <<'BODY'` … `BODY` heredoc form instead.\n";
|
|
22
|
+
/**
|
|
23
|
+
* Decide the `--body` outcome from an already-known fd-0 shape.
|
|
24
|
+
*
|
|
25
|
+
* Absent flag or a TTY both mean "don't read": the caller's fallback body
|
|
26
|
+
* applies. `other` covers a heredoc (regular file), a pipe (FIFO), and
|
|
27
|
+
* `/dev/null` (character device) alike: each is read to EOF, and an empty or
|
|
28
|
+
* failed read degrades to the same fallback an absent `--body` would use.
|
|
29
|
+
*/
|
|
30
|
+
export function evaluateCommitBody(hasBodyFlag, shape, read) {
|
|
31
|
+
if (!hasBodyFlag || shape === 'tty') {
|
|
32
|
+
return { ok: true, body: null };
|
|
33
|
+
}
|
|
34
|
+
if (shape === 'socket') {
|
|
35
|
+
return { ok: false, message: SOCKET_REFUSAL };
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const raw = read().trimEnd();
|
|
39
|
+
return { ok: true, body: raw.length > 0 ? raw : null };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { ok: true, body: null };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the `--body` commit body from the real fd 0, or refuse when it can
|
|
47
|
+
* never deliver one.
|
|
48
|
+
*
|
|
49
|
+
* `checkpoint` and `finish` both call this where each used to call its own
|
|
50
|
+
* copy of `bodyArg`: one implementation, one refusal string, so the two
|
|
51
|
+
* verbs cannot drift apart on the wording.
|
|
52
|
+
*/
|
|
53
|
+
export function readCommitBody(args) {
|
|
54
|
+
return evaluateCommitBody(args.includes('--body'), stdinShape(), () => readFileSync(0, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The fd-0 shape the guard cares about: a TTY skips the read (never sends
|
|
58
|
+
* EOF), a socket refuses (the same problem, but silent), everything else is
|
|
59
|
+
* read as before. An unreadable fd (no stdin attached at all) is treated the
|
|
60
|
+
* same as any other shape; the read attempt that follows fails safely.
|
|
61
|
+
*/
|
|
62
|
+
function stdinShape() {
|
|
63
|
+
if (process.stdin.isTTY === true) {
|
|
64
|
+
return 'tty';
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return fstatSync(0).isSocket() ? 'socket' : 'other';
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 'other';
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/lib/commitmsg.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Conventional-Commit subject assembly. Plumbbob's commit subjects read as
|
|
2
|
-
// `type(scope): description`
|
|
3
|
-
// scope
|
|
2
|
+
// `type(scope): description` (real feat/fix/chore types and a real parenthesised
|
|
3
|
+
// scope) so `git log` speaks the same grammar as the rest of the branch. The
|
|
4
4
|
// `plumbbob`/`step N` identifiers ride a marker line at the head of the body,
|
|
5
|
-
// which `withMarker` prepends
|
|
5
|
+
// which `withMarker` prepends, so `git log --grep plumbbob` finds every plumbbob
|
|
6
6
|
// commit. Functional and procedural, node builtins only, no gate: these only
|
|
7
7
|
// shape a string the CLI is about to commit.
|
|
8
8
|
/**
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* prefix on a step title.
|
|
11
11
|
*
|
|
12
12
|
* Anchoring on this closed set keeps a prose title like `Note: rewire the
|
|
13
|
-
* cache` from being misread as a `Note` type
|
|
13
|
+
* cache` from being misread as a `Note` type; only these words, immediately
|
|
14
14
|
* followed by an optional `(scope)`, an optional `!`, and a colon, count as an
|
|
15
15
|
* author-supplied prefix.
|
|
16
16
|
*/
|
|
@@ -23,7 +23,7 @@ const PREFIX = new RegExp(`^(${TYPES.join('|')})(?:\\(([^)]+)\\))?(!)?:\\s+(.+)$
|
|
|
23
23
|
*
|
|
24
24
|
* `feat(escape-hatch): add the flag` → { type:'feat', scope:'escape-hatch',
|
|
25
25
|
* breaking:false, description:'add the flag' }. A title with no recognised
|
|
26
|
-
* prefix comes back whole as the description with a null type/scope
|
|
26
|
+
* prefix comes back whole as the description with a null type/scope; the
|
|
27
27
|
* caller then defaults the type and fills the scope from the build.
|
|
28
28
|
*/
|
|
29
29
|
export function parseConventionalTitle(title) {
|
|
@@ -46,11 +46,11 @@ export function conventionalSubject(parts) {
|
|
|
46
46
|
/**
|
|
47
47
|
* A Conventional subject from a step title, filling gaps from the build.
|
|
48
48
|
*
|
|
49
|
-
* An author-written prefix (`fix(parser): …`) is honoured verbatim
|
|
49
|
+
* An author-written prefix (`fix(parser): …`) is honoured verbatim: its type,
|
|
50
50
|
* scope, breaking marker, and exact wording all win. A bare prose title
|
|
51
51
|
* defaults to `defaultType` and the build `scope`, and gets its sentence-case
|
|
52
52
|
* opener de-capitalised (`Add the flag` → `add the flag`) to match
|
|
53
|
-
* Conventional style
|
|
53
|
+
* Conventional style, while an all-caps acronym start (`API`, `OAuth`) is
|
|
54
54
|
* left alone.
|
|
55
55
|
*/
|
|
56
56
|
export function subjectFromTitle(title, defaultType, scope) {
|
package/dist/lib/git.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// Thin git wrapper over `node:child_process
|
|
1
|
+
// Thin git wrapper over `node:child_process`: the CLI stays on node builtins
|
|
2
2
|
// plus a small deliberate dependency allowlist, and subprocess spawning is
|
|
3
3
|
// centralized in files like this one. Plumbbob's git footprint is additive by
|
|
4
4
|
// rule: these helpers read, locate, stage, and commit forward, and reset
|
|
5
|
-
// `--hard` only to plumbbob's own recorded checkpoint SHAs
|
|
5
|
+
// `--hard` only to plumbbob's own recorded checkpoint SHAs: never a
|
|
6
6
|
// history-rewriting operation on anything pushed.
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
8
|
import { isAbsolute, join } from 'node:path';
|
|
9
9
|
/**
|
|
10
10
|
* Run git in `root` and return its trimmed stdout.
|
|
11
11
|
*
|
|
12
|
-
* stderr is discarded and a non-zero exit throws
|
|
12
|
+
* stderr is discarded and a non-zero exit throws: callers that tolerate
|
|
13
13
|
* failure wrap the call in their own try/catch.
|
|
14
14
|
*/
|
|
15
15
|
function runGit(root, args) {
|
|
@@ -35,8 +35,8 @@ export function findRepoRoot(cwd) {
|
|
|
35
35
|
* Plumbbob writes its control-plane excludes to the shared gitdir's
|
|
36
36
|
* `info/exclude` (personal machinery, never the repo's `.gitignore`), and
|
|
37
37
|
* `--git-path` maps common-dir entries like `info/exclude` to the shared file
|
|
38
|
-
* even from a linked worktree
|
|
39
|
-
* which git never reads for excludes. The result is relative to `root` unless
|
|
38
|
+
* even from a linked worktree (whose per-worktree gitdir has no `info/` and
|
|
39
|
+
* which git never reads for excludes). The result is relative to `root` unless
|
|
40
40
|
* already absolute.
|
|
41
41
|
*/
|
|
42
42
|
export function gitPath(root, relative) {
|
|
@@ -68,16 +68,16 @@ export function isDirty(root) {
|
|
|
68
68
|
return runGit(root, ['status', '--porcelain']).length > 0;
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* The number of commits on HEAD not reachable from `sha
|
|
71
|
+
* The number of commits on HEAD not reachable from `sha`: `git rev-list
|
|
72
72
|
* --count --first-parent <sha>..HEAD`.
|
|
73
73
|
*
|
|
74
74
|
* `status` reads this for its receipts line: one neutral note when commits
|
|
75
75
|
* landed since the last checkpoint outside plumbbob's ledger (out-of-band
|
|
76
76
|
* commits are surfaced, never blocked). `--first-parent` keeps the count on
|
|
77
77
|
* the branch's own line: merging upstream reads as the one merge commit, not
|
|
78
|
-
* the dozens it carried
|
|
78
|
+
* the dozens it carried; those didn't land "outside the ledger" in any sense
|
|
79
79
|
* the receipt should nag about. Best-effort and never throws: 0 when the range
|
|
80
|
-
* is empty, `sha` is unknown to the repo, or HEAD is unborn
|
|
80
|
+
* is empty, `sha` is unknown to the repo, or HEAD is unborn: the count is
|
|
81
81
|
* informational (the human commits freely), never a gate.
|
|
82
82
|
*/
|
|
83
83
|
export function commitsSince(root, sha) {
|
|
@@ -92,22 +92,51 @@ export function commitsSince(root, sha) {
|
|
|
92
92
|
// --- mutation helpers (build-loop: done checkpoints, revert resets). Additive
|
|
93
93
|
// only: stage/commit forward, reset --hard to a recorded checkpoint SHA. ---
|
|
94
94
|
/**
|
|
95
|
-
* Stage everything (`git add -A`)
|
|
95
|
+
* Stage everything (`git add -A`): the checkpoint sweep that carries the work
|
|
96
96
|
* plus plumbbob's own bookkeeping into one commit.
|
|
97
97
|
*/
|
|
98
98
|
export function stageAll(root) {
|
|
99
99
|
runGit(root, ['add', '-A']);
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
* Stage a single path (vs `stageAll`'s `-A`).
|
|
102
|
+
* Stage a single path (vs `stageAll`'s `-A`), returning whether it staged.
|
|
103
103
|
*
|
|
104
104
|
* The plan-approval commit stages only the build's artifact folder so the
|
|
105
|
-
* first step's diff can't absorb the plan scaffold
|
|
106
|
-
* own commit. `path` may be absolute or repo-relative
|
|
105
|
+
* first step's diff can't absorb the plan scaffold: plan approval gets its
|
|
106
|
+
* own commit. `path` may be absolute or repo-relative; git resolves it
|
|
107
107
|
* against `root`. The `--` guards a path that could look like a flag.
|
|
108
|
+
*
|
|
109
|
+
* A repo that gitignores the sidecar has decided not to track it, and git
|
|
110
|
+
* hard-refuses an explicit `git add` of an ignored path, so probe
|
|
111
|
+
* `check-ignore` first: an ignored path is skipped and returns false (the
|
|
112
|
+
* caller's commit is then record-only), a tracked one stages and returns true.
|
|
113
|
+
* Never `git add -f`: honoring the repo's exclusion is the whole point.
|
|
108
114
|
*/
|
|
109
115
|
export function stagePath(root, path) {
|
|
116
|
+
if (isIgnored(root, path)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
110
119
|
runGit(root, ['add', '--', path]);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Whether git ignores `path`: a `git check-ignore -q` probe.
|
|
124
|
+
*
|
|
125
|
+
* Exit 0 means ignored; exit 1 means not (execFileSync throws on the non-zero,
|
|
126
|
+
* caught and read as false); any other status (128, a genuine failure)
|
|
127
|
+
* propagates. `path` may be absolute or repo-relative.
|
|
128
|
+
*/
|
|
129
|
+
export function isIgnored(root, path) {
|
|
130
|
+
try {
|
|
131
|
+
runGit(root, ['check-ignore', '-q', '--', path]);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error.status === 1) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
111
140
|
}
|
|
112
141
|
/**
|
|
113
142
|
* The repo-relative paths of non-ignored untracked files.
|
|
@@ -130,7 +159,7 @@ export function commit(root, subject, body) {
|
|
|
130
159
|
return headSha(root);
|
|
131
160
|
}
|
|
132
161
|
/**
|
|
133
|
-
* The `--stat` summary of what is currently staged (vs HEAD)
|
|
162
|
+
* The `--stat` summary of what is currently staged (vs HEAD): the diffstat
|
|
134
163
|
* the deterministic checkpoint body carries when no `--body` prose arrives.
|
|
135
164
|
* Empty when nothing is staged.
|
|
136
165
|
*/
|
|
@@ -138,7 +167,7 @@ export function stagedStat(root) {
|
|
|
138
167
|
return runGit(root, ['diff', '--cached', '--stat']);
|
|
139
168
|
}
|
|
140
169
|
/**
|
|
141
|
-
* The repo-relative paths currently staged (vs HEAD)
|
|
170
|
+
* The repo-relative paths currently staged (vs HEAD): the set `checkpoint`
|
|
142
171
|
* checks against the step's seam (its granted edit paths) to warn about scope
|
|
143
172
|
* drift. Empty when nothing is staged.
|
|
144
173
|
*/
|
|
@@ -147,7 +176,7 @@ export function stagedPaths(root) {
|
|
|
147
176
|
return out.length === 0 ? [] : out.split('\n');
|
|
148
177
|
}
|
|
149
178
|
/**
|
|
150
|
-
* Reset the worktree hard to `sha
|
|
179
|
+
* Reset the worktree hard to `sha`: only ever one of plumbbob's own recorded
|
|
151
180
|
* checkpoint SHAs, and only `revert` imports this (an ast-grep rule pins that
|
|
152
181
|
* single importer).
|
|
153
182
|
*/
|
package/dist/lib/intent.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// The intent.md parser. intent.md is a build's plan document
|
|
1
|
+
// The intent.md parser. intent.md is a build's plan document: title, the
|
|
2
2
|
// numbered increments under `## Steps`, decisions, constraints, open questions.
|
|
3
3
|
// This module reads it in two registers. The seam parse is strict by design: a
|
|
4
|
-
// seam is a step's edit grant
|
|
5
|
-
// its single `seam:` sub-line
|
|
4
|
+
// seam is a step's edit grant (the backtick-wrapped, repo-relative paths on
|
|
5
|
+
// its single `seam:` sub-line) and it gates git behavior, so the parse refuses
|
|
6
6
|
// precisely on globs, absolute paths, a missing step, a missing seam, or more
|
|
7
7
|
// than one seam line, rather than guessing. The best-effort scrapers below
|
|
8
8
|
// (title, done-when, decisions, constraints) feed an agent's context, not a
|
|
9
|
-
// gate, so they never refuse
|
|
9
|
+
// gate, so they never refuse; they return what they can and report the lines
|
|
10
10
|
// they skipped.
|
|
11
11
|
const GLOB_CHARS = /[*?[\]{}]/;
|
|
12
12
|
/**
|
|
13
|
-
* The lines belonging to step `n` under `## Steps
|
|
13
|
+
* The lines belonging to step `n` under `## Steps`: from its `N. ` opener to
|
|
14
14
|
* the next opener or the section end.
|
|
15
15
|
*
|
|
16
16
|
* Shared by the strict seam parse and the best-effort meta scrape; it keeps the
|
|
@@ -44,12 +44,12 @@ function sliceStep(content, step) {
|
|
|
44
44
|
return { ok: true, itemLines: lines.slice(itemStart, itemEnd) };
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* Parse step `n`'s seam
|
|
47
|
+
* Parse step `n`'s seam: the backtick-wrapped paths on its single `seam:` sub-line.
|
|
48
48
|
*
|
|
49
49
|
* Strict: the seam is the step's edit grant and gates git behavior, so this
|
|
50
50
|
* refuses on a missing step, a missing or duplicated seam line, an empty token,
|
|
51
51
|
* a glob, or an absolute path rather than guessing. Tokens are exact
|
|
52
|
-
* repo-relative paths or `dir/` prefix grants
|
|
52
|
+
* repo-relative paths or `dir/` prefix grants: never globs.
|
|
53
53
|
*/
|
|
54
54
|
export function parseStepSeam(content, step) {
|
|
55
55
|
const slice = sliceStep(content, step);
|
|
@@ -132,7 +132,7 @@ export function isArtifactPath(relPath) {
|
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
134
|
* The staged paths that fall outside the step's seam AND outside the artifact
|
|
135
|
-
* plane
|
|
135
|
+
* plane: the scope-drift set `checkpoint` warns about.
|
|
136
136
|
*
|
|
137
137
|
* Guidance, not a gate: the checkpoint still commits them. An empty seam yields
|
|
138
138
|
* no drift, so callers that cannot resolve a seam simply skip the warning
|
|
@@ -171,15 +171,15 @@ export function parseStepMeta(content, step) {
|
|
|
171
171
|
const body = collected.join(' ').replace(/^\d+\.\s*(?:\[[ xX]\]\s*)?/, '').trim();
|
|
172
172
|
const m = DONE_WHEN.exec(body);
|
|
173
173
|
if (m === null) {
|
|
174
|
-
return { title:
|
|
174
|
+
return { title: stripTrailingSeparator(body), doneWhen: '' };
|
|
175
175
|
}
|
|
176
176
|
return {
|
|
177
|
-
title:
|
|
177
|
+
title: stripTrailingSeparator(body.slice(0, m.index)),
|
|
178
178
|
doneWhen: body.slice(m.index + m[0].length).trim(),
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
|
-
* The build title
|
|
182
|
+
* The build title: the first `# ` heading. '' when absent.
|
|
183
183
|
*/
|
|
184
184
|
export function parseBuildTitle(content) {
|
|
185
185
|
for (const line of content.split('\n')) {
|
|
@@ -193,12 +193,12 @@ export function parseBuildTitle(content) {
|
|
|
193
193
|
const SCOPE_HEADER = /^\*\*Scope:\*\*\s*(.*)$/;
|
|
194
194
|
const SCOPE_PLACEHOLDER = /^<.*>$/;
|
|
195
195
|
/**
|
|
196
|
-
* The build's default Conventional-Commit scope
|
|
196
|
+
* The build's default Conventional-Commit scope: the `**Scope:**` header field
|
|
197
197
|
* authored once in intent.md at plan time.
|
|
198
198
|
*
|
|
199
199
|
* The value is read verbatim off the line, with any trailing `<!-- ... -->`
|
|
200
|
-
* guidance note stripped. An unfilled placeholder
|
|
201
|
-
* angle-bracket placeholder like `<feature>`
|
|
200
|
+
* guidance note stripped. An unfilled placeholder (an empty value or an
|
|
201
|
+
* angle-bracket placeholder like `<feature>`) parses as absent, so a freshly
|
|
202
202
|
* scaffolded, not-yet-edited build falls through to the next rung of the scope
|
|
203
203
|
* fallback chain (the build slug) rather than landing commits scoped
|
|
204
204
|
* `(<feature>)`. null when the header is missing entirely, so an intent.md
|
|
@@ -221,7 +221,7 @@ export function parseBuildScope(content) {
|
|
|
221
221
|
*
|
|
222
222
|
* Verbatim keeps a bullet's `*because*` rationale intact for the agent that
|
|
223
223
|
* reads it. `skipped` holds any line under the heading that was neither a
|
|
224
|
-
* bullet, a continuation, nor blank
|
|
224
|
+
* bullet, a continuation, nor blank: a malformed bullet the caller warns
|
|
225
225
|
* about rather than dropping silently.
|
|
226
226
|
*/
|
|
227
227
|
export function scrapeBullets(content, heading) {
|
|
@@ -264,11 +264,12 @@ export function scrapeBullets(content, heading) {
|
|
|
264
264
|
return { items, skipped };
|
|
265
265
|
}
|
|
266
266
|
/**
|
|
267
|
-
* Trim a trailing
|
|
268
|
-
* space, so a title never carries the marker that introduced its
|
|
267
|
+
* Trim a trailing separator (a comma, or the legacy em dash or hyphen) plus
|
|
268
|
+
* surrounding space, so a title never carries the marker that introduced its
|
|
269
|
+
* done-when.
|
|
269
270
|
*/
|
|
270
|
-
function
|
|
271
|
-
return text.replace(/\s*[
|
|
271
|
+
function stripTrailingSeparator(text) {
|
|
272
|
+
return text.replace(/\s*[—,-]+\s*$/, '').trim();
|
|
272
273
|
}
|
|
273
274
|
/**
|
|
274
275
|
* Shorthand for a failed SeamParse.
|