plumbbob 0.1.3 → 0.2.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.
Files changed (54) hide show
  1. package/README.md +69 -26
  2. package/dist/cli.js +99 -0
  3. package/dist/lib/archive.js +69 -0
  4. package/dist/lib/check.js +26 -0
  5. package/dist/lib/git.js +62 -0
  6. package/dist/lib/intent.js +88 -0
  7. package/dist/lib/settings.js +79 -0
  8. package/dist/lib/sidecar.js +69 -0
  9. package/dist/verbs/build.js +30 -0
  10. package/dist/verbs/done.js +63 -0
  11. package/dist/verbs/finish.js +54 -0
  12. package/dist/verbs/mode.js +26 -0
  13. package/dist/verbs/park.js +51 -0
  14. package/dist/verbs/revert.js +96 -0
  15. package/dist/verbs/review.js +24 -0
  16. package/dist/verbs/setup.js +141 -0
  17. package/dist/verbs/spike.js +113 -0
  18. package/dist/verbs/start.js +68 -0
  19. package/dist/verbs/status.js +13 -0
  20. package/dist/verbs/wrap.js +28 -0
  21. package/hooks/bash-guard.sh +5 -1
  22. package/package.json +6 -4
  23. package/skills/park/SKILL.md +4 -4
  24. package/skills/pb-build/SKILL.md +19 -0
  25. package/skills/pb-done/SKILL.md +18 -0
  26. package/skills/pb-finish/SKILL.md +18 -0
  27. package/skills/pb-revert/SKILL.md +19 -0
  28. package/skills/pb-review/SKILL.md +18 -0
  29. package/skills/pb-spike/SKILL.md +19 -0
  30. package/skills/pb-start/SKILL.md +19 -0
  31. package/skills/pb-wrap/SKILL.md +18 -0
  32. package/skills/plumbbob-docs/SKILL.md +2 -2
  33. package/skills/plumbbob-interrogate/SKILL.md +2 -2
  34. package/skills/plumbbob-report/SKILL.md +2 -2
  35. package/skills/plumbbob-triage/SKILL.md +2 -2
  36. package/src/cli.ts +0 -121
  37. package/src/lib/archive.ts +0 -76
  38. package/src/lib/check.ts +0 -29
  39. package/src/lib/git.ts +0 -73
  40. package/src/lib/intent.ts +0 -103
  41. package/src/lib/settings.ts +0 -90
  42. package/src/lib/sidecar.ts +0 -82
  43. package/src/verbs/build.ts +0 -38
  44. package/src/verbs/done.ts +0 -71
  45. package/src/verbs/finish.ts +0 -72
  46. package/src/verbs/mode.ts +0 -27
  47. package/src/verbs/park.ts +0 -56
  48. package/src/verbs/revert.ts +0 -110
  49. package/src/verbs/review.ts +0 -30
  50. package/src/verbs/setup.ts +0 -95
  51. package/src/verbs/spike.ts +0 -129
  52. package/src/verbs/start.ts +0 -103
  53. package/src/verbs/status.ts +0 -15
  54. package/src/verbs/wrap.ts +0 -34
@@ -1,110 +0,0 @@
1
- // `plumbbob revert [--to n]` — git reset --hard to a checkpoint SHA (the most
2
- // recent step, or `--to n`, with the baseline as fallback), then remove untracked
3
- // files under the SEAM only. The sidecar is git-excluded (D17), so the reset
4
- // never touches it — park lines and intent edits survive the revert (C4).
5
-
6
- import { readFileSync, rmSync } from 'node:fs'
7
- import { join } from 'node:path'
8
- import { findRepoRoot, resetHard, untrackedPaths } from '../lib/git.ts'
9
- import { checkpointsPath, hasSession, seamPath, stepPath, writeState } from '../lib/sidecar.ts'
10
- import { matchesSeam } from '../lib/intent.ts'
11
-
12
- export function revert(cwd: string, args: ReadonlyArray<string>): number {
13
- const root = findRepoRoot(cwd)
14
- if (root === null || !hasSession(root)) {
15
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n')
16
- return 1
17
- }
18
-
19
- const to = parseTo(args)
20
- if (to === 'invalid') {
21
- process.stderr.write('plumbbob: revert --to needs a step number. Try: plumbbob revert --to 2.\n')
22
- return 1
23
- }
24
-
25
- const checkpoints = readCheckpoints(root)
26
- let sha: string | undefined
27
- if (to === null) {
28
- sha = checkpoints.steps.at(-1)?.sha ?? checkpoints.baseline
29
- } else {
30
- const entry = checkpoints.steps.find((e) => e.n === to)
31
- if (entry === undefined) {
32
- process.stderr.write(`plumbbob: no checkpoint recorded for step ${to}.\n`)
33
- return 1
34
- }
35
- sha = entry.sha
36
- }
37
- if (sha === undefined) {
38
- process.stderr.write('plumbbob: no baseline recorded in checkpoints — cannot revert.\n')
39
- return 1
40
- }
41
-
42
- // Compute untracked-in-seam BEFORE the reset (reset --hard leaves untracked and
43
- // ignored files alone, so they must be removed explicitly afterward).
44
- const seam = readSeamTokens(root)
45
- const toRemove = untrackedPaths(root).filter((p) => matchesSeam(p, seam))
46
-
47
- resetHard(root, sha)
48
- for (const rel of toRemove) {
49
- rmSync(join(root, rel), { force: true, recursive: true })
50
- }
51
- rmSync(seamPath(root), { force: true })
52
- rmSync(stepPath(root), { force: true })
53
- writeState(root, 'DESIGN')
54
-
55
- process.stdout.write(
56
- `plumbbob: reverted to ${sha.slice(0, 9)} — STATE=DESIGN. Park lines and intent edits were preserved.\n`,
57
- )
58
- return 0
59
- }
60
-
61
- function parseTo(args: ReadonlyArray<string>): number | null | 'invalid' {
62
- const idx = args.indexOf('--to')
63
- if (idx === -1) {
64
- return null
65
- }
66
- const raw = args[idx + 1]
67
- if (raw === undefined || !/^\d+$/.test(raw)) {
68
- return 'invalid'
69
- }
70
- return Number(raw)
71
- }
72
-
73
- type Checkpoints = {
74
- readonly baseline: string | undefined
75
- readonly steps: ReadonlyArray<{ readonly n: number; readonly sha: string }>
76
- }
77
-
78
- function readCheckpoints(root: string): Checkpoints {
79
- let content = ''
80
- try {
81
- content = readFileSync(checkpointsPath(root), 'utf8')
82
- } catch {
83
- return { baseline: undefined, steps: [] }
84
- }
85
- let baseline: string | undefined
86
- const steps: Array<{ readonly n: number; readonly sha: string }> = []
87
- for (const line of content.split('\n')) {
88
- const baselineMatch = /^baseline\s+(\S+)/.exec(line)
89
- if (baselineMatch) {
90
- baseline = baselineMatch[1]
91
- continue
92
- }
93
- const stepMatch = /^step\s+(\d+)\s+(\S+)/.exec(line)
94
- if (stepMatch) {
95
- steps.push({ n: Number(stepMatch[1]), sha: stepMatch[2] ?? '' })
96
- }
97
- }
98
- return { baseline, steps }
99
- }
100
-
101
- function readSeamTokens(root: string): ReadonlyArray<string> {
102
- try {
103
- return readFileSync(seamPath(root), 'utf8')
104
- .split('\n')
105
- .map((l) => l.trim())
106
- .filter((l) => l.length > 0)
107
- } catch {
108
- return []
109
- }
110
- }
@@ -1,30 +0,0 @@
1
- // `plumbbob review` — run the heavy check from BUILD. Green flips to REVIEW (the
2
- // muzzle goes back on so reading the diff can't slide into editing). Red stays in
3
- // BUILD. The state only advances on a green check.
4
-
5
- import { findRepoRoot } from '../lib/git.ts'
6
- import { hasSession, readState, writeState } from '../lib/sidecar.ts'
7
- import { runCheck } from '../lib/check.ts'
8
-
9
- export function review(cwd: string): number {
10
- const root = findRepoRoot(cwd)
11
- if (root === null || !hasSession(root)) {
12
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n')
13
- return 1
14
- }
15
- if (readState(root) !== 'BUILD') {
16
- process.stderr.write(`plumbbob: review runs from BUILD (current state is ${readState(root) ?? 'UNKNOWN'}). Run \`plumbbob build <n>\` first.\n`)
17
- return 1
18
- }
19
-
20
- if (runCheck(root) !== 0) {
21
- process.stderr.write('plumbbob: check failed (red) — staying in BUILD. Fix it and re-run `review` (or `done` once green).\n')
22
- return 1
23
- }
24
-
25
- writeState(root, 'REVIEW')
26
- process.stdout.write(
27
- 'plumbbob: check green — STATE=REVIEW. Read the diff against intent.md (edits muzzled). `build <n>` to re-enter and fix, or `done` to checkpoint.\n',
28
- )
29
- return 0
30
- }
@@ -1,95 +0,0 @@
1
- // `plumbbob setup` — the production installer (D27, replacing dev-install.sh).
2
- // Copies the hooks to ~/.claude/plumbbob/hooks/ and the skills to
3
- // ~/.claude/skills/, then merges the hook registration into the settings file the
4
- // chosen scope selects:
5
- // (default) ~/.claude/settings.json — global, this machine
6
- // --project <repo>/.claude/settings.json — committable, enrolls the team
7
- // --local <repo>/.claude/settings.local.json — personal, untracked
8
- // Hooks and skills always install once under ~/.claude/; only the registration's
9
- // location and command-path form vary by scope. `--uninstall` strips the
10
- // registration (the installed files are left in place). The hooks are
11
- // session-gated, so a global registration is safe in every repo (C7).
12
-
13
- import { chmodSync, cpSync } from 'node:fs'
14
- import { homedir } from 'node:os'
15
- import { join } from 'node:path'
16
- import { fileURLToPath } from 'node:url'
17
- import { findRepoRoot } from '../lib/git.ts'
18
- import { mergeRegistration, readSettings, stripRegistration, writeSettings } from '../lib/settings.ts'
19
-
20
- const HOOK_FILES: ReadonlyArray<string> = ['pre-edit.sh', 'bash-guard.sh', 'post-edit.sh']
21
-
22
- type Scope = 'global' | 'project' | 'local'
23
-
24
- export function setup(cwd: string, args: ReadonlyArray<string>): number {
25
- const scope = pickScope(args)
26
- const home = process.env.HOME ?? homedir()
27
-
28
- const settingsFile = resolveSettingsFile(scope, cwd, home)
29
- if (settingsFile === null) {
30
- process.stderr.write(
31
- `plumbbob: --${scope} writes <repo>/.claude/, but this is not a git repository. Run setup from inside the repo, or use the default global scope.\n`,
32
- )
33
- return 1
34
- }
35
-
36
- if (args.includes('--uninstall')) {
37
- writeSettings(settingsFile, stripRegistration(readSettings(settingsFile)))
38
- process.stdout.write(
39
- `plumbbob: removed the hook registration from ${settingsFile}. The installed hooks/skills under ~/.claude/ were left in place.\n`,
40
- )
41
- return 0
42
- }
43
-
44
- // Hooks + skills install once under ~/.claude/ regardless of scope.
45
- const installedHooksDir = join(home, '.claude', 'plumbbob', 'hooks')
46
- const installedSkillsDir = join(home, '.claude', 'skills')
47
- cpSync(packageDir('hooks'), installedHooksDir, { recursive: true })
48
- for (const file of HOOK_FILES) {
49
- chmodSync(join(installedHooksDir, file), 0o755)
50
- }
51
- cpSync(packageDir('skills'), installedSkillsDir, { recursive: true })
52
-
53
- // D27: global registers absolute command paths (its settings file is itself
54
- // under ~); the repo-scoped files register `~`-prefixed paths so committed
55
- // settings carry no machine-absolute home dir.
56
- const commandDir = scope === 'global' ? installedHooksDir : '~/.claude/plumbbob/hooks'
57
- writeSettings(settingsFile, mergeRegistration(readSettings(settingsFile), commandDir))
58
-
59
- process.stdout.write(
60
- `plumbbob: installed hooks → ${installedHooksDir}, skills → ${installedSkillsDir}.\n` +
61
- `plumbbob: registered the hooks in ${settingsFile} (${scope} scope).\n` +
62
- 'plumbbob: restart Claude Code (or reload settings) for the hooks to take effect.\n' +
63
- "plumbbob: installed from npm, `plumbbob` (and its `pb` alias) is already on your PATH; from a dev checkout add `alias plumbbob='node <repo>/src/cli.ts'` so the skills' status pre-injection resolves.\n",
64
- )
65
- return 0
66
- }
67
-
68
- function pickScope(args: ReadonlyArray<string>): Scope {
69
- if (args.includes('--project')) {
70
- return 'project'
71
- }
72
- if (args.includes('--local')) {
73
- return 'local'
74
- }
75
- return 'global'
76
- }
77
-
78
- // The settings file the scope writes. Global lives under ~; the repo scopes need
79
- // a git root and return null outside one so the verb can refuse with a clear note.
80
- function resolveSettingsFile(scope: Scope, cwd: string, home: string): string | null {
81
- if (scope === 'global') {
82
- return join(home, '.claude', 'settings.json')
83
- }
84
- const root = findRepoRoot(cwd)
85
- if (root === null) {
86
- return null
87
- }
88
- return scope === 'project' ? join(root, '.claude', 'settings.json') : join(root, '.claude', 'settings.local.json')
89
- }
90
-
91
- // hooks/ and skills/ ship beside src/ in the package; resolve them off this
92
- // module's URL the way `start` resolves templates/.
93
- function packageDir(name: string): string {
94
- return fileURLToPath(new URL(`../../${name}/`, import.meta.url))
95
- }
@@ -1,129 +0,0 @@
1
- // `plumbbob spike` (D18) — the spike lifecycle for a genuine fork the design
2
- // phase couldn't settle. `spike "<slug>" [opt…]` creates a sibling git worktree +
3
- // `spike/<slug>-<opt>` branch per option OUTSIDE the repo root (default opts a/b)
4
- // and sets STATE=SPIKE; the main tree stays DESIGN-locked while you experiment in
5
- // the worktrees, which are hook-dormant by construction — the untracked sidecar
6
- // (D17) doesn't exist in a fresh checkout, so the hooks find no STATE there.
7
- // `spike done` removes every spike worktree + branch and returns to DESIGN.
8
- //
9
- // Worktree git calls run directly here rather than via lib/git.ts (which holds the
10
- // shared additive read/commit helpers): worktree management is spike-local, and
11
- // this is the only place Plumbbob creates branches.
12
-
13
- import { execFileSync } from 'node:child_process'
14
- import { existsSync } from 'node:fs'
15
- import { basename, dirname, join } from 'node:path'
16
- import { findRepoRoot } from '../lib/git.ts'
17
- import { hasSession, readState, writeState } from '../lib/sidecar.ts'
18
-
19
- const DEFAULT_OPTIONS: ReadonlyArray<string> = ['a', 'b']
20
-
21
- export function spike(cwd: string, args: ReadonlyArray<string>): number {
22
- const root = findRepoRoot(cwd)
23
- if (root === null || !hasSession(root)) {
24
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n')
25
- return 1
26
- }
27
- const positionals = args.filter((a) => !a.startsWith('--'))
28
- if (positionals[0] === 'done') {
29
- return spikeDone(root)
30
- }
31
- return spikeStart(root, positionals)
32
- }
33
-
34
- function spikeStart(root: string, positionals: ReadonlyArray<string>): number {
35
- const state = readState(root)
36
- if (state === 'SPIKE') {
37
- process.stderr.write('plumbbob: already in a spike. Run `plumbbob spike done` to close it first.\n')
38
- return 1
39
- }
40
- if (state !== 'DESIGN') {
41
- process.stderr.write(
42
- `plumbbob: spike starts from DESIGN (current state is ${state ?? 'UNKNOWN'}). ` +
43
- 'A spike is a deliberate fork — close the current step first with `done`.\n',
44
- )
45
- return 1
46
- }
47
- const slug = sanitize(positionals[0] ?? '')
48
- if (slug.length === 0) {
49
- process.stderr.write('plumbbob: spike needs a slug. Try: plumbbob spike "auth-store" a b.\n')
50
- return 1
51
- }
52
- const explicit = positionals.slice(1).map(sanitize).filter((o) => o.length > 0)
53
- const options = explicit.length > 0 ? explicit : DEFAULT_OPTIONS
54
-
55
- const created: string[] = []
56
- for (const opt of options) {
57
- const path = join(dirname(root), `${basename(root)}-spike-${slug}-${opt}`)
58
- if (existsSync(path)) {
59
- process.stderr.write(`plumbbob: ${path} already exists — remove it or run \`plumbbob spike done\` first.\n`)
60
- return 1
61
- }
62
- git(root, ['worktree', 'add', '-b', `spike/${slug}-${opt}`, path, 'HEAD'])
63
- created.push(path)
64
- }
65
-
66
- writeState(root, 'SPIKE')
67
- process.stdout.write(
68
- `plumbbob: STATE=SPIKE — the main tree stays DESIGN-locked. Experiment in the throwaway worktrees:\n${created
69
- .map((p) => ` ${p}`)
70
- .join('\n')}\nWhen you've decided, record the verdict in intent.md and run \`plumbbob spike done\`.\n`,
71
- )
72
- return 0
73
- }
74
-
75
- function spikeDone(root: string): number {
76
- const state = readState(root)
77
- if (state !== 'SPIKE') {
78
- process.stderr.write(`plumbbob: no active spike (state is ${state ?? 'UNKNOWN'}).\n`)
79
- return 1
80
- }
81
- for (const path of spikeWorktrees(root)) {
82
- git(root, ['worktree', 'remove', '--force', path])
83
- }
84
- git(root, ['worktree', 'prune'])
85
- for (const branch of spikeBranches(root)) {
86
- git(root, ['branch', '-D', branch])
87
- }
88
- writeState(root, 'DESIGN')
89
- process.stdout.write(
90
- 'plumbbob: spike closed — STATE=DESIGN, worktrees and branches removed. ' +
91
- 'Record the verdict (which option won, and why) in intent.md before you `build`.\n',
92
- )
93
- return 0
94
- }
95
-
96
- // Worktree paths whose checked-out branch is under spike/ — parsed from the
97
- // porcelain output (blank-line-separated `worktree <path>` / `branch <ref>` blocks).
98
- function spikeWorktrees(root: string): ReadonlyArray<string> {
99
- const out = git(root, ['worktree', 'list', '--porcelain'])
100
- const paths: string[] = []
101
- let current: string | null = null
102
- for (const line of out.split('\n')) {
103
- if (line.startsWith('worktree ')) {
104
- current = line.slice('worktree '.length)
105
- } else if (current !== null && line.startsWith('branch refs/heads/spike/')) {
106
- paths.push(current)
107
- }
108
- }
109
- return paths
110
- }
111
-
112
- function spikeBranches(root: string): ReadonlyArray<string> {
113
- const out = git(root, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/spike/'])
114
- return out.length === 0 ? [] : out.split('\n').filter((b) => b.length > 0)
115
- }
116
-
117
- function sanitize(raw: string): string {
118
- return raw
119
- .toLowerCase()
120
- .replace(/[^a-z0-9]+/g, '-')
121
- .replace(/^-+|-+$/g, '')
122
- }
123
-
124
- function git(root: string, args: ReadonlyArray<string>): string {
125
- return execFileSync('git', ['-C', root, ...args], {
126
- encoding: 'utf8',
127
- stdio: ['ignore', 'pipe', 'inherit'],
128
- }).trim()
129
- }
@@ -1,103 +0,0 @@
1
- // `plumbbob start "<title>"` — scaffold the sidecar, record the baseline, enter
2
- // DESIGN. Refuses on a dirty tree (D22), an existing session, or a non-git dir.
3
-
4
- import { fileURLToPath } from 'node:url'
5
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
6
- import { join } from 'node:path'
7
- import { findRepoRoot, hasCommit, headSha, isDirty } from '../lib/git.ts'
8
- import {
9
- sidecarDir,
10
- checkpointsPath,
11
- configPath,
12
- intentPath,
13
- buildLogPath,
14
- writeState,
15
- hasSession,
16
- excludeSidecar,
17
- } from '../lib/sidecar.ts'
18
-
19
- const DEFAULT_CHECK = 'pnpm run check'
20
-
21
- export function start(cwd: string, args: ReadonlyArray<string>): number {
22
- const positionals = args.filter((a) => !a.startsWith('--'))
23
- const allowDirty = args.includes('--allow-dirty')
24
- const title = (positionals[0] ?? '').trim()
25
-
26
- if (title.length === 0) {
27
- process.stderr.write('plumbbob: start needs a title. Try: plumbbob start "what you are building".\n')
28
- return 1
29
- }
30
-
31
- const root = findRepoRoot(cwd)
32
- if (root === null) {
33
- process.stderr.write(
34
- 'plumbbob: not a git repository. Plumbbob records a baseline commit — run `git init` and make an initial commit first.\n',
35
- )
36
- return 1
37
- }
38
- if (!hasCommit(root)) {
39
- process.stderr.write(
40
- 'plumbbob: this repository has no commits yet. Make an initial commit so `start` can record a baseline.\n',
41
- )
42
- return 1
43
- }
44
- if (hasSession(root)) {
45
- process.stderr.write(
46
- 'plumbbob: a session is already active here. Run `plumbbob finish` to close it before starting another.\n',
47
- )
48
- return 1
49
- }
50
- if (isDirty(root)) {
51
- if (!allowDirty) {
52
- process.stderr.write(
53
- 'plumbbob: the working tree is dirty. Commit or stash first, or `plumbbob start --allow-dirty "<title>"` to record the current HEAD as the baseline.\n',
54
- )
55
- return 1
56
- }
57
- process.stderr.write(
58
- 'plumbbob: WARNING --allow-dirty: recording HEAD as baseline with a dirty tree. A later revert-to-baseline will DISCARD the uncommitted work.\n',
59
- )
60
- }
61
-
62
- const sha = headSha(root)
63
- const check = detectCheck(root)
64
-
65
- mkdirSync(sidecarDir(root), { recursive: true })
66
- writeState(root, 'DESIGN')
67
- writeFileSync(checkpointsPath(root), `baseline ${sha}\n`)
68
- writeFileSync(configPath(root), `check=${check.command}\n`)
69
- writeFileSync(intentPath(root), stamp(readTemplate('intent.md'), title, check.command))
70
- writeFileSync(buildLogPath(root), stamp(readTemplate('build-log.md'), title, check.command))
71
- excludeSidecar(root)
72
-
73
- if (check.warn) {
74
- process.stderr.write(
75
- `plumbbob: WARNING the heavy check '${check.command}' is not defined in this repo's package.json. Edit .plumbbob/config (check=...) to set the real gate before \`review\`/\`done\`.\n`,
76
- )
77
- }
78
- process.stdout.write(
79
- `plumbbob: started "${title}" — STATE=DESIGN, baseline ${sha.slice(0, 9)}. Frame and decide in .plumbbob/intent.md; flip to BUILD only once the decisions are made.\n`,
80
- )
81
- return 0
82
- }
83
-
84
- // D24: default the heavy check to `pnpm run check`; warn (but still record it)
85
- // when the target repo has no such script, so a non-pnpm repo gets a clear nudge.
86
- function detectCheck(root: string): { readonly command: string; readonly warn: boolean } {
87
- try {
88
- const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as {
89
- scripts?: Record<string, string>
90
- }
91
- return { command: DEFAULT_CHECK, warn: pkg.scripts?.check === undefined }
92
- } catch {
93
- return { command: DEFAULT_CHECK, warn: true }
94
- }
95
- }
96
-
97
- function readTemplate(name: string): string {
98
- return readFileSync(fileURLToPath(new URL(`../../templates/${name}`, import.meta.url)), 'utf8')
99
- }
100
-
101
- function stamp(template: string, title: string, check: string): string {
102
- return template.split('{{TITLE}}').join(title).split('{{CHECK}}').join(check)
103
- }
@@ -1,15 +0,0 @@
1
- // `plumbbob status` — print the session state, or NO ACTIVE SESSION. Read-only,
2
- // always exits 0. Skills pre-inject this output to gate their own behavior.
3
-
4
- import { findRepoRoot } from '../lib/git.ts'
5
- import { hasSession, readState } from '../lib/sidecar.ts'
6
-
7
- export function status(cwd: string): number {
8
- const root = findRepoRoot(cwd)
9
- if (root === null || !hasSession(root)) {
10
- process.stdout.write('NO ACTIVE SESSION\n')
11
- return 0
12
- }
13
- process.stdout.write(`STATE: ${readState(root) ?? 'UNKNOWN'}\n`)
14
- return 0
15
- }
package/src/verbs/wrap.ts DELETED
@@ -1,34 +0,0 @@
1
- // `plumbbob wrap` (D19/D28) — the FINISH-entry verb. Sets STATE=FINISH so
2
- // /plumbbob-report can write report.md and /plumbbob-docs can touch docs/.
3
- // `finish` stays the closing gate; wrap just opens the one state where
4
- // documentation may be projected. A transition verb, so dispatch refuses it under
5
- // CLAUDECODE (D21).
6
-
7
- import { findRepoRoot } from '../lib/git.ts'
8
- import { hasSession, readState, writeState } from '../lib/sidecar.ts'
9
-
10
- export function wrap(cwd: string): number {
11
- const root = findRepoRoot(cwd)
12
- if (root === null || !hasSession(root)) {
13
- process.stderr.write('plumbbob: no active session. Run `plumbbob start "<title>"` first.\n')
14
- return 1
15
- }
16
- const state = readState(root)
17
- if (state === 'FINISH') {
18
- process.stdout.write('plumbbob: already in FINISH. Run `/plumbbob-report`, then `plumbbob finish` to close.\n')
19
- return 0
20
- }
21
- if (state !== 'DESIGN') {
22
- process.stderr.write(
23
- `plumbbob: wrap enters FINISH from DESIGN (current state is ${state ?? 'UNKNOWN'}). ` +
24
- 'Close the current step first — `done` from BUILD/REVIEW, or `spike done` from SPIKE.\n',
25
- )
26
- return 1
27
- }
28
- writeState(root, 'FINISH')
29
- process.stdout.write(
30
- 'plumbbob: STATE=FINISH. Now `/plumbbob-report` writes the report (and `/plumbbob-docs` may touch docs/); ' +
31
- 'then `plumbbob finish` archives and closes.\n',
32
- )
33
- return 0
34
- }