biotonomy 0.1.0 → 0.2.2
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 +63 -151
- package/bt +12 -1
- 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 +5 -3
- package/prompts/plan-review.md +6 -0
- package/scripts/release-readiness.mjs +78 -0
package/README.md
CHANGED
|
@@ -1,186 +1,98 @@
|
|
|
1
1
|
# Biotonomy
|
|
2
2
|
|
|
3
|
-
Biotonomy is a CLI
|
|
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 (`bt`) is a CLI for running a Codex-driven development workflow in a repo:
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
- project config lives in `.bt.env`
|
|
9
|
-
- ephemeral state lives in `.bt/`
|
|
10
|
-
- feature state lives in `specs/<feature>/`
|
|
5
|
+
`spec -> research -> plan-review -> implement -> review -> fix -> pr`
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
It supports both:
|
|
8
|
+
- manual stage-by-stage execution
|
|
9
|
+
- a one-command iterative loop with `bt loop`
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
## Quickstart
|
|
15
12
|
|
|
16
|
-
|
|
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)
|
|
26
|
-
|
|
27
|
-
## Ship Archie (Walkthrough)
|
|
28
|
-
|
|
29
|
-
This is the intended "ship a feature" path. Some steps are still manual; each TODO points at the tracking issue.
|
|
13
|
+
Prereqs: Node.js >= 18, `git`, Codex CLI available as `codex` (or set `BT_CODEX_BIN`).
|
|
30
14
|
|
|
31
15
|
```bash
|
|
32
|
-
#
|
|
33
|
-
npm
|
|
16
|
+
# Install
|
|
17
|
+
npm i -g biotonomy
|
|
34
18
|
|
|
35
|
-
#
|
|
19
|
+
# In your project repo
|
|
36
20
|
bt bootstrap
|
|
37
21
|
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# (Optional) If "Archie" is a GitHub issue:
|
|
42
|
-
# bt spec 123 # pulls issue title/body via `gh` into specs/issue-123/SPEC.md
|
|
43
|
-
|
|
44
|
-
# 3) Research (requires Codex; see Issue #3 for the end-to-end loop/demo harness)
|
|
45
|
-
bt research archie
|
|
46
|
-
|
|
47
|
-
# 4) Implement (runs gates; if Codex is unavailable this records history and still runs gates)
|
|
48
|
-
bt implement archie
|
|
49
|
-
|
|
50
|
-
# 5) Review (writes specs/archie/REVIEW.md; stub output if Codex is unavailable)
|
|
51
|
-
bt review archie
|
|
52
|
-
|
|
53
|
-
# 6) Fix until review is clean (gates are re-run)
|
|
54
|
-
bt fix archie
|
|
55
|
-
|
|
56
|
-
# 7) Check progress at any time
|
|
57
|
-
bt status
|
|
58
|
-
```
|
|
59
|
-
|
|
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)
|
|
65
|
-
|
|
66
|
-
## Artifacts And Layout
|
|
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
|
|
74
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
## Quality Gates
|
|
87
|
-
|
|
88
|
-
Stages that run gates:
|
|
89
|
-
- `bt implement <feature>`
|
|
90
|
-
- `bt fix <feature>`
|
|
91
|
-
- `bt gates [<feature>]`
|
|
92
|
-
|
|
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)
|
|
22
|
+
# Create a feature scaffold
|
|
23
|
+
FEATURE=hello-world
|
|
24
|
+
bt spec "$FEATURE"
|
|
96
25
|
|
|
97
|
-
|
|
26
|
+
# Loop requires an approved plan review verdict first
|
|
27
|
+
cat > "specs/$FEATURE/PLAN_REVIEW.md" <<'MD'
|
|
28
|
+
Verdict: APPROVED_PLAN
|
|
29
|
+
MD
|
|
98
30
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
npm run pr:open -- archie --dry-run
|
|
103
|
-
npm run pr:open -- archie --run
|
|
31
|
+
# Run autonomous implement/review/fix iterations (with gates)
|
|
32
|
+
bt loop "$FEATURE" --max-iterations 3
|
|
104
33
|
```
|
|
105
34
|
|
|
106
|
-
|
|
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).
|
|
35
|
+
## `bt loop`
|
|
108
36
|
|
|
109
|
-
|
|
37
|
+
`bt loop <feature> [--max-iterations N]` runs:
|
|
38
|
+
1. preflight quality gates
|
|
39
|
+
2. `implement`
|
|
40
|
+
3. `review`
|
|
41
|
+
4. `fix` only when review verdict is `NEEDS_CHANGES`
|
|
42
|
+
5. repeat until verdict is `APPROVE`/`APPROVED` and gates pass, or max iterations is reached
|
|
110
43
|
|
|
111
|
-
|
|
44
|
+
Loop hard-requires an approved `specs/<feature>/PLAN_REVIEW.md` verdict (`APPROVE_PLAN` or `APPROVED_PLAN`).
|
|
112
45
|
|
|
113
|
-
|
|
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/`
|
|
46
|
+
## Artifacts And State
|
|
118
47
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
48
|
+
Biotonomy writes feature state under `specs/<feature>/`:
|
|
49
|
+
- `SPEC.md`
|
|
50
|
+
- `RESEARCH.md`
|
|
51
|
+
- `PLAN_REVIEW.md`
|
|
52
|
+
- `REVIEW.md`
|
|
53
|
+
- `history/` stage snapshots (`###-<stage>.md`) and loop iteration snapshots (`*-loop-iter-###.md`)
|
|
54
|
+
- `loop-progress.json` loop summary and per-iteration status
|
|
55
|
+
- `progress.txt` append-only stage log
|
|
56
|
+
- `.artifacts/` Codex logs and command artifacts (for example `codex-implement.log`, `codex-review.log`, `codex-fix.log`)
|
|
57
|
+
- `gates.json` feature gate results when running `bt gates <feature>`
|
|
127
58
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Global install:
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
npm install -g biotonomy
|
|
134
|
-
bt --help
|
|
135
|
-
```
|
|
59
|
+
Global gate state is written to `.bt/state/gates.json` when running `bt gates` without a feature.
|
|
136
60
|
|
|
137
|
-
|
|
61
|
+
## Manual Commands
|
|
138
62
|
|
|
139
63
|
```bash
|
|
140
|
-
|
|
64
|
+
bt bootstrap
|
|
65
|
+
bt spec <feature|issue#>
|
|
66
|
+
bt research <feature>
|
|
67
|
+
bt plan-review <feature>
|
|
68
|
+
bt implement <feature>
|
|
69
|
+
bt review <feature>
|
|
70
|
+
bt fix <feature>
|
|
71
|
+
bt loop <feature> [--max-iterations N]
|
|
72
|
+
bt gates [feature]
|
|
73
|
+
bt status
|
|
74
|
+
bt pr <feature> [--run]
|
|
141
75
|
```
|
|
142
76
|
|
|
143
|
-
##
|
|
144
|
-
|
|
145
|
-
By default, `bt` uses your current working directory as the project root.
|
|
77
|
+
## Configuration
|
|
146
78
|
|
|
147
|
-
|
|
79
|
+
Project config lives in `.bt.env` (created by `bt bootstrap`). Common overrides:
|
|
148
80
|
|
|
149
81
|
```bash
|
|
150
|
-
|
|
151
|
-
bt
|
|
152
|
-
|
|
82
|
+
BT_SPECS_DIR=specs
|
|
83
|
+
BT_STATE_DIR=.bt
|
|
84
|
+
BT_GATE_LINT="npm run lint"
|
|
85
|
+
BT_GATE_TYPECHECK="tsc --noEmit"
|
|
86
|
+
BT_GATE_TEST="npm test"
|
|
87
|
+
BT_CODEX_BIN="/path/to/codex"
|
|
153
88
|
```
|
|
154
89
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
BT_TARGET_DIR=/path/to/repo bt status
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Status (v0.1.0)
|
|
162
|
-
|
|
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
|
|
170
|
-
|
|
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
|
|
90
|
+
## Release
|
|
178
91
|
|
|
179
|
-
|
|
92
|
+
Run the release readiness checks:
|
|
180
93
|
|
|
181
94
|
```bash
|
|
182
|
-
npm
|
|
183
|
-
npm run lint
|
|
95
|
+
npm run release:ready
|
|
184
96
|
```
|
|
185
97
|
|
|
186
|
-
|
|
98
|
+
That script runs tests, lint, pack verification, and `npm pack --dry-run`.
|
package/bt
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
bt_script_dir() {
|
|
5
|
+
local src="${BASH_SOURCE[0]}"
|
|
6
|
+
while [ -h "$src" ]; do
|
|
7
|
+
local dir
|
|
8
|
+
dir="$(cd -P "$(dirname "$src")" && pwd)"
|
|
9
|
+
src="$(readlink "$src")"
|
|
10
|
+
[[ "$src" != /* ]] && src="$dir/$src"
|
|
11
|
+
done
|
|
12
|
+
cd -P "$(dirname "$src")" && pwd
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(bt_script_dir)"
|
|
5
16
|
exec "$SCRIPT_DIR/bt.sh" "$@"
|
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.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Codex-native autonomous development loop CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/archive-dot-com/biotonomy#readme",
|
|
14
14
|
"bin": {
|
|
15
|
-
"bt": "bt"
|
|
15
|
+
"bt": "bt",
|
|
16
|
+
"biotonomy": "bt"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"bt",
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
"demo": "bash scripts/demo-issue-3-real-loop.sh",
|
|
32
33
|
"pr:open": "bash scripts/gh-pr.sh",
|
|
33
34
|
"verify:pack": "node scripts/verify-pack.mjs",
|
|
34
|
-
"
|
|
35
|
+
"release:ready": "node scripts/release-readiness.mjs",
|
|
36
|
+
"prepublishOnly": "npm run release:ready"
|
|
35
37
|
},
|
|
36
38
|
"engines": {
|
|
37
39
|
"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");
|