biotonomy 0.1.0 → 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.
package/README.md CHANGED
@@ -1,180 +1,207 @@
1
1
  # Biotonomy
2
2
 
3
- Biotonomy is a CLI that runs Codex loops (research→implement→review→fix) inside your repo.
4
- It enforces quality gates (lint/typecheck/test) between stages and records everything as files.
5
- PR automation is now first-class (`bt pr` / `bt ship`) and keeps deterministic artifacts for every run.
3
+ Biotonomy is a command-line workflow for shipping code changes with Codex.
6
4
 
7
- Biotonomy is intentionally file-based:
8
- - project config lives in `.bt.env`
9
- - ephemeral state lives in `.bt/`
10
- - feature state lives in `specs/<feature>/`
5
+ - What it is: A CLI that runs a repeatable flow: `spec -> research -> implement -> review -> fix -> pr`.
6
+ - Who it is for: Developers who want a structured way to ship small changes fast.
7
+ - What problem it solves: Keeps work organized in files, runs quality checks, and reduces "what do I do next?" during AI-assisted coding.
11
8
 
12
- ## The Codex Loop
9
+ ## 60-Second Quickstart
13
10
 
14
- For a feature folder `specs/<feature>/`, the loop is:
11
+ Install either way:
15
12
 
16
- 1. `bt spec <feature>`: create `SPEC.md` (or `bt spec <issue#>` to pull from GitHub via `gh`)
17
- 2. `bt research <feature>`: Codex writes `RESEARCH.md` (Codex required)
18
- 3. `bt implement <feature>`: Codex applies code changes + Biotonomy runs quality gates
19
- 4. `bt review <feature>`: Codex reviews into `REVIEW.md` (writes a stub if Codex is missing)
20
- 5. `bt fix <feature>`: Codex applies targeted fixes + Biotonomy runs quality gates
21
-
22
- Supporting commands:
23
- - `bt status`: summarize story status from `SPEC.md` plus latest `gates.json` (if present)
24
- - `bt gates [<feature>]`: run gates and write `gates.json` (feature-local or global)
25
- - `bt reset`: delete `.bt/` and `specs/**/.lock` (does not modify your git working tree)
13
+ ```bash
14
+ npm i -g biotonomy
15
+ # then use: bt ...
16
+ ```
26
17
 
27
- ## Ship Archie (Walkthrough)
18
+ ```bash
19
+ npx biotonomy ...
20
+ ```
28
21
 
29
- This is the intended "ship a feature" path. Some steps are still manual; each TODO points at the tracking issue.
22
+ Minimal demo in a fresh repo:
30
23
 
31
24
  ```bash
32
- # 0) Install
33
- npm install -g biotonomy
25
+ mkdir biotonomy-demo && cd biotonomy-demo
26
+ git init
27
+ npm init -y
28
+
29
+ npx biotonomy bootstrap
30
+ npx biotonomy spec hello-world
31
+ npx biotonomy review hello-world
32
+ npx biotonomy status
33
+ ```
34
34
 
35
- # 1) Initialize the repo for Biotonomy (creates .bt.env, specs/, .bt/)
36
- bt bootstrap
35
+ Expected files after the demo:
37
36
 
38
- # 2) Create a spec (local feature name)
39
- bt spec archie
37
+ - `.bt.env`
38
+ - `.bt/`
39
+ - `specs/hello-world/SPEC.md`
40
+ - `specs/hello-world/REVIEW.md`
41
+ - `specs/hello-world/progress.txt`
42
+ - `specs/hello-world/history/001-spec.md`
43
+ - `specs/hello-world/history/002-review.md`
44
+ - `specs/hello-world/.artifacts/codex-review.log`
40
45
 
41
- # (Optional) If "Archie" is a GitHub issue:
42
- # bt spec 123 # pulls issue title/body via `gh` into specs/issue-123/SPEC.md
46
+ Notes:
43
47
 
44
- # 3) Research (requires Codex; see Issue #3 for the end-to-end loop/demo harness)
45
- bt research archie
48
+ - `review` still creates `REVIEW.md` even if Codex is not installed.
49
+ - `research` requires Codex.
46
50
 
47
- # 4) Implement (runs gates; if Codex is unavailable this records history and still runs gates)
48
- bt implement archie
51
+ ## Ship A Small Change
49
52
 
50
- # 5) Review (writes specs/archie/REVIEW.md; stub output if Codex is unavailable)
51
- bt review archie
53
+ Use this for a real change from idea to PR.
52
54
 
53
- # 6) Fix until review is clean (gates are re-run)
54
- bt fix archie
55
+ 1. `spec` (define the change)
55
56
 
56
- # 7) Check progress at any time
57
- bt status
57
+ ```bash
58
+ bt spec my-change
59
+ # or from GitHub issue:
60
+ # bt spec 123
58
61
  ```
59
62
 
60
- Tracked work (open issues):
61
- - Core loop expansion (`bt loop` style driver and polish): [Issue #10](https://github.com/archive-dot-com/biotonomy/issues/10), [Issue #3](https://github.com/archive-dot-com/biotonomy/issues/3)
62
- - Release readiness and publish workflow (`npm i -g biotonomy`): [Issue #7](https://github.com/archive-dot-com/biotonomy/issues/7)
63
- - Docs refresh for Archie walkthrough and current commands: [Issue #8](https://github.com/archive-dot-com/biotonomy/issues/8), [Issue #1](https://github.com/archive-dot-com/biotonomy/issues/1)
64
- - Reliability hardening and bugfixes: [Issues #13-#17](https://github.com/archive-dot-com/biotonomy/issues)
63
+ - Automated today: creates `specs/<feature>/SPEC.md`, history, and progress logs.
64
+ - Manual today: fill in/clean up stories and acceptance criteria in `SPEC.md`.
65
65
 
66
- ## Artifacts And Layout
66
+ 2. `research` (gather context)
67
67
 
68
- Biotonomy is minimal bash plus prompt templates:
69
- - `bt.sh`: CLI entrypoint and router
70
- - `commands/*.sh`: command implementations
71
- - `lib/*.sh`: shared helpers (env loading, state paths, notifications, gates, Codex exec)
72
- - `prompts/*.md`: prompt templates for Codex stages
73
- - `hooks/*`: example notification hooks
68
+ ```bash
69
+ bt research my-change
70
+ ```
74
71
 
75
- On disk, expect:
76
- - `.bt.env`: project config (parsed as `KEY=VALUE` without `source` / without executing code)
77
- - `.bt/`: ephemeral state (locks/caches; safe to delete via `bt reset`)
78
- - `specs/<feature>/SPEC.md`: plan and story statuses (parseable; created by `bt spec`)
79
- - `specs/<feature>/RESEARCH.md`: research notes (`bt research`)
80
- - `specs/<feature>/REVIEW.md`: review verdict + findings (`bt review`)
81
- - `specs/<feature>/gates.json`: latest gate results when running `bt gates <feature>`
82
- - `specs/<feature>/progress.txt`: timestamped progress log
83
- - `specs/<feature>/history/*.md`: append-only run history for each stage
84
- - `specs/<feature>/.artifacts/*`: captured stderr from Codex/gh for debugging/repro
72
+ - Automated today: runs Codex in read-only mode and writes `RESEARCH.md`.
73
+ - Manual today: confirm research quality and adjust plan if needed.
85
74
 
86
- ## Quality Gates
75
+ 3. `implement` (make the change)
87
76
 
88
- Stages that run gates:
89
- - `bt implement <feature>`
90
- - `bt fix <feature>`
91
- - `bt gates [<feature>]`
77
+ ```bash
78
+ bt implement my-change
79
+ ```
80
+
81
+ - Automated today: runs Codex in full-auto and then runs quality gates.
82
+ - Manual today: if Codex is unavailable, this stage is a stub and you implement changes yourself.
92
83
 
93
- Gate configuration:
94
- - Override per-project in `.bt.env` via `BT_GATE_LINT`, `BT_GATE_TYPECHECK`, `BT_GATE_TEST`
95
- - If unset, Biotonomy tries simple auto-detection (npm/yarn/pnpm/Makefile)
84
+ 4. `review` (check what changed)
85
+
86
+ ```bash
87
+ bt review my-change
88
+ ```
96
89
 
97
- ## PR Automation (Opt-In Today)
90
+ - Automated today: writes `REVIEW.md` (with a fallback stub if Codex fails).
91
+ - Manual today: decide whether findings are acceptable for your team.
98
92
 
99
- There is currently a safe helper script (defaults to `--dry-run`) that uses `git` and `gh`:
93
+ 5. `fix` (address findings)
100
94
 
101
95
  ```bash
102
- npm run pr:open -- archie --dry-run
103
- npm run pr:open -- archie --run
96
+ bt fix my-change
104
97
  ```
105
98
 
106
- It determines the branch from `specs/<feature>/SPEC.md` frontmatter (`branch:`) when present, otherwise uses `feat/<feature>`.
107
- The planned end state is a first-class `bt pr ...` flow. Tracked in [Issue #8](https://github.com/archive-dot-com/biotonomy/issues/8).
99
+ - Automated today: runs Codex fix pass and re-runs quality gates.
100
+ - Manual today: rerun until you are satisfied; no built-in auto-loop to "done" yet.
101
+
102
+ 6. `pr` (open pull request)
103
+
104
+ ```bash
105
+ bt pr my-change --dry-run
106
+ bt pr my-change --run
107
+ ```
108
108
 
109
- ## Demos
109
+ - Automated today: runs tests/lint, creates branch, optionally commits, pushes, opens PR via `gh`.
110
+ - Manual today: choose reviewers/labels, final PR polish, and merge strategy.
110
111
 
111
- ### Issue #3 Real Loop (End-to-End, Deterministic)
112
+ ## Current Limitations
112
113
 
113
- This repo includes an end-to-end "real loop" runner that:
114
- - runs the actual `bt.sh` entrypoint
115
- - uses a deterministic workspace
116
- - stubs `gh` and `codex` so it is reproducible offline
117
- - writes a scrubbed transcript + snapshot under `specs/issue-3-real-loop/`
114
+ - No one-command autonomous loop yet (you run each stage yourself).
115
+ - `research` needs Codex installed and available.
116
+ - `implement`/`fix` can run as stubs without Codex (gates still run, code may not change).
117
+ - PR flow depends on `gh` and repository permissions.
118
+
119
+ ## Troubleshooting
120
+
121
+ `gh` auth fails (`bt spec 123` or `bt pr ...`):
118
122
 
119
123
  ```bash
120
- npm install
121
- npm run demo
124
+ gh auth status
125
+ gh auth login
126
+ ```
127
+
128
+ Codex missing (`codex required` or `codex not found`):
129
+
130
+ - Install Codex and make sure `codex` is on your `PATH`.
131
+ - Or set a custom binary in `.bt.env`: `BT_CODEX_BIN=/path/to/codex`.
122
132
 
123
- ls -R specs/issue-3-real-loop
124
- sed -n '1,120p' specs/issue-3-real-loop/transcript.txt
125
- sed -n '1,120p' specs/issue-3-real-loop/snapshot.txt
133
+ Quality gate failures on `implement`/`fix`:
134
+
135
+ ```bash
136
+ bt gates my-change
126
137
  ```
127
138
 
128
- ## Install
139
+ - Fix failing lint/typecheck/test commands.
140
+ - Override gate commands in `.bt.env` if auto-detection is wrong:
141
+ - `BT_GATE_LINT=...`
142
+ - `BT_GATE_TYPECHECK=...`
143
+ - `BT_GATE_TEST=...`
144
+
145
+ ## Release Publish Steps
146
+
147
+ Use this when preparing an npm release. This workflow validates readiness but does not publish automatically.
148
+
149
+ 1. Confirm clean main branch and pull latest changes.
129
150
 
130
- Global install:
151
+ ```bash
152
+ git checkout main
153
+ git pull --ff-only
154
+ ```
155
+
156
+ 2. Run release preflight checks (tests, lint, pack verification, and `npm pack --dry-run` summary).
131
157
 
132
158
  ```bash
133
- npm install -g biotonomy
134
- bt --help
159
+ npm run release:ready
135
160
  ```
136
161
 
137
- Local (repo) usage:
162
+ 3. Ensure npm authentication is ready for publish.
138
163
 
139
164
  ```bash
140
- npx biotonomy --help
165
+ npm whoami
166
+ # if needed:
167
+ npm login
141
168
  ```
142
169
 
143
- ## Targeting A Repo From Anywhere
170
+ 4. Bump version and create a tag.
144
171
 
145
- By default, `bt` uses your current working directory as the project root.
172
+ ```bash
173
+ npm version patch
174
+ # or: npm version minor / npm version major
175
+ ```
146
176
 
147
- To run against a different repo, use `--target <path>` (sets `BT_TARGET_DIR` for that invocation):
177
+ 5. Push commit and tag.
148
178
 
149
179
  ```bash
150
- bt --target /path/to/repo bootstrap
151
- bt --target /path/to/repo status
152
- bt --target /path/to/repo spec archie
180
+ git push --follow-tags
153
181
  ```
154
182
 
155
- You can also set `BT_TARGET_DIR` in the environment (same behavior):
183
+ 6. Publish manually.
156
184
 
157
185
  ```bash
158
- BT_TARGET_DIR=/path/to/repo bt status
186
+ npm publish --access public
159
187
  ```
160
188
 
161
- ## Status (v0.1.0)
189
+ - If your npm account uses 2FA for publish, npm will require a one-time code during `npm publish`.
162
190
 
163
- Implemented today:
164
- - File-based loop artifacts: `.bt.env`, `.bt/`, `specs/<feature>/...`
165
- - `bt bootstrap`, `bt spec <feature>`, `bt status`, `bt gates`, `bt reset`
166
- - `bt review` produces `REVIEW.md` even without Codex (stub output + required `Verdict:` line)
167
- - `bt implement` and `bt fix` always run quality gates and record history; if Codex is missing they behave as stubs (no code changes)
168
- - `bt research` requires Codex (it dies early if `codex` is not available)
169
- - Opt-in PR helper (`npm run pr:open`) with `--dry-run` by default
191
+ ## Commands
170
192
 
171
- In progress (tracked in open issues):
172
- - [Issue #10](https://github.com/archive-dot-com/biotonomy/issues/10): add a first-class loop driver (implement→review→fix until APPROVE + gates pass)
173
- - [Issue #3](https://github.com/archive-dot-com/biotonomy/issues/3): complete Codex-native loop ergonomics
174
- - [Issue #7](https://github.com/archive-dot-com/biotonomy/issues/7): publish workflow + npm readiness
175
- - [Issue #8](https://github.com/archive-dot-com/biotonomy/issues/8): docs rewrite around Codex loop + Archie walkthrough
176
- - [Issue #12](https://github.com/archive-dot-com/biotonomy/issues/12): close stale “already done” tracker issues
177
- - [Issues #13-#17](https://github.com/archive-dot-com/biotonomy/issues): active bugfix queue
193
+ ```bash
194
+ bt bootstrap
195
+ bt spec <feature|issue#>
196
+ bt research <feature>
197
+ bt implement <feature>
198
+ bt review <feature>
199
+ bt fix <feature>
200
+ bt gates [feature]
201
+ bt status
202
+ bt pr <feature> [--dry-run|--run]
203
+ bt reset
204
+ ```
178
205
 
179
206
  ## Development
180
207
 
@@ -182,5 +209,3 @@ In progress (tracked in open issues):
182
209
  npm test
183
210
  npm run lint
184
211
  ```
185
-
186
- Lint uses `shellcheck` if it is installed; otherwise it skips with a warning (CI installs shellcheck and runs strict).
package/bt.sh CHANGED
@@ -41,7 +41,7 @@ Usage:
41
41
  bt [--target <path>] <command> [args]
42
42
 
43
43
  Commands:
44
- bootstrap spec research implement review fix compound design status gates reset pr ship
44
+ bootstrap spec research implement review fix loop compound design status gates reset pr ship
45
45
 
46
46
  Global options:
47
47
  -h, --help Show help
@@ -99,7 +99,7 @@ bt_dispatch() {
99
99
  bt_env_load || true
100
100
 
101
101
  case "$cmd" in
102
- bootstrap|spec|research|implement|review|fix|compound|design|status|gates|reset|pr|ship) ;;
102
+ bootstrap|spec|research|plan-review|implement|review|fix|loop|compound|design|status|gates|reset|pr|ship) ;;
103
103
  *)
104
104
  bt_err "unknown command: $cmd"
105
105
  bt_usage >&2
@@ -119,11 +119,15 @@ bt_dispatch() {
119
119
  # shellcheck source=/dev/null
120
120
  source "$cmd_file"
121
121
 
122
- local fn="bt_cmd_$cmd"
122
+ local safe_cmd="${cmd//-/_}"
123
+ local fn="bt_cmd_$safe_cmd"
123
124
  if [[ "$cmd" == "ship" ]]; then
124
125
  fn="bt_cmd_pr"
125
126
  fi
126
127
 
128
+ # Log for debugging
129
+ # bt_info "dispatching to $fn"
130
+
127
131
  if ! declare -F "$fn" >/dev/null 2>&1; then
128
132
  bt_die "command function not found: $fn (in $cmd_file)"
129
133
  fi
@@ -15,6 +15,14 @@ EOF
15
15
  # Operate within the effective project root (BT_TARGET_DIR if set).
16
16
  local root="${BT_PROJECT_ROOT:-$PWD}"
17
17
 
18
+ local critical_dir
19
+ for critical_dir in "specs" ".bt" "hooks"; do
20
+ local critical_path="$root/$critical_dir"
21
+ if [[ -e "$critical_path" && ! -d "$critical_path" ]]; then
22
+ bt_die "critical scaffold path exists as a file: $critical_path"
23
+ fi
24
+ done
25
+
18
26
  local env_path="$root/.bt.env"
19
27
  if [[ -f "$env_path" ]]; then
20
28
  bt_info "found existing .bt.env"
package/commands/fix.sh CHANGED
@@ -35,9 +35,13 @@ EOF
35
35
  mkdir -p "$artifacts_dir"
36
36
  codex_logf="$artifacts_dir/codex-fix.log"
37
37
  : >"$codex_logf"
38
- if ! BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_full_auto "$BT_ROOT/prompts/fix.md"; then
38
+ if BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_full_auto "$BT_ROOT/prompts/fix.md"; then
39
+ codex_ec=0
40
+ else
39
41
  codex_ec=$?
40
42
  bt_warn "codex exited non-zero (fix): $codex_ec"
43
+ bt_die "codex failed (fix), stopping."
44
+ return 1
41
45
  fi
42
46
  else
43
47
  codex_ec=127
@@ -25,6 +25,18 @@ EOF
25
25
  dir="$(bt_feature_dir "$feature")"
26
26
  [[ -d "$dir" ]] || bt_die "missing feature dir: $dir (run: bt spec ...)"
27
27
 
28
+ local plan_review="$dir/PLAN_REVIEW.md"
29
+ if [[ ! -f "$plan_review" ]]; then
30
+ bt_err "missing $plan_review"
31
+ bt_err "run: bt plan-review $feature"
32
+ bt_die "implement hard-fails without approved PLAN_REVIEW verdict"
33
+ fi
34
+
35
+ if ! grep -qiE "Verdict:.*(APPROVE_PLAN|APPROVED_PLAN)" "$plan_review"; then
36
+ bt_err "PLAN_REVIEW.md exists but is not approved."
37
+ bt_die "implement hard-fails without approved PLAN_REVIEW verdict"
38
+ fi
39
+
28
40
  bt_progress_append "$feature" "implement: bt implement $feature (starting)"
29
41
 
30
42
  local codex_ec=0
@@ -35,9 +47,13 @@ EOF
35
47
  mkdir -p "$artifacts_dir"
36
48
  codex_logf="$artifacts_dir/codex-implement.log"
37
49
  : >"$codex_logf"
38
- if ! BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_full_auto "$BT_ROOT/prompts/implement.md"; then
50
+ if BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_full_auto "$BT_ROOT/prompts/implement.md"; then
51
+ codex_ec=0
52
+ else
39
53
  codex_ec=$?
40
54
  bt_warn "codex exited non-zero (implement): $codex_ec"
55
+ bt_die "codex failed (implement), stopping."
56
+ return 1
41
57
  fi
42
58
  else
43
59
  codex_ec=127
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # biotonomy loop command
5
+ # Implement -> Review -> Fix loop until review is APPROVED and gates pass
6
+
7
+ # shellcheck source=/dev/null
8
+ source "$BT_ROOT/lib/state.sh"
9
+
10
+ bt_loop_usage() {
11
+ cat <<EOF
12
+ Usage: bt loop <feature> [options]
13
+
14
+ Repeatedly runs implement/review/fix until review returns Verdict: APPROVED
15
+ and quality gates pass.
16
+
17
+ Options:
18
+ --max-iterations <n> Maximum number of retry loops (default: 5)
19
+ -h, --help Show this help
20
+ EOF
21
+ }
22
+
23
+ bt_cmd_loop() {
24
+ local max_iter=5
25
+ local feature=""
26
+
27
+ while [[ $# -gt 0 ]]; do
28
+ case "${1:-}" in
29
+ -h|--help) bt_loop_usage; return 0 ;;
30
+ --max-iterations)
31
+ if [[ $# -lt 2 || -z "${2:-}" || "${2:-}" == -* ]]; then
32
+ bt_err "--max-iterations requires a value"
33
+ return 2
34
+ fi
35
+ max_iter="$2"
36
+ shift 2
37
+ ;;
38
+ -*)
39
+ bt_err "unknown flag: $1"
40
+ return 2
41
+ ;;
42
+ *)
43
+ feature="$1"
44
+ shift
45
+ ;;
46
+ esac
47
+ done
48
+
49
+ if [[ -z "$feature" ]]; then
50
+ bt_err "feature name is required"
51
+ return 2
52
+ fi
53
+
54
+ if ! [[ "$max_iter" =~ ^[1-9][0-9]*$ ]]; then
55
+ bt_err "--max-iterations must be a positive integer"
56
+ return 2
57
+ fi
58
+
59
+ bt_env_load || true
60
+ bt_ensure_dirs
61
+
62
+ bt_info "starting loop for: $feature (max iterations: $max_iter)"
63
+
64
+ bt_info "running preflight gates..."
65
+ if ! bt_run_gates; then
66
+ bt_err "preflight gates failed; aborting before implement/review"
67
+ return 1
68
+ fi
69
+ bt_info "preflight gates: PASS"
70
+
71
+ local feat_dir
72
+ feat_dir="$(bt_feature_dir "$feature")"
73
+ local plan_review="$feat_dir/PLAN_REVIEW.md"
74
+ if [[ ! -f "$plan_review" ]] || ! grep -qiE "Verdict:.*(APPROVE_PLAN|APPROVED_PLAN)" "$plan_review"; then
75
+ bt_err "missing or unapproved $plan_review"
76
+ bt_err "run: bt plan-review $feature"
77
+ bt_die "loop hard-fails without approved PLAN_REVIEW verdict before implement/review"
78
+ fi
79
+
80
+ # Source required commands so we can call them directly
81
+ # shellcheck source=/dev/null
82
+ source "$BT_ROOT/commands/implement.sh"
83
+ # shellcheck source=/dev/null
84
+ source "$BT_ROOT/commands/review.sh"
85
+ # shellcheck source=/dev/null
86
+ source "$BT_ROOT/commands/fix.sh"
87
+
88
+ local iter=0
89
+ local review_file
90
+ local feat_dir
91
+ feat_dir="$(bt_feature_dir "$feature")"
92
+ review_file="$feat_dir/REVIEW.md"
93
+ local history_dir="$feat_dir/history"
94
+ local progress_file="$feat_dir/loop-progress.json"
95
+
96
+ # Initialize progress
97
+ mkdir -p "$history_dir"
98
+ cat > "$progress_file" <<EOF
99
+ {
100
+ "feature": "$feature",
101
+ "maxIterations": $max_iter,
102
+ "completedIterations": 0,
103
+ "result": "in-progress",
104
+ "iterations": []
105
+ }
106
+ EOF
107
+
108
+ # Ensure subcommands return non-zero instead of exiting the entire loop process.
109
+ export BT_DIE_MODE="return"
110
+
111
+ while [[ "$iter" -lt "$max_iter" ]]; do
112
+ iter=$((iter + 1))
113
+ bt_info "--- Iteration $iter / $max_iter ---"
114
+
115
+ # 1. Run Implement on every iteration
116
+ # Note: bt_cmd_implement already runs gates internally.
117
+ bt_info "running implement..."
118
+ if bt_cmd_implement "$feature"; then
119
+ :
120
+ else
121
+ bt_err "implement failed on iter $iter; aborting loop"
122
+ python3 - <<PY
123
+ import json
124
+ p = "$progress_file"
125
+ with open(p, 'r') as f:
126
+ d = json.load(f)
127
+ d['completedIterations'] = $iter
128
+ d['result'] = 'implement-failed'
129
+ d.setdefault('iterations', []).append({
130
+ "iteration": $iter,
131
+ "implementStatus": "FAIL",
132
+ "reviewStatus": "SKIP",
133
+ "fixStatus": "SKIP",
134
+ "verdict": "",
135
+ "gates": "FAIL",
136
+ "historyFile": ""
137
+ })
138
+ with open(p, 'w') as f:
139
+ json.dump(d, f, indent=2)
140
+ PY
141
+ return 1
142
+ fi
143
+
144
+ # 2. Run Review
145
+ bt_info "running review..."
146
+ if bt_cmd_review "$feature"; then
147
+ :
148
+ else
149
+ bt_err "review process failed"
150
+ return 1
151
+ fi
152
+
153
+ # 3. Check Verdict
154
+ if [[ ! -f "$review_file" ]]; then
155
+ bt_err "REVIEW.md missing after review command"
156
+ return 1
157
+ fi
158
+
159
+ local verdict
160
+ verdict="$(awk '/^Verdict:/{print toupper($2); exit}' "$review_file" | tr -d '\r' || true)"
161
+ bt_info "verdict: $verdict"
162
+
163
+ # 4. Final Gate Check for convergence
164
+ # We use the gates result from the last command that ran them (implement/fix).
165
+ # bt_cmd_implement/fix return non-zero if gates fail, but we capture the status.
166
+ # We call bt_run_gates here just to be sure of the final state post-review.
167
+ local gates_ok=1
168
+ if ! bt_run_gates; then
169
+ gates_ok=0
170
+ bt_info "gates: FAIL"
171
+ else
172
+ bt_info "gates: PASS"
173
+ fi
174
+
175
+ local fix_status="SKIP"
176
+ if [[ "$verdict" == "NEEDS_CHANGES" ]]; then
177
+ bt_info "running fix..."
178
+ # Note: bt_cmd_fix already runs gates internally.
179
+ if bt_cmd_fix "$feature"; then
180
+ fix_status="PASS"
181
+ else
182
+ fix_status="FAIL"
183
+ bt_err "fix failed after iter $iter review; aborting loop"
184
+ return 1
185
+ fi
186
+ fi
187
+
188
+ # Record iteration
189
+ local ts
190
+ ts="$(date +%Y-%m-%dT%H%M%S%z)"
191
+ local iter_padded
192
+ iter_padded="$(printf "%03d" "$iter")"
193
+ local hist_file="$history_dir/$ts-loop-iter-$iter_padded.md"
194
+ cp "$review_file" "$hist_file"
195
+
196
+ # Update progress.json
197
+ python3 - <<PY
198
+ import json, sys
199
+ p = "$progress_file"
200
+ with open(p, 'r') as f:
201
+ data = json.load(f)
202
+ data['completedIterations'] = $iter
203
+ data['iterations'].append({
204
+ "iteration": $iter,
205
+ "implementStatus": "PASS",
206
+ "reviewStatus": "PASS",
207
+ "fixStatus": "$fix_status",
208
+ "verdict": "$verdict",
209
+ "gates": "PASS" if "$gates_ok" == "1" else "FAIL",
210
+ "historyFile": "$hist_file"
211
+ })
212
+ with open(p, 'w') as f:
213
+ json.dump(data, f, indent=2)
214
+ PY
215
+
216
+ if [[ ( "$verdict" == "APPROVE" || "$verdict" == "APPROVED" ) && "$gates_ok" == "1" ]]; then
217
+ python3 -c "import json; p='$progress_file'; d=json.load(open(p)); d['result']='success'; json.dump(d, open(p, 'w'), indent=2)"
218
+ bt_info "Loop successful! Verdict $verdict and Gates PASS."
219
+ return 0
220
+ fi
221
+
222
+ bt_info "Verdict is $verdict (or gates failed); looping..."
223
+ done
224
+
225
+ python3 -c "import json; p='$progress_file'; d=json.load(open(p)); d['result']='max-iterations-exceeded'; json.dump(d, open(p, 'w'), indent=2)"
226
+ bt_err "Loop reached max iterations ($max_iter) without approval."
227
+ return 1
228
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # shellcheck source=/dev/null
5
+ source "$BT_ROOT/lib/state.sh"
6
+
7
+ bt_cmd_plan_review() {
8
+ # Feature name might start with - (e.g. bt plan-review -h)
9
+ # but shell parsing might be tricky.
10
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
11
+ cat <<'EOF'
12
+ Usage:
13
+ bt plan-review <feature>
14
+
15
+ Runs Codex in full-auto using prompts/plan-review.md to approve or reject the SPEC/RESEARCH plan.
16
+ EOF
17
+ return 0
18
+ fi
19
+
20
+ bt_env_load || true
21
+ bt_ensure_dirs
22
+
23
+ local feature
24
+ feature="$(bt_require_feature "${1:-}")"
25
+
26
+ local dir
27
+ dir="$(bt_feature_dir "$feature")"
28
+ [[ -d "$dir" ]] || bt_die "missing feature dir: $dir (run: bt spec ...)"
29
+
30
+ local out="$dir/PLAN_REVIEW.md"
31
+
32
+ if bt_codex_available; then
33
+ bt_info "running codex (full-auto) using prompts/plan-review.md"
34
+ if BT_FEATURE="$feature" BT_CODEX_LOG_FILE="/tmp/codex.log" bt_codex_exec_full_auto "$BT_ROOT/prompts/plan-review.md"; then
35
+ :
36
+ else
37
+ bt_die "codex failed (plan-review)"
38
+ fi
39
+ else
40
+ # v0.1.0 stub
41
+ cat <<'EOF' > "$out"
42
+ # Plan Review: Stub Approved
43
+
44
+ Verdict: APPROVED_PLAN
45
+ EOF
46
+ bt_info "wrote $out"
47
+ fi
48
+
49
+ if [[ ! -f "$out" ]]; then
50
+ bt_die "bt_cmd_plan_review failed: $out was not created"
51
+ fi
52
+
53
+ bt_history_write "$feature" "plan-review" "$(cat "$out")"
54
+ bt_notify "bt plan-review complete for $feature"
55
+ }
package/commands/pr.sh CHANGED
@@ -129,8 +129,22 @@ bt_cmd_pr() {
129
129
  --run) run_mode="run"; shift ;;
130
130
  --dry-run) run_mode="dry-run"; shift ;;
131
131
  --draft) draft=1; shift ;;
132
- --base) base="${2:-}"; shift 2 ;;
133
- --remote) remote="${2:-}"; shift 2 ;;
132
+ --base)
133
+ if [[ $# -lt 2 || "${2:-}" == -* ]]; then
134
+ bt_err "--base requires a value"
135
+ return 2
136
+ fi
137
+ base="${2:-}"
138
+ shift 2
139
+ ;;
140
+ --remote)
141
+ if [[ $# -lt 2 || "${2:-}" == -* ]]; then
142
+ bt_err "--remote requires a value"
143
+ return 2
144
+ fi
145
+ remote="${2:-}"
146
+ shift 2
147
+ ;;
134
148
  --no-commit) commit=0; shift ;;
135
149
  -*)
136
150
  bt_err "unknown flag: $1"
@@ -155,13 +169,7 @@ bt_cmd_pr() {
155
169
 
156
170
  bt_info "shipping feature: $feature"
157
171
 
158
- # 1. Run Tests & Lint
159
- bt_info "running tests..."
160
- npm test
161
- bt_info "running lint..."
162
- npm run lint
163
-
164
- # 2. Determine branch and metadata from SPEC.md
172
+ # 1. Determine branch and metadata from SPEC.md
165
173
  local specs_dir="${BT_SPECS_DIR:-specs}"
166
174
  local spec_file="$specs_dir/$feature/SPEC.md"
167
175
  local branch="feat/$feature"
@@ -180,7 +188,62 @@ bt_cmd_pr() {
180
188
  [[ -n "${i:-}" ]] && issue="$i"
181
189
  fi
182
190
 
183
- # 3. Create branch if needed
191
+ # 2. Fail-loud preflight for unstaged expected files (before tests/commit flow).
192
+ if [[ "$commit" == "1" ]]; then
193
+ local unstaged=""
194
+ local check_paths=(tests lib commands scripts specs prompts) # Added specs and prompts
195
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
196
+ unstaged="$(git ls-files --others --modified --exclude-standard -- "${check_paths[@]}" 2>/dev/null || true)"
197
+ else
198
+ # Outside a git repo, treat present implementation files as unstaged by definition.
199
+ unstaged="$(find "${check_paths[@]}" -type f 2>/dev/null | LC_ALL=C sort || true)"
200
+ fi
201
+ if [[ -n "$unstaged" ]]; then
202
+ bt_err "Found unstaged files that might be required for this feature:"
203
+ printf '%s\n' "$unstaged" >&2
204
+ bt_die "Abort: ship requires all feature files to be staged. Use git add and try again."
205
+ fi
206
+ fi
207
+
208
+ if [[ "$run_mode" == "dry-run" ]]; then
209
+ if [[ -z "$base" ]]; then
210
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
211
+ local ref
212
+ ref="$(git symbolic-ref -q "refs/remotes/$remote/HEAD" 2>/dev/null || true)"
213
+ if [[ -n "$ref" ]]; then
214
+ base="${ref##*/}"
215
+ else
216
+ base="main"
217
+ fi
218
+ else
219
+ base="main"
220
+ fi
221
+ fi
222
+
223
+ local title="feat: $feature"
224
+ local body="Feature: $feature"
225
+ [[ -n "$repo" && -n "$issue" ]] && body+=$'\n'"Issue: https://github.com/$repo/issues/$issue"
226
+ [[ -f "$spec_file" ]] && body+=$'\n'"Spec: $spec_file"
227
+
228
+ bt_info "[dry-run] git push -u $remote $branch"
229
+ bt_info "[dry-run] gh pr create --head $branch --base $base --title $title --body $body"
230
+ local artifacts_preview
231
+ artifacts_preview="$(mktemp "${TMPDIR:-/tmp}/bt-pr-artifacts-preview.XXXXXX")"
232
+ bt_pr_write_artifacts_comment "$feature" "$specs_dir" "$artifacts_preview"
233
+ bt_info "[dry-run] Artifacts comment would contain:"
234
+ cat "$artifacts_preview"
235
+ rm -f "$artifacts_preview"
236
+ bt_info "ship complete for $feature"
237
+ return 0
238
+ fi
239
+
240
+ # 3. Run tests & lint (run mode only)
241
+ bt_info "running tests..."
242
+ npm test
243
+ bt_info "running lint..."
244
+ npm run lint
245
+
246
+ # 4. Create branch if needed
184
247
  if git show-ref --verify --quiet "refs/heads/$branch"; then
185
248
  bt_info "branch $branch already exists, checking it out..."
186
249
  git checkout "$branch"
@@ -192,17 +255,13 @@ bt_cmd_pr() {
192
255
  # 4. Commit changes if requested
193
256
  if [[ "$commit" == "1" ]]; then
194
257
  bt_info "committing changes..."
195
- # Add SPEC.md and any tests/implementations related to this feature
196
- # We use explicit paths to avoid staging unrelated items
197
- local paths_to_add=()
198
- [[ -d "$specs_dir/$feature" ]] && paths_to_add+=("$specs_dir/$feature")
199
- [[ -d "tests" ]] && paths_to_add+=("tests")
200
- [[ -d "lib" ]] && paths_to_add+=("lib")
201
- [[ -d "commands" ]] && paths_to_add+=("commands")
202
- [[ -d "scripts" ]] && paths_to_add+=("scripts")
203
-
204
- if [[ ${#paths_to_add[@]} -gt 0 ]]; then
205
- git add "${paths_to_add[@]}"
258
+ local unstaged
259
+ local check_paths=(tests lib commands scripts specs prompts)
260
+ unstaged="$(git status --porcelain -- "${check_paths[@]}" 2>/dev/null || true)"
261
+ if [[ -n "$unstaged" ]]; then
262
+ bt_err "Found unstaged files that might be required for this feature:"
263
+ printf '%s\n' "$unstaged" >&2
264
+ bt_die "Abort: ship requires all feature files to be staged. Use git add and try again."
206
265
  fi
207
266
 
208
267
  if ! git diff --cached --quiet; then
@@ -259,6 +318,12 @@ bt_cmd_pr() {
259
318
  fi
260
319
  else
261
320
  bt_info "[dry-run] gh ${pr_args[*]}"
321
+ local artifacts_preview
322
+ artifacts_preview="$(mktemp "${TMPDIR:-/tmp}/bt-pr-artifacts-preview.XXXXXX")"
323
+ bt_pr_write_artifacts_comment "$feature" "$specs_dir" "$artifacts_preview"
324
+ bt_info "[dry-run] Artifacts comment would contain:"
325
+ cat "$artifacts_preview"
326
+ rm -f "$artifacts_preview"
262
327
  fi
263
328
 
264
329
  bt_info "ship complete for $feature"
@@ -32,9 +32,13 @@ EOF
32
32
  codex_logf="$artifacts_dir/codex-review.log"
33
33
  : >"$codex_logf"
34
34
  local codex_ec=0
35
- if ! BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_read_only "$BT_ROOT/prompts/review.md" "$out"; then
35
+ if BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_read_only "$BT_ROOT/prompts/review.md" "$out"; then
36
+ codex_ec=0
37
+ else
36
38
  codex_ec=$?
37
39
  bt_warn "codex exited non-zero (review): $codex_ec"
40
+ bt_die "codex failed (review), stopping."
41
+ return 1
38
42
  fi
39
43
 
40
44
  if [[ ! -f "$out" ]]; then
package/commands/spec.sh CHANGED
@@ -39,24 +39,48 @@ NODE
39
39
  }
40
40
 
41
41
  bt_cmd_spec() {
42
- if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
43
- cat <<'EOF'
42
+ local force=0
43
+ local arg=""
44
+ while [[ $# -gt 0 ]]; do
45
+ case "${1:-}" in
46
+ -h|--help)
47
+ cat <<'EOF'
44
48
  Usage:
45
- bt spec <issue#>
46
- bt spec <feature>
49
+ bt spec [--force] <issue#>
50
+ bt spec [--force] <feature>
51
+
52
+ Options:
53
+ --force Overwrite existing SPEC.md if it already exists.
47
54
 
48
55
  For an <issue#>, requires `gh` and creates `specs/issue-<n>/SPEC.md` using the issue title/body.
49
56
  For a <feature>, creates `specs/<feature>/SPEC.md` with a minimal, parseable story list.
50
57
  EOF
51
- return 0
58
+ return 0
59
+ ;;
60
+ --force)
61
+ force=1
62
+ shift
63
+ ;;
64
+ --*)
65
+ bt_die "unknown option for spec: $1"
66
+ ;;
67
+ *)
68
+ if [[ -n "$arg" ]]; then
69
+ bt_die "spec accepts exactly one <issue#> or <feature>"
70
+ fi
71
+ arg="$1"
72
+ shift
73
+ ;;
74
+ esac
75
+ done
76
+
77
+ if [[ -z "$arg" ]]; then
78
+ bt_die "spec requires <issue#> or <feature>"
52
79
  fi
53
80
 
54
81
  bt_env_load || true
55
82
  bt_ensure_dirs
56
83
 
57
- local arg="${1:-}"
58
- [[ -n "$arg" ]] || bt_die "spec requires <issue#> or <feature>"
59
-
60
84
  local feature issue
61
85
  issue=""
62
86
  if [[ "$arg" =~ ^[0-9]+$ ]]; then
@@ -72,8 +96,12 @@ EOF
72
96
 
73
97
  local spec="$dir/SPEC.md"
74
98
  if [[ -f "$spec" ]]; then
75
- bt_info "SPEC already exists: $spec"
76
- return 0
99
+ if (( force == 1 )); then
100
+ bt_info "overwriting existing SPEC: $spec"
101
+ else
102
+ bt_info "SPEC already exists: $spec"
103
+ return 0
104
+ fi
77
105
  fi
78
106
 
79
107
  if [[ -n "$issue" ]]; then
@@ -5,14 +5,14 @@ bt__count_statuses() {
5
5
  local spec="$1"
6
6
  awk '
7
7
  BEGIN { pending=0; in_progress=0; done=0; failed=0; blocked=0; total=0 }
8
- /^\- \*\*status:\*\*/ {
8
+ /^[[:space:]]*- \*\*status:\*\*/ {
9
9
  s=$0
10
- sub(/.*\*\*status:\*\* /,"",s)
10
+ sub(/^[[:space:]]*- \*\*status:\*\*[[:space:]]*/,"",s)
11
11
  gsub(/[[:space:]]+/,"",s)
12
12
  total++
13
13
  if (s=="pending") pending++
14
14
  else if (s=="in_progress") in_progress++
15
- else if (s=="done") done++
15
+ else if (s=="done" || s=="completed") done++
16
16
  else if (s=="failed") failed++
17
17
  else if (s=="blocked") blocked++
18
18
  }
@@ -33,12 +33,12 @@ bt__show_gates() {
33
33
  json="$(cat "$json_file")"
34
34
 
35
35
  local ts
36
- ts=$(printf '%s' "$json" | grep -oE '"ts": "[^"]+"' | cut -d'"' -f4 || echo "unknown")
36
+ ts=$(printf '%s' "$json" | grep -oE '"ts"[[:space:]]*:[[:space:]]*"[^"]+"' | cut -d'"' -f4 || echo "unknown")
37
37
 
38
38
  # A simple heuristic to check if any status is non-zero
39
- # We look for "status": N where N > 0
39
+ # We look for "status": N where N > 0 (whitespace-insensitive)
40
40
  local fails
41
- fails=$(printf '%s' "$json" | grep -oE '"status": [1-9][0-9]*' | wc -l | xargs)
41
+ fails=$(printf '%s' "$json" | grep -oE '"status"[[:space:]]*:[[:space:]]*[1-9][0-9]*' | wc -l | xargs)
42
42
 
43
43
  local status="pass"
44
44
  [[ "$fails" -gt 0 ]] && status="fail"
@@ -52,7 +52,7 @@ bt__show_gates() {
52
52
  local k
53
53
  # This regex is a bit fragile but works for the predictable format we write
54
54
  for k in "lint" "typecheck" "test"; do
55
- if echo "$json" | grep -qE "\"$k\": \{[^\}]*\"status\": [1-9][0-9]*"; then
55
+ if echo "$json" | grep -qE "\"$k\"[[:space:]]*:[[:space:]]*\{[^\}]*\"status\"[[:space:]]*:[[:space:]]*[1-9][0-9]*"; then
56
56
  detail="${detail}${k} "
57
57
  fi
58
58
  done
package/lib/codex.sh CHANGED
@@ -19,7 +19,8 @@ bt_codex_exec_full_auto() {
19
19
  bin="$(bt_codex_bin)"
20
20
  local log_file
21
21
  log_file="${BT_CODEX_LOG_FILE:-/dev/null}"
22
- "$bin" exec --full-auto -C "$BT_PROJECT_ROOT" "$(cat "$prompt_file")" > >(tee -a "$log_file") 2>&1
22
+ "$bin" exec --full-auto -C "$BT_PROJECT_ROOT" "$(cat "$prompt_file")" 2>&1 | tee -a "$log_file"
23
+ return "${PIPESTATUS[0]}"
23
24
  }
24
25
 
25
26
  bt_codex_exec_read_only() {
@@ -34,6 +35,5 @@ bt_codex_exec_read_only() {
34
35
  bin="$(bt_codex_bin)"
35
36
  local log_file
36
37
  log_file="${BT_CODEX_LOG_FILE:-/dev/null}"
37
- "$bin" exec -s read-only -C "$BT_PROJECT_ROOT" -o "$out_file" "$(cat "$prompt_file")" > >(tee -a "$log_file") 2>&1
38
+ "$bin" exec -s read-only -C "$BT_PROJECT_ROOT" -o "$out_file" "$(cat "$prompt_file")" 2>&1 | tee -a "$log_file"
38
39
  }
39
-
package/lib/env.sh CHANGED
@@ -68,12 +68,12 @@ bt_env_load() {
68
68
  env_file="$(bt_realpath "$env_file")"
69
69
  bt_env_load_file "$env_file" || bt_die "failed to load BT_ENV_FILE=$env_file"
70
70
  else
71
- # Prefer project config from the caller's current working directory.
72
- if [[ -f "$PWD/.bt.env" ]]; then
73
- bt_env_load_file "$PWD/.bt.env" || bt_die "failed to load env: $PWD/.bt.env"
74
- # If running with a target repo, fall back to that repo's .bt.env.
75
- elif [[ -n "${BT_TARGET_DIR:-}" && -f "$BT_TARGET_DIR/.bt.env" ]]; then
71
+ # When targeting another repo, its env must win over caller CWD config.
72
+ if [[ -n "${BT_TARGET_DIR:-}" && -f "$BT_TARGET_DIR/.bt.env" ]]; then
76
73
  bt_env_load_file "$BT_TARGET_DIR/.bt.env" || bt_die "failed to load env: $BT_TARGET_DIR/.bt.env"
74
+ # Otherwise use caller project config.
75
+ elif [[ -f "$PWD/.bt.env" ]]; then
76
+ bt_env_load_file "$PWD/.bt.env" || bt_die "failed to load env: $PWD/.bt.env"
77
77
  fi
78
78
  fi
79
79
 
package/lib/gates.sh CHANGED
@@ -33,6 +33,18 @@ bt__gate_cmd() {
33
33
  esac
34
34
  }
35
35
 
36
+ bt__json_escape() {
37
+ local s="${1-}"
38
+ s="${s//\\/\\\\}"
39
+ s="${s//\"/\\\"}"
40
+ s="${s//$'\n'/\\n}"
41
+ s="${s//$'\r'/\\r}"
42
+ s="${s//$'\t'/\\t}"
43
+ s="${s//$'\b'/\\b}"
44
+ s="${s//$'\f'/\\f}"
45
+ printf '%s' "$s"
46
+ }
47
+
36
48
  # Returns gate config (key=cmd) for those available.
37
49
  bt_get_gate_config() {
38
50
  local detected
@@ -89,8 +101,10 @@ bt_run_gates() {
89
101
  overall_ok=1
90
102
  fi
91
103
 
92
- local entry
93
- printf -v entry '"%s": {"cmd": "%s", "status": %d}' "$k" "$v" "$status"
104
+ local entry k_json v_json
105
+ k_json="$(bt__json_escape "$k")"
106
+ v_json="$(bt__json_escape "$v")"
107
+ printf -v entry '"%s": {"cmd": "%s", "status": %d}' "$k_json" "$v_json" "$status"
94
108
  if [[ -z "$results_json" ]]; then
95
109
  results_json="$entry"
96
110
  else
package/lib/log.sh CHANGED
@@ -43,6 +43,9 @@ bt_debug() {
43
43
 
44
44
  bt_die() {
45
45
  bt_err "$*"
46
+ if [[ "${BT_DIE_MODE:-exit}" == "return" ]]; then
47
+ return 1
48
+ fi
46
49
  exit 1
47
50
  }
48
51
 
package/lib/path.sh CHANGED
@@ -11,11 +11,42 @@ bt_realpath() {
11
11
  python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$p" 2>/dev/null && return 0
12
12
  fi
13
13
 
14
- # Best-effort fallback: not fully resolving .. or symlinks.
14
+ # Pure-bash lexical fallback when neither `realpath` nor `python3` is available.
15
+ # Resolves relative paths and normalizes "."/".." segments without requiring
16
+ # filesystem existence checks.
17
+ local abs
15
18
  case "$p" in
16
- /*) printf '%s\n' "$p" ;;
17
- *) printf '%s/%s\n' "$(pwd)" "$p" ;;
19
+ /*) abs="$p" ;;
20
+ *) abs="$(pwd)/$p" ;;
18
21
  esac
22
+
23
+ local IFS='/'
24
+ local -a parts stack
25
+ read -r -a parts <<< "$abs"
26
+
27
+ local seg
28
+ for seg in "${parts[@]}"; do
29
+ [[ -z "$seg" || "$seg" == "." ]] && continue
30
+ if [[ "$seg" == ".." ]]; then
31
+ if ((${#stack[@]} > 0)); then
32
+ unset 'stack[${#stack[@]}-1]'
33
+ fi
34
+ continue
35
+ fi
36
+ stack+=("$seg")
37
+ done
38
+
39
+ if ((${#stack[@]} == 0)); then
40
+ printf '/\n'
41
+ return 0
42
+ fi
43
+
44
+ printf '/%s' "${stack[0]}"
45
+ local i
46
+ for ((i = 1; i < ${#stack[@]}; i++)); do
47
+ printf '/%s' "${stack[$i]}"
48
+ done
49
+ printf '\n'
19
50
  }
20
51
 
21
52
  bt_find_up() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biotonomy",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Codex-native autonomous development loop CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,8 @@
31
31
  "demo": "bash scripts/demo-issue-3-real-loop.sh",
32
32
  "pr:open": "bash scripts/gh-pr.sh",
33
33
  "verify:pack": "node scripts/verify-pack.mjs",
34
- "prepublishOnly": "npm test && npm run lint && npm run verify:pack"
34
+ "release:ready": "node scripts/release-readiness.mjs",
35
+ "prepublishOnly": "npm run release:ready"
35
36
  },
36
37
  "engines": {
37
38
  "node": ">=18"
@@ -0,0 +1,6 @@
1
+ # Plan Review Loop
2
+
3
+ Review the generated SPEC.md and RESEARCH.md (if exists) for the feature.
4
+ Ensure the plan is sound and safe.
5
+
6
+ Output Verdict: APPROVED_PLAN or Verdict: NEEDS_CHANGES.
@@ -0,0 +1,78 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ function run(label, cmd, args, opts = {}) {
5
+ process.stderr.write(`\n==> ${label}\n`);
6
+ const res = spawnSync(cmd, args, {
7
+ encoding: "utf8",
8
+ stdio: opts.stdio || "inherit",
9
+ });
10
+ return {
11
+ ok: (res.status ?? 1) === 0,
12
+ status: res.status ?? 1,
13
+ stdout: res.stdout || "",
14
+ stderr: res.stderr || "",
15
+ };
16
+ }
17
+
18
+ function parsePackJson(stdout) {
19
+ try {
20
+ const data = JSON.parse(stdout);
21
+ return data?.[0] || null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function formatBytes(n) {
28
+ if (!Number.isFinite(n) || n < 0) return "n/a";
29
+ if (n < 1024) return `${n} B`;
30
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
31
+ return `${(n / (1024 * 1024)).toFixed(2)} MiB`;
32
+ }
33
+
34
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
35
+
36
+ const test = run("npm test", "npm", ["test"]);
37
+ const lint = run("npm run lint", "npm", ["run", "lint"]);
38
+ const verifyPack = run("npm run verify:pack", "npm", ["run", "verify:pack"]);
39
+
40
+ const packDryRun = run("npm pack --dry-run --json", "npm", ["pack", "--dry-run", "--json"], {
41
+ stdio: "pipe",
42
+ });
43
+ if (packDryRun.stdout) process.stdout.write(packDryRun.stdout);
44
+ if (packDryRun.stderr) process.stderr.write(packDryRun.stderr);
45
+
46
+ const packInfo = packDryRun.ok ? parsePackJson(packDryRun.stdout) : null;
47
+ const npmWhoami = run("npm whoami (optional)", "npm", ["whoami"], { stdio: "pipe" });
48
+ const npmUser = npmWhoami.ok ? npmWhoami.stdout.trim() : "not authenticated";
49
+
50
+ const checks = [
51
+ ["tests", test.ok],
52
+ ["lint", lint.ok],
53
+ ["pack contents", verifyPack.ok],
54
+ ["npm pack --dry-run", packDryRun.ok],
55
+ ];
56
+ const allOk = checks.every(([, ok]) => ok);
57
+
58
+ process.stderr.write("\nPublish preflight summary\n");
59
+ process.stderr.write(`- package: ${pkg.name}@${pkg.version}\n`);
60
+ process.stderr.write(`- npm auth: ${npmUser}\n`);
61
+ for (const [name, ok] of checks) {
62
+ process.stderr.write(`- ${name}: ${ok ? "PASS" : "FAIL"}\n`);
63
+ }
64
+
65
+ if (packInfo) {
66
+ process.stderr.write("- pack artifact:\n");
67
+ process.stderr.write(` - file: ${packInfo.filename || "n/a"}\n`);
68
+ process.stderr.write(` - files: ${packInfo.files?.length ?? "n/a"}\n`);
69
+ process.stderr.write(` - package size: ${formatBytes(packInfo.size)}\n`);
70
+ process.stderr.write(` - unpacked size: ${formatBytes(packInfo.unpackedSize)}\n`);
71
+ }
72
+
73
+ if (!allOk) {
74
+ process.stderr.write("\nRelease readiness failed. Fix failures before publishing.\n");
75
+ process.exit(1);
76
+ }
77
+
78
+ process.stderr.write("\nRelease readiness passed. Safe to proceed with manual publish steps.\n");