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 +143 -118
- package/bt.sh +7 -3
- package/commands/bootstrap.sh +8 -0
- package/commands/fix.sh +5 -1
- package/commands/implement.sh +17 -1
- package/commands/loop.sh +228 -0
- package/commands/plan-review.sh +55 -0
- package/commands/pr.sh +86 -21
- package/commands/review.sh +5 -1
- package/commands/spec.sh +38 -10
- package/commands/status.sh +7 -7
- package/lib/codex.sh +3 -3
- package/lib/env.sh +5 -5
- package/lib/gates.sh +16 -2
- package/lib/log.sh +3 -0
- package/lib/path.sh +34 -3
- package/package.json +3 -2
- package/prompts/plan-review.md +6 -0
- package/scripts/release-readiness.mjs +78 -0
package/README.md
CHANGED
|
@@ -1,180 +1,207 @@
|
|
|
1
1
|
# Biotonomy
|
|
2
2
|
|
|
3
|
-
Biotonomy is a
|
|
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
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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
|
-
##
|
|
9
|
+
## 60-Second Quickstart
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
Install either way:
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
18
|
+
```bash
|
|
19
|
+
npx biotonomy ...
|
|
20
|
+
```
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
Minimal demo in a fresh repo:
|
|
30
23
|
|
|
31
24
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
bt bootstrap
|
|
35
|
+
Expected files after the demo:
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
bt
|
|
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
|
-
|
|
42
|
-
# bt spec 123 # pulls issue title/body via `gh` into specs/issue-123/SPEC.md
|
|
46
|
+
Notes:
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
- `review` still creates `REVIEW.md` even if Codex is not installed.
|
|
49
|
+
- `research` requires Codex.
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
bt implement archie
|
|
51
|
+
## Ship A Small Change
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
bt review archie
|
|
53
|
+
Use this for a real change from idea to PR.
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
bt fix archie
|
|
55
|
+
1. `spec` (define the change)
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
bt
|
|
57
|
+
```bash
|
|
58
|
+
bt spec my-change
|
|
59
|
+
# or from GitHub issue:
|
|
60
|
+
# bt spec 123
|
|
58
61
|
```
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
-
|
|
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
|
-
|
|
66
|
+
2. `research` (gather context)
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
-
|
|
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
|
-
|
|
75
|
+
3. `implement` (make the change)
|
|
87
76
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
4. `review` (check what changed)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bt review my-change
|
|
88
|
+
```
|
|
96
89
|
|
|
97
|
-
|
|
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
|
-
|
|
93
|
+
5. `fix` (address findings)
|
|
100
94
|
|
|
101
95
|
```bash
|
|
102
|
-
|
|
103
|
-
npm run pr:open -- archie --run
|
|
96
|
+
bt fix my-change
|
|
104
97
|
```
|
|
105
98
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
+
## Current Limitations
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
Quality gate failures on `implement`/`fix`:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bt gates my-change
|
|
126
137
|
```
|
|
127
138
|
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
bt --help
|
|
159
|
+
npm run release:ready
|
|
135
160
|
```
|
|
136
161
|
|
|
137
|
-
|
|
162
|
+
3. Ensure npm authentication is ready for publish.
|
|
138
163
|
|
|
139
164
|
```bash
|
|
140
|
-
|
|
165
|
+
npm whoami
|
|
166
|
+
# if needed:
|
|
167
|
+
npm login
|
|
141
168
|
```
|
|
142
169
|
|
|
143
|
-
|
|
170
|
+
4. Bump version and create a tag.
|
|
144
171
|
|
|
145
|
-
|
|
172
|
+
```bash
|
|
173
|
+
npm version patch
|
|
174
|
+
# or: npm version minor / npm version major
|
|
175
|
+
```
|
|
146
176
|
|
|
147
|
-
|
|
177
|
+
5. Push commit and tag.
|
|
148
178
|
|
|
149
179
|
```bash
|
|
150
|
-
|
|
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
|
-
|
|
183
|
+
6. Publish manually.
|
|
156
184
|
|
|
157
185
|
```bash
|
|
158
|
-
|
|
186
|
+
npm publish --access public
|
|
159
187
|
```
|
|
160
188
|
|
|
161
|
-
|
|
189
|
+
- If your npm account uses 2FA for publish, npm will require a one-time code during `npm publish`.
|
|
162
190
|
|
|
163
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
package/commands/bootstrap.sh
CHANGED
|
@@ -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
|
|
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
|
package/commands/implement.sh
CHANGED
|
@@ -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
|
|
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
|
package/commands/loop.sh
ADDED
|
@@ -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)
|
|
133
|
-
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
[[ -
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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"
|
package/commands/review.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
package/commands/status.sh
CHANGED
|
@@ -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
|
-
|
|
8
|
+
/^[[:space:]]*- \*\*status:\*\*/ {
|
|
9
9
|
s=$0
|
|
10
|
-
sub(
|
|
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":
|
|
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":
|
|
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\":
|
|
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")"
|
|
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")"
|
|
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
|
-
#
|
|
72
|
-
if [[ -f "$
|
|
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
|
-
|
|
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
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
|
-
#
|
|
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
|
-
/*)
|
|
17
|
-
*)
|
|
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
|
|
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
|
-
"
|
|
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,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");
|