plumbbob 0.9.5 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +89 -87
- package/dist/cli-core.js +18 -8
- package/dist/cli.js +1 -1
- package/dist/lib/agents.js +26 -26
- package/dist/lib/buildlog.js +21 -11
- package/dist/lib/buildlogsync.js +8 -8
- package/dist/lib/check.js +14 -14
- package/dist/lib/commitbody.js +72 -0
- package/dist/lib/commitmsg.js +7 -7
- package/dist/lib/git.js +44 -15
- package/dist/lib/intent.js +20 -19
- package/dist/lib/latch.js +16 -16
- package/dist/lib/orient.js +36 -17
- package/dist/lib/reentry.js +3 -3
- package/dist/lib/settings.js +16 -16
- package/dist/lib/sidecar.js +70 -54
- package/dist/lib/templates.js +1 -1
- package/dist/verbs/abandon.js +122 -0
- package/dist/verbs/agent.js +25 -25
- package/dist/verbs/build.js +9 -9
- package/dist/verbs/check.js +6 -6
- package/dist/verbs/checkpoint.js +48 -47
- package/dist/verbs/doctor.js +35 -29
- package/dist/verbs/finish.js +27 -40
- package/dist/verbs/handoff.js +9 -8
- package/dist/verbs/init.js +9 -9
- package/dist/verbs/park.js +3 -3
- package/dist/verbs/recover.js +12 -12
- package/dist/verbs/revert.js +12 -12
- package/dist/verbs/spike.js +21 -21
- package/dist/verbs/start.js +49 -19
- package/dist/verbs/status.js +8 -8
- package/dist/verbs/turn.js +16 -16
- package/dist/verbs/use.js +4 -4
- package/node_modules/checkride/.claude-plugin/plugin.json +1 -1
- package/node_modules/checkride/CHANGELOG.md +401 -0
- package/node_modules/checkride/README.md +27 -9
- package/node_modules/checkride/dist/adapters.d.ts +12 -4
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +21 -1
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts +9 -1
- package/node_modules/checkride/dist/agent-setup/cursor.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/cursor.js +59 -1
- package/node_modules/checkride/dist/agent-setup/cursor.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.d.ts +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/harness.js +10 -1
- package/node_modules/checkride/dist/agent-setup/harness.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts +23 -0
- package/node_modules/checkride/dist/agent-setup/scripts.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/scripts.js +387 -82
- package/node_modules/checkride/dist/agent-setup/scripts.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.js +2 -2
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts +14 -5
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.js +58 -5
- package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
- package/node_modules/checkride/dist/baseline/history.d.ts +98 -0
- package/node_modules/checkride/dist/baseline/history.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/history.js +232 -0
- package/node_modules/checkride/dist/baseline/history.js.map +1 -0
- package/node_modules/checkride/dist/baseline/index.d.ts +4 -2
- package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/index.js +2 -1
- package/node_modules/checkride/dist/baseline/index.js.map +1 -1
- package/node_modules/checkride/dist/baseline/store.d.ts +30 -7
- package/node_modules/checkride/dist/baseline/store.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/store.js +50 -16
- package/node_modules/checkride/dist/baseline/store.js.map +1 -1
- package/node_modules/checkride/dist/cli.d.ts +5 -1
- package/node_modules/checkride/dist/cli.d.ts.map +1 -1
- package/node_modules/checkride/dist/cli.js +98 -25
- package/node_modules/checkride/dist/cli.js.map +1 -1
- package/node_modules/checkride/dist/config.d.ts +33 -4
- package/node_modules/checkride/dist/config.d.ts.map +1 -1
- package/node_modules/checkride/dist/config.js +21 -6
- package/node_modules/checkride/dist/config.js.map +1 -1
- package/node_modules/checkride/dist/digest/digest.js +1 -1
- package/node_modules/checkride/dist/doctor.d.ts +2 -0
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
- package/node_modules/checkride/dist/doctor.js +39 -1
- package/node_modules/checkride/dist/doctor.js.map +1 -1
- package/node_modules/checkride/dist/gate.d.ts +49 -4
- package/node_modules/checkride/dist/gate.d.ts.map +1 -1
- package/node_modules/checkride/dist/gate.js +143 -16
- package/node_modules/checkride/dist/gate.js.map +1 -1
- package/node_modules/checkride/dist/init.d.ts +8 -1
- package/node_modules/checkride/dist/init.d.ts.map +1 -1
- package/node_modules/checkride/dist/init.js +68 -7
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/node-pin.d.ts +1 -1
- package/node_modules/checkride/dist/node-pin.js +1 -1
- package/node_modules/checkride/dist/orchestrator.d.ts +13 -0
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +118 -13
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pm/index.d.ts +1 -0
- package/node_modules/checkride/dist/pm/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/index.js.map +1 -1
- package/node_modules/checkride/dist/pm/launch.d.ts +23 -2
- package/node_modules/checkride/dist/pm/launch.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/launch.js +16 -0
- package/node_modules/checkride/dist/pm/launch.js.map +1 -1
- package/node_modules/checkride/dist/pm/translate.js +1 -1
- package/node_modules/checkride/dist/pm/translate.js.map +1 -1
- package/node_modules/checkride/dist/qa/mutation.d.ts +4 -4
- package/node_modules/checkride/dist/qa/mutation.js +4 -4
- package/node_modules/checkride/dist/recover.d.ts +48 -0
- package/node_modules/checkride/dist/recover.d.ts.map +1 -0
- package/node_modules/checkride/dist/recover.js +220 -0
- package/node_modules/checkride/dist/recover.js.map +1 -0
- package/node_modules/checkride/dist/smoke.d.ts +1 -1
- package/node_modules/checkride/dist/smoke.js +1 -1
- package/node_modules/checkride/dist/snippets.d.ts +1 -1
- package/node_modules/checkride/package.json +3 -2
- package/node_modules/checkride/schema/checkride.config.schema.json +9 -1
- package/node_modules/checkride/skills/check/SKILL.md +9 -1
- package/node_modules/checkride/templates/shared/styles/Repo/Drift.yml +41 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Latin.yml +14 -0
- package/node_modules/checkride/templates/shared/styles/Repo/LyHyphen.yml +28 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Minted.yml +33 -0
- package/node_modules/checkride/templates/shared/styles/Repo/ThereIs.yml +16 -0
- package/node_modules/checkride/templates/shared/styles/Repo/Weasel.yml +26 -0
- package/node_modules/checkride/templates/shared/vale.ini +34 -0
- package/package.json +13 -11
- package/skills/abandon/SKILL.md +20 -0
- package/skills/build/SKILL.md +69 -64
- package/skills/doctor/SKILL.md +11 -11
- package/skills/finish/SKILL.md +20 -19
- package/skills/harvest/SKILL.md +13 -12
- package/skills/park/SKILL.md +10 -10
- package/skills/plan/SKILL.md +63 -54
- package/skills/recover/SKILL.md +7 -7
- package/skills/refine/SKILL.md +18 -16
- package/skills/revert/SKILL.md +6 -6
- package/skills/spike/SKILL.md +9 -9
- package/skills/status/SKILL.md +5 -5
- package/skills/step/SKILL.md +17 -12
- package/skills/verify/SKILL.md +45 -40
- package/templates/build-log.md +7 -7
- package/templates/intent.md +49 -39
- package/templates/spike-report.md +10 -10
package/dist/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.
|
package/dist/lib/latch.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
// The approval latch
|
|
1
|
+
// The approval latch: the ledger-plane predicate `checkpoint` evaluates before
|
|
2
2
|
// its check gate (the latch is cheap, so it runs first; the heavy gate already
|
|
3
|
-
// ran in the verify tick). The work plane stays guidance
|
|
4
|
-
// blocked
|
|
3
|
+
// ran in the verify tick). The work plane stays guidance (no edit is ever
|
|
4
|
+
// blocked) but the commit tick is latched: a step may not land unless a human
|
|
5
5
|
// turn intervened since the step was entered, the human granted a one-turn
|
|
6
6
|
// self-approval from their literal prompt, or the turn ledger is dormant (a
|
|
7
7
|
// host with no hooks grows no ledger, and the latch stays out of the way rather
|
|
8
8
|
// than wedging). The ledger is a pair of flat sidecar control files: TURN
|
|
9
9
|
// counts human prompts (ticked by the `plumbbob turn` hook), TICK stamps that
|
|
10
|
-
// count when a step is entered
|
|
10
|
+
// count when a step is entered; TURN > TICK means a human turn intervened.
|
|
11
11
|
// Five rows, evaluated in order, first hit wins; the refusal message *is* the
|
|
12
12
|
// pause affordance. A standing `auto` in a settings file is never a grant: a
|
|
13
|
-
// model can write that file, and a grant it can forge is no grant
|
|
13
|
+
// model can write that file, and a grant it can forge is no grant; the latch
|
|
14
14
|
// reads it only to say so at the pause, never to allow. It reads only
|
|
15
|
-
// TURN/TICK/GRANT and isTTY
|
|
15
|
+
// TURN/TICK/GRANT and isTTY; it never sniffs the host session. Named exports,
|
|
16
16
|
// node builtins.
|
|
17
17
|
import { readFileSync } from 'node:fs';
|
|
18
18
|
import { grantPath, tickPath, turnPath } from "./sidecar.js";
|
|
19
19
|
import { resolveBoolean } from "./settings.js";
|
|
20
20
|
const ALLOW = { allow: true };
|
|
21
21
|
/**
|
|
22
|
-
* The refusal message is the affordance
|
|
22
|
+
* The refusal message is the affordance; it tells the model what the pause is,
|
|
23
23
|
* how it ends (approval on the next turn lands it; `/plumbbob:build` only starts the
|
|
24
24
|
* next step), and names the only legitimate grants.
|
|
25
25
|
*/
|
|
@@ -31,7 +31,7 @@ boundary; \`/plumbbob:build\` only starts the next step, it never lands this one
|
|
|
31
31
|
`;
|
|
32
32
|
/**
|
|
33
33
|
* The plan commit gets its own wording: there is no step, no diff, and no
|
|
34
|
-
* self-review at plan time
|
|
34
|
+
* self-review at plan time; what the human approves is the plan itself.
|
|
35
35
|
*/
|
|
36
36
|
const NO_TURN_PLAN_MESSAGE = `plumbbob: checkpoint refused — no human turn since \`start\` stamped this plan. This
|
|
37
37
|
is the plan pause: present the plan, then end the turn; the human's approving
|
|
@@ -39,7 +39,7 @@ message is the tick that lets it land on the next run. (An explicit \`/plumbbob:
|
|
|
39
39
|
step range in the human's own prompt is the only self-approval.)
|
|
40
40
|
`;
|
|
41
41
|
/**
|
|
42
|
-
* A settings `auto` is never a grant
|
|
42
|
+
* A settings `auto` is never a grant; a model can write that file, and a grant
|
|
43
43
|
* it can forge is no grant. When one is set, the refusal names it rather than
|
|
44
44
|
* silently ignoring it, and points at the only self-approval that works.
|
|
45
45
|
*/
|
|
@@ -60,19 +60,19 @@ function ceilingMessage(ceiling) {
|
|
|
60
60
|
*/
|
|
61
61
|
export function evaluateLatch(input) {
|
|
62
62
|
if (input.isTTY)
|
|
63
|
-
return ALLOW; // 1
|
|
63
|
+
return ALLOW; // 1: a human at the keyboard
|
|
64
64
|
if (input.turn === null || input.tick === null)
|
|
65
|
-
return ALLOW; // 2
|
|
65
|
+
return ALLOW; // 2: dormant ledger / hand-built diff
|
|
66
66
|
if (input.grant?.kind === 'auto')
|
|
67
|
-
return ALLOW; // 3
|
|
67
|
+
return ALLOW; // 3: one-turn grant the human typed
|
|
68
68
|
if (input.grant?.kind === 'range' && input.step !== null) {
|
|
69
69
|
if (input.step <= input.grant.ceiling)
|
|
70
70
|
return ALLOW;
|
|
71
71
|
return { allow: false, reason: 'ceiling', message: ceilingMessage(input.grant.ceiling) };
|
|
72
72
|
}
|
|
73
73
|
if (input.turn > input.tick)
|
|
74
|
-
return ALLOW; // 4
|
|
75
|
-
// 5
|
|
74
|
+
return ALLOW; // 4: a human turn intervened
|
|
75
|
+
// 5: the pause (plan-worded when no step is landing). A settings `auto` never
|
|
76
76
|
// allows; when one is set, the refusal notes it rather than honoring it.
|
|
77
77
|
const base = input.step === null ? NO_TURN_PLAN_MESSAGE : NO_TURN_MESSAGE;
|
|
78
78
|
return { allow: false, reason: 'no-turn', message: input.settingsAuto ? base + SETTINGS_AUTO_NOTE : base };
|
|
@@ -80,7 +80,7 @@ export function evaluateLatch(input) {
|
|
|
80
80
|
/**
|
|
81
81
|
* Parse a GRANT file's content (`auto` | `range <M>`).
|
|
82
82
|
*
|
|
83
|
-
* Anything else
|
|
83
|
+
* Anything else (including a corrupted or hand-mangled file) contributes
|
|
84
84
|
* nothing: no grant, never an error.
|
|
85
85
|
*/
|
|
86
86
|
export function parseGrant(raw) {
|
|
@@ -105,7 +105,7 @@ export function checkLatch(root, step) {
|
|
|
105
105
|
tick: readCount(tickPath(root)),
|
|
106
106
|
grant: readGrant(root),
|
|
107
107
|
step,
|
|
108
|
-
// Read from the settings ladder
|
|
108
|
+
// Read from the settings ladder, but only to decorate the pause, never to allow.
|
|
109
109
|
settingsAuto: resolveBoolean(root, 'auto', false),
|
|
110
110
|
});
|
|
111
111
|
}
|
package/dist/lib/orient.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The orientation model behind `status`: parse a live build session into the
|
|
2
|
-
// where-am-I dashboard
|
|
2
|
+
// where-am-I dashboard: title, phase, step list, last checkpoint, counts, and
|
|
3
3
|
// one suggested next move. Pure and best-effort by design: it takes raw file
|
|
4
4
|
// contents (no fs), and a malformed document degrades to fewer fields rather
|
|
5
5
|
// than throwing.
|
|
@@ -20,7 +20,7 @@ function sectionLines(content, heading) {
|
|
|
20
20
|
return lines.slice(start + 1, end);
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* The build title
|
|
23
|
+
* The build title: the first `# ` heading in intent.md, or null when absent.
|
|
24
24
|
*/
|
|
25
25
|
export function parseTitle(intent) {
|
|
26
26
|
for (const line of intent.split('\n')) {
|
|
@@ -32,12 +32,14 @@ export function parseTitle(intent) {
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Parse the steps under `## Steps`: `N. [ |x] Title
|
|
35
|
+
* Parse the steps under `## Steps`: `N. [ |x] Title, **done when:** ...`
|
|
36
|
+
* (the legacy em-dash separator before the marker parses the same).
|
|
36
37
|
*
|
|
37
|
-
* The title is the text
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* The title is the text before the `**done when:**` marker on the opener line,
|
|
39
|
+
* trailing separator stripped; an opener without the marker falls back to the
|
|
40
|
+
* text up to the first em dash. `planned` is true when the step's block carries
|
|
41
|
+
* a `done when` criterion. Only `## Steps` is machine-read as the build plan;
|
|
42
|
+
* narrative roadmap prose lives in its own section and never lands here.
|
|
41
43
|
*/
|
|
42
44
|
export function parseSteps(intent) {
|
|
43
45
|
const lines = sectionLines(intent, '## Steps');
|
|
@@ -46,10 +48,12 @@ export function parseSteps(intent) {
|
|
|
46
48
|
const m = /^(\d+)\.\s+\[([ xX])\]\s+(.*)$/.exec(line);
|
|
47
49
|
if (m) {
|
|
48
50
|
const rest = m[3] ?? '';
|
|
51
|
+
const marker = /\*\*done when:\*\*/i.exec(rest);
|
|
52
|
+
const head = marker ? rest.slice(0, marker.index) : (rest.split('—')[0] ?? rest);
|
|
49
53
|
starts.push({
|
|
50
54
|
n: Number(m[1]),
|
|
51
55
|
done: (m[2] ?? ' ').toLowerCase() === 'x',
|
|
52
|
-
title:
|
|
56
|
+
title: head.replace(/\s*[—,-]+\s*$/, '').trim(),
|
|
53
57
|
idx,
|
|
54
58
|
});
|
|
55
59
|
}
|
|
@@ -88,13 +92,28 @@ export function markStepDone(intent, n) {
|
|
|
88
92
|
}
|
|
89
93
|
/**
|
|
90
94
|
* Count the questions still open: `- Q\d+:` openers under `## Open questions`
|
|
91
|
-
* that
|
|
95
|
+
* that carry neither a resolution marker nor an unfilled scaffold body.
|
|
92
96
|
*
|
|
93
|
-
* The opener may carry a slug-at-birth gloss
|
|
94
|
-
*
|
|
97
|
+
* The opener may carry a slug-at-birth gloss (`- Q2 (some-slug): ...`) and the
|
|
98
|
+
* anchored form a citable question is born in:
|
|
99
|
+
* `- <a id="q2"></a>**Q2 (some-slug)**: ...`, which is what a `[Q2 (some-slug)](#q2)`
|
|
100
|
+
* reference site elsewhere in the file lands on. The count reads through both, and
|
|
101
|
+
* through any mix of them a hand-edited intent leaves mid-build; sub-lines
|
|
102
|
+
* (`*plain:*`/`*lean:*`) never match.
|
|
103
|
+
*
|
|
104
|
+
* Two rules settle the rest. "Resolved" counts as a marker only as a whole word, so
|
|
105
|
+
* a question whose opener still calls itself "unresolved" keeps counting; a bare
|
|
106
|
+
* substring read that as resolved and dropped a live hole off the dashboard. And an
|
|
107
|
+
* opener whose body has not been filled in yet, still opening on its `<...>`
|
|
108
|
+
* fill-in, is scaffold rather than a question, which is what makes a fresh build
|
|
109
|
+
* report zero rather than one.
|
|
95
110
|
*/
|
|
96
111
|
export function parseOpenQuestions(intent) {
|
|
97
|
-
|
|
112
|
+
const opener = /^-\s+(?:<a id="[^"]*"><\/a>\s*)?\*{0,2}Q\d+(?: \([^)]+\))?\*{0,2}:\s*(.*)$/;
|
|
113
|
+
return sectionLines(intent, '## Open questions').filter((l) => {
|
|
114
|
+
const body = opener.exec(l.trim())?.[1];
|
|
115
|
+
return body !== undefined && !/\bresolved\b/i.test(l) && !/^<[^>]*>/.test(body);
|
|
116
|
+
}).length;
|
|
98
117
|
}
|
|
99
118
|
/**
|
|
100
119
|
* Count the open parked items: `- [ ]` lines under `## Park list`.
|
|
@@ -122,12 +141,12 @@ export function parseLastCheckpoint(checkpoints) {
|
|
|
122
141
|
return last;
|
|
123
142
|
}
|
|
124
143
|
/**
|
|
125
|
-
* The SHA of the last ledger line of ANY kind
|
|
144
|
+
* The SHA of the last ledger line of ANY kind: baseline, plan, or step.
|
|
126
145
|
*
|
|
127
146
|
* This is the reconciliation anchor for the out-of-band commit count: a commit
|
|
128
147
|
* that lands after the plan but before the first step checkpoint is exactly the
|
|
129
148
|
* window the reconciliation line exists for, so anchoring on step lines alone
|
|
130
|
-
* would leave it invisible. `parseLastCheckpoint` stays step-only
|
|
149
|
+
* would leave it invisible. `parseLastCheckpoint` stays step-only; it feeds
|
|
131
150
|
* the "last checkpoint step N" display, where baseline/plan would be noise.
|
|
132
151
|
*/
|
|
133
152
|
export function lastLedgerSha(checkpoints) {
|
|
@@ -162,7 +181,7 @@ function nextMove(spiking, steps, inFlight, parked) {
|
|
|
162
181
|
return 'plan the first step — `/plumbbob:step`';
|
|
163
182
|
}
|
|
164
183
|
// Batch-default: the steps were planned up front, so finishing them usually
|
|
165
|
-
// means "finish up"
|
|
184
|
+
// means "finish up", but `/plumbbob:step` can still add an increment if reality grew.
|
|
166
185
|
const harvest = parked > 0 ? `harvest ${parked} parked idea${parked === 1 ? '' : 's'} — \`/plumbbob:harvest\`; then ` : '';
|
|
167
186
|
return `${harvest}finish up — \`/plumbbob:finish\` (or \`/plumbbob:step\` to add another increment)`;
|
|
168
187
|
}
|
|
@@ -207,7 +226,7 @@ export function formatOrientation(o) {
|
|
|
207
226
|
return head;
|
|
208
227
|
}
|
|
209
228
|
// Surface the next step's detail so the human can review it (and `/plumbbob:step`-
|
|
210
|
-
// revise) before building. Only what's present
|
|
229
|
+
// revise) before building. Only what's present: a rough step shows neither.
|
|
211
230
|
const detail = [];
|
|
212
231
|
if (o.nextDoneWhen !== null) {
|
|
213
232
|
detail.push(` done when: ${o.nextDoneWhen}`);
|
|
@@ -225,7 +244,7 @@ export function formatOrientation(o) {
|
|
|
225
244
|
const cpLine = cp === null ? 'last checkpoint none yet' : `last checkpoint step ${cp.n} · ${cp.sha.slice(0, 7)}`;
|
|
226
245
|
// A neutral reconciliation note, only when there is something to reconcile:
|
|
227
246
|
// commits landed since the last checkpoint that plumbbob's ledger didn't
|
|
228
|
-
// record. Informational
|
|
247
|
+
// record. Informational: the human commits freely, so this never gates.
|
|
229
248
|
const receipts = o.outOfBand > 0
|
|
230
249
|
? [`${o.outOfBand} commit${o.outOfBand === 1 ? '' : 's'} since the last checkpoint landed outside plumbbob's ledger.`]
|
|
231
250
|
: [];
|
package/dist/lib/reentry.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
// The failure it exists to stop is not slowness, it is unbounded recursion. A
|
|
5
5
|
// repo whose `check` setting is its own full pipeline (`pnpm check`) has a gate
|
|
6
|
-
// that runs its test suite; anything in that suite which gates THAT SAME REPO
|
|
7
|
-
// a test driving `checkpoint` against the process cwd
|
|
6
|
+
// that runs its test suite; anything in that suite which gates THAT SAME REPO
|
|
7
|
+
// (a test driving `checkpoint` against the process cwd) spawns a second full
|
|
8
8
|
// pipeline, whose suite spawns a third. Each generation forks wider than the
|
|
9
9
|
// last, and nothing in the loop is aware there is a loop. The only thing that
|
|
10
10
|
// ended the first observed occurrence was checkride's 600s per-check cap
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// It is scoped BY ROOT, and that is the whole design. A flat "a gate is
|
|
14
14
|
// running" flag is wrong in the one case that matters most: plumbbob's own
|
|
15
15
|
// suite legitimately gates dozens of fixture repos, and when the suite itself
|
|
16
|
-
// is running under this repo's gate it inherits the marker
|
|
16
|
+
// is running under this repo's gate it inherits the marker, so a flat flag
|
|
17
17
|
// refuses every one of them and turns a green suite red. Recursion is a repo
|
|
18
18
|
// re-entering ITS OWN gate; a different root is ordinary nested work and must
|
|
19
19
|
// pass through untouched.
|
package/dist/lib/settings.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// The settings ladder: a resolved setting comes from, in priority order,
|
|
2
|
-
// 1. a CLI flag
|
|
3
|
-
// 2. settings.local.json
|
|
4
|
-
// 3. settings.json
|
|
5
|
-
// 4. a built-in default
|
|
2
|
+
// 1. a CLI flag: passed in by the verb (undefined when absent)
|
|
3
|
+
// 2. settings.local.json: untracked personal overlay, per-worktree
|
|
4
|
+
// 3. settings.json: tracked project defaults
|
|
5
|
+
// 4. a built-in default: supplied by the caller
|
|
6
6
|
// The first defined rung wins. Both files are optional JSON; a missing or
|
|
7
7
|
// malformed file contributes nothing rather than throwing, so a broken personal
|
|
8
8
|
// overlay can never wedge the tool. Functional and procedural, node builtins only.
|
|
9
9
|
//
|
|
10
|
-
// Known keys: `check` (string
|
|
11
|
-
// default in settings.json) and `auto` (boolean
|
|
10
|
+
// Known keys: `check` (string: the command for the heavy verify gate, a project
|
|
11
|
+
// default in settings.json) and `auto` (boolean: a personal preference, so it
|
|
12
12
|
// belongs in settings.local.json). `auto` is NOT a checkpoint grant: the approval
|
|
13
|
-
// latch never reads it to allow a land
|
|
14
|
-
// it can forge is no grant
|
|
13
|
+
// latch never reads it to allow a land (a model can write this file, and a grant
|
|
14
|
+
// it can forge is no grant) only to name it at the pause when set; self-approval
|
|
15
15
|
// comes solely from the human's literal `/build --auto` or a typed step range.
|
|
16
|
-
// The per-worktree active-build cursor is NOT here
|
|
16
|
+
// The per-worktree active-build cursor is NOT here: it lives in the STATE file
|
|
17
17
|
// (see sidecar.ts), which keeps this overlay a human-authored file rather than
|
|
18
18
|
// tool-managed state.
|
|
19
19
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -34,7 +34,7 @@ export function localSettingsPath(root) {
|
|
|
34
34
|
/**
|
|
35
35
|
* Read one settings file as a plain object.
|
|
36
36
|
*
|
|
37
|
-
* A missing, malformed, or non-object file yields {}
|
|
37
|
+
* A missing, malformed, or non-object file yields {}: an empty rung that
|
|
38
38
|
* contributes nothing rather than wedging the tool.
|
|
39
39
|
*/
|
|
40
40
|
function readSettings(path) {
|
|
@@ -61,7 +61,7 @@ function resolveSetting(root, key, flag) {
|
|
|
61
61
|
return readSettings(settingsPath(root))[key];
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* Resolve a string setting (
|
|
64
|
+
* Resolve a string setting (for example `check`).
|
|
65
65
|
*
|
|
66
66
|
* A missing rung, or one holding a non-string / blank value, yields the
|
|
67
67
|
* caller's fallback rather than gating on garbage.
|
|
@@ -71,7 +71,7 @@ export function resolveString(root, key, fallback, flag) {
|
|
|
71
71
|
return typeof value === 'string' && value.trim().length > 0 ? value : fallback;
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
* Resolve a boolean setting (
|
|
74
|
+
* Resolve a boolean setting (for example `auto`). A missing or non-boolean rung
|
|
75
75
|
* yields the caller's fallback.
|
|
76
76
|
*/
|
|
77
77
|
export function resolveBoolean(root, key, fallback, flag) {
|
|
@@ -79,10 +79,10 @@ export function resolveBoolean(root, key, fallback, flag) {
|
|
|
79
79
|
return typeof value === 'boolean' ? value : fallback;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Resolve a non-negative integer setting (
|
|
82
|
+
* Resolve a non-negative integer setting (for example `agentTimeout`).
|
|
83
83
|
*
|
|
84
84
|
* A missing rung, or one holding a non-finite / negative / non-integer number,
|
|
85
|
-
* yields the caller's fallback rather than a garbage timeout
|
|
85
|
+
* yields the caller's fallback rather than a garbage timeout: 0 means "no
|
|
86
86
|
* timeout", so a broken value must never silently become one.
|
|
87
87
|
*/
|
|
88
88
|
export function resolveNumber(root, key, fallback, flag) {
|
|
@@ -90,11 +90,11 @@ export function resolveNumber(root, key, fallback, flag) {
|
|
|
90
90
|
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
* Resolve an object-valued setting (
|
|
93
|
+
* Resolve an object-valued setting (for example the `agents` slot-binding defaults)
|
|
94
94
|
* across the same ladder.
|
|
95
95
|
*
|
|
96
96
|
* The first defined, object-typed rung wins; a missing rung, or one holding a
|
|
97
|
-
* non-object / array / scalar, yields {} rather than garbage
|
|
97
|
+
* non-object / array / scalar, yields {} rather than garbage: mirrors the
|
|
98
98
|
* string/boolean/number resolvers above. No flag rung: the caller (`agent
|
|
99
99
|
* run`) merges these under the per-build harness bindings, not over a flag.
|
|
100
100
|
*/
|