biotonomy 0.2.4 → 0.2.7
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/bt.sh +31 -3
- package/commands/loop.sh +26 -23
- package/commands/plan-review.sh +13 -19
- package/commands/pr.sh +1 -0
- package/commands/spec.sh +13 -3
- package/commands/status.sh +1 -1
- package/lib/codex.sh +10 -1
- package/lib/env.sh +8 -0
- package/lib/gates.sh +51 -3
- package/lib/state.sh +19 -0
- package/package.json +1 -1
package/bt.sh
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
bt_package_version() {
|
|
5
|
+
local pkg="$BT_ROOT/package.json"
|
|
6
|
+
if [[ -f "$pkg" ]]; then
|
|
7
|
+
awk -F '"' '/"version"[[:space:]]*:/ { print $4; exit }' "$pkg"
|
|
8
|
+
return 0
|
|
9
|
+
fi
|
|
10
|
+
return 1
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
bt_script_dir() {
|
|
7
14
|
local src="${BASH_SOURCE[0]}"
|
|
@@ -15,6 +22,8 @@ bt_script_dir() {
|
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
BT_ROOT="$(bt_script_dir)"
|
|
25
|
+
BT_VERSION="$(bt_package_version 2>/dev/null || true)"
|
|
26
|
+
BT_VERSION="${BT_VERSION:-0.1.0}"
|
|
18
27
|
|
|
19
28
|
# shellcheck source=/dev/null
|
|
20
29
|
source "$BT_ROOT/lib/log.sh"
|
|
@@ -41,7 +50,7 @@ Usage:
|
|
|
41
50
|
bt [--target <path>] <command> [args]
|
|
42
51
|
|
|
43
52
|
Commands:
|
|
44
|
-
bootstrap spec research implement review fix loop compound design status gates reset pr ship
|
|
53
|
+
bootstrap spec research plan-review implement review fix loop compound design status gates reset pr ship
|
|
45
54
|
|
|
46
55
|
Global options:
|
|
47
56
|
-h, --help Show help
|
|
@@ -93,7 +102,26 @@ bt_dispatch() {
|
|
|
93
102
|
shift || true
|
|
94
103
|
|
|
95
104
|
case "$cmd" in
|
|
96
|
-
-h|--help|help)
|
|
105
|
+
-h|--help|help)
|
|
106
|
+
if [[ $# -gt 0 ]]; then
|
|
107
|
+
# bt help <cmd>
|
|
108
|
+
cmd="$1"
|
|
109
|
+
shift
|
|
110
|
+
local cmd_file="$BT_ROOT/commands/$cmd.sh"
|
|
111
|
+
if [[ "$cmd" == "ship" ]]; then cmd_file="$BT_ROOT/commands/pr.sh"; fi
|
|
112
|
+
if [[ -f "$cmd_file" ]]; then
|
|
113
|
+
# shellcheck source=/dev/null
|
|
114
|
+
source "$cmd_file"
|
|
115
|
+
local safe_cmd="${cmd//-/_}"
|
|
116
|
+
local fn="bt_${safe_cmd}_usage"
|
|
117
|
+
if [[ "$cmd" == "ship" ]]; then fn="bt_pr_usage"; fi
|
|
118
|
+
if declare -F "$fn" >/dev/null 2>&1; then
|
|
119
|
+
"$fn"
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
bt_usage; return 0 ;;
|
|
97
125
|
esac
|
|
98
126
|
|
|
99
127
|
bt_env_load || true
|
package/commands/loop.sh
CHANGED
|
@@ -56,28 +56,39 @@ bt_cmd_loop() {
|
|
|
56
56
|
return 2
|
|
57
57
|
fi
|
|
58
58
|
|
|
59
|
+
feature="$(bt_require_feature "$feature")"
|
|
60
|
+
|
|
59
61
|
bt_env_load || true
|
|
60
62
|
bt_ensure_dirs
|
|
61
63
|
|
|
62
64
|
local feat_dir
|
|
63
65
|
feat_dir="$(bt_feature_dir "$feature")"
|
|
66
|
+
|
|
67
|
+
# Auto-run plan-review if PLAN_REVIEW.md is missing or unapproved
|
|
64
68
|
local plan_review="$feat_dir/PLAN_REVIEW.md"
|
|
65
69
|
if [[ ! -f "$plan_review" ]] || ! grep -qiE "Verdict:.*(APPROVE_PLAN|APPROVED_PLAN)" "$plan_review"; then
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
bt_info "PLAN_REVIEW missing or unapproved; auto-running plan-review..."
|
|
71
|
+
# shellcheck source=/dev/null
|
|
72
|
+
source "$BT_ROOT/commands/plan-review.sh"
|
|
73
|
+
if ! bt_cmd_plan_review "$feature"; then
|
|
74
|
+
bt_err "auto plan-review failed for $feature"
|
|
75
|
+
bt_err "manually run: bt plan-review $feature"
|
|
76
|
+
bt_die "loop hard-fails without approved PLAN_REVIEW verdict before implement/review"
|
|
77
|
+
fi
|
|
78
|
+
# Re-check after auto-run
|
|
79
|
+
if [[ ! -f "$plan_review" ]] || ! grep -qiE "Verdict:.*(APPROVE_PLAN|APPROVED_PLAN)" "$plan_review"; then
|
|
80
|
+
bt_err "plan-review ran but did not produce an approved verdict"
|
|
81
|
+
bt_err "check $plan_review and re-run: bt plan-review $feature"
|
|
82
|
+
bt_die "loop hard-fails without approved PLAN_REVIEW verdict before implement/review"
|
|
83
|
+
fi
|
|
84
|
+
bt_info "plan-review auto-approved; continuing loop"
|
|
69
85
|
fi
|
|
70
86
|
|
|
71
87
|
bt_info "starting loop for: $feature (max iterations: $max_iter)"
|
|
72
88
|
|
|
73
|
-
local -a gate_args=()
|
|
74
|
-
if [[ "${BT_LOOP_REQUIRE_GATES:-0}" == "1" ]]; then
|
|
75
|
-
gate_args=(--require-any)
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
89
|
bt_info "running preflight gates..."
|
|
79
|
-
if
|
|
80
|
-
if ! bt_run_gates
|
|
90
|
+
if [[ "${BT_LOOP_REQUIRE_GATES:-0}" == "1" ]]; then
|
|
91
|
+
if ! bt_run_gates --require-any; then
|
|
81
92
|
bt_err "preflight gates failed (or none configured); aborting before implement/review"
|
|
82
93
|
return 1
|
|
83
94
|
fi
|
|
@@ -164,6 +175,7 @@ EOF
|
|
|
164
175
|
# Ensure subcommands return non-zero instead of exiting the entire loop process.
|
|
165
176
|
export BT_DIE_MODE="return"
|
|
166
177
|
|
|
178
|
+
|
|
167
179
|
while [[ "$iter" -lt "$max_iter" ]]; do
|
|
168
180
|
iter=$((iter + 1))
|
|
169
181
|
bt_info "--- Iteration $iter / $max_iter ---"
|
|
@@ -221,20 +233,11 @@ PY
|
|
|
221
233
|
# bt_cmd_implement/fix return non-zero if gates fail, but we capture the status.
|
|
222
234
|
# We call bt_run_gates here just to be sure of the final state post-review.
|
|
223
235
|
local gates_ok=1
|
|
224
|
-
if
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
bt_info "gates: FAIL"
|
|
228
|
-
else
|
|
229
|
-
bt_info "gates: PASS"
|
|
230
|
-
fi
|
|
236
|
+
if ! bt_run_gates --require-any; then
|
|
237
|
+
gates_ok=0
|
|
238
|
+
bt_info "gates: FAIL"
|
|
231
239
|
else
|
|
232
|
-
|
|
233
|
-
gates_ok=0
|
|
234
|
-
bt_info "gates: FAIL"
|
|
235
|
-
else
|
|
236
|
-
bt_info "gates: PASS"
|
|
237
|
-
fi
|
|
240
|
+
bt_info "gates: PASS"
|
|
238
241
|
fi
|
|
239
242
|
|
|
240
243
|
local fix_status="SKIP"
|
package/commands/plan-review.sh
CHANGED
|
@@ -29,26 +29,20 @@ EOF
|
|
|
29
29
|
|
|
30
30
|
local out="$dir/PLAN_REVIEW.md"
|
|
31
31
|
|
|
32
|
-
if bt_codex_available; then
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
artifacts_dir="$dir/.artifacts"
|
|
36
|
-
mkdir -p "$artifacts_dir"
|
|
37
|
-
codex_logf="$artifacts_dir/codex-plan-review.log"
|
|
38
|
-
: >"$codex_logf"
|
|
39
|
-
if BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_full_auto "$BT_ROOT/prompts/plan-review.md"; then
|
|
40
|
-
:
|
|
41
|
-
else
|
|
42
|
-
bt_die "codex failed (plan-review)"
|
|
43
|
-
fi
|
|
44
|
-
else
|
|
45
|
-
# v0.1.0 stub
|
|
46
|
-
cat <<'EOF' > "$out"
|
|
47
|
-
# Plan Review: Stub Approved
|
|
32
|
+
if ! bt_codex_available; then
|
|
33
|
+
bt_die "codex required for plan-review (set BT_CODEX_BIN to a valid codex executable or install codex)"
|
|
34
|
+
fi
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
bt_info "running codex (full-auto) using prompts/plan-review.md"
|
|
37
|
+
local artifacts_dir codex_logf
|
|
38
|
+
artifacts_dir="$dir/.artifacts"
|
|
39
|
+
mkdir -p "$artifacts_dir"
|
|
40
|
+
codex_logf="$artifacts_dir/codex-plan-review.log"
|
|
41
|
+
: >"$codex_logf"
|
|
42
|
+
if BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_full_auto "$BT_ROOT/prompts/plan-review.md" "$out"; then
|
|
43
|
+
:
|
|
44
|
+
else
|
|
45
|
+
bt_die "codex failed (plan-review)"
|
|
52
46
|
fi
|
|
53
47
|
|
|
54
48
|
if [[ ! -f "$out" ]]; then
|
package/commands/pr.sh
CHANGED
package/commands/spec.sh
CHANGED
|
@@ -159,13 +159,19 @@ EOF
|
|
|
159
159
|
bt_env_load || true
|
|
160
160
|
bt_ensure_dirs
|
|
161
161
|
|
|
162
|
-
local feature issue
|
|
162
|
+
local feature issue url_slug
|
|
163
163
|
issue=""
|
|
164
|
+
url_slug=""
|
|
164
165
|
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
|
165
166
|
issue="$arg"
|
|
166
167
|
feature="issue-$arg"
|
|
168
|
+
elif [[ "$arg" =~ ^https?://github.com/([^/]+/[^/]+)/issues/([0-9]+) ]]; then
|
|
169
|
+
# support URL format
|
|
170
|
+
issue="${BASH_REMATCH[2]}"
|
|
171
|
+
url_slug="${BASH_REMATCH[1]}"
|
|
172
|
+
feature="issue-$issue"
|
|
167
173
|
else
|
|
168
|
-
feature="$arg"
|
|
174
|
+
feature="$(bt_require_feature "$arg")"
|
|
169
175
|
fi
|
|
170
176
|
|
|
171
177
|
local dir
|
|
@@ -186,7 +192,11 @@ EOF
|
|
|
186
192
|
bt__require_cmd gh
|
|
187
193
|
|
|
188
194
|
local repo
|
|
189
|
-
|
|
195
|
+
if [[ -n "$url_slug" ]]; then
|
|
196
|
+
repo="$url_slug"
|
|
197
|
+
else
|
|
198
|
+
repo="$(bt_repo_resolve "$BT_PROJECT_ROOT")"
|
|
199
|
+
fi
|
|
190
200
|
|
|
191
201
|
local -a gh_cmd
|
|
192
202
|
gh_cmd=(gh issue view "$issue" -R "$repo" --json "title,body,url")
|
package/commands/status.sh
CHANGED
package/lib/codex.sh
CHANGED
|
@@ -11,6 +11,7 @@ bt_codex_available() {
|
|
|
11
11
|
|
|
12
12
|
bt_codex_exec_full_auto() {
|
|
13
13
|
local prompt_file="$1"
|
|
14
|
+
local out_file="${2:-}"
|
|
14
15
|
if ! bt_codex_available; then
|
|
15
16
|
bt_warn "codex not found; skipping (set BT_CODEX_BIN or install codex)"
|
|
16
17
|
return 0
|
|
@@ -19,7 +20,15 @@ bt_codex_exec_full_auto() {
|
|
|
19
20
|
bin="$(bt_codex_bin)"
|
|
20
21
|
local log_file
|
|
21
22
|
log_file="${BT_CODEX_LOG_FILE:-/dev/null}"
|
|
22
|
-
|
|
23
|
+
|
|
24
|
+
local -a args
|
|
25
|
+
args=(exec --full-auto -C "$BT_PROJECT_ROOT")
|
|
26
|
+
if [[ -n "$out_file" ]]; then
|
|
27
|
+
args+=(-o "$out_file")
|
|
28
|
+
fi
|
|
29
|
+
args+=("$(cat "$prompt_file")")
|
|
30
|
+
|
|
31
|
+
"$bin" "${args[@]}" 2>&1 | tee -a "$log_file"
|
|
23
32
|
return "${PIPESTATUS[0]}"
|
|
24
33
|
}
|
|
25
34
|
|
package/lib/env.sh
CHANGED
|
@@ -6,6 +6,14 @@ bt__export_kv() {
|
|
|
6
6
|
local val="$2"
|
|
7
7
|
|
|
8
8
|
[[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || return 0
|
|
9
|
+
|
|
10
|
+
# Respect process env: only set if not already present in environment.
|
|
11
|
+
# This ensures BT_GATE_TEST='echo hi' bt gates works even when .bt.env
|
|
12
|
+
# contains an empty BT_GATE_TEST= line. (#38)
|
|
13
|
+
if [[ -n "${!key+x}" ]]; then
|
|
14
|
+
return 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
9
17
|
# Export without eval; keep value literal even if it contains spaces/symbols.
|
|
10
18
|
export "$key=$val"
|
|
11
19
|
}
|
package/lib/gates.sh
CHANGED
|
@@ -76,6 +76,33 @@ bt_get_gate_config() {
|
|
|
76
76
|
|
|
77
77
|
# Runs gates and returns a JSON string fragment with results.
|
|
78
78
|
# Writes logs to stderr. Returns 0 if all gates passed, 1 otherwise.
|
|
79
|
+
bt__gate_script_exists() {
|
|
80
|
+
# Check if an npm/pnpm/yarn script actually exists in package.json.
|
|
81
|
+
# Returns 0 if the script exists or if we can't determine (non-npm project).
|
|
82
|
+
local cmd="$1"
|
|
83
|
+
local script_name=""
|
|
84
|
+
|
|
85
|
+
# Extract script name from common patterns
|
|
86
|
+
case "$cmd" in
|
|
87
|
+
"npm run "*) script_name="${cmd#npm run }" ;;
|
|
88
|
+
"npm test") script_name="test" ;;
|
|
89
|
+
"pnpm "*) script_name="${cmd#pnpm }" ;;
|
|
90
|
+
"yarn "*) script_name="${cmd#yarn }" ;;
|
|
91
|
+
*) return 0 ;; # Not an npm-style command, assume exists
|
|
92
|
+
esac
|
|
93
|
+
script_name="${script_name%% *}" # Take first word only
|
|
94
|
+
|
|
95
|
+
local pkg="$BT_PROJECT_ROOT/package.json"
|
|
96
|
+
[[ -f "$pkg" ]] || return 0 # No package.json, can't check
|
|
97
|
+
|
|
98
|
+
# Check if script exists in package.json
|
|
99
|
+
if command -v node >/dev/null 2>&1; then
|
|
100
|
+
node -e "const p=require('$pkg'); process.exit(p.scripts && p.scripts['$script_name'] ? 0 : 1)" 2>/dev/null
|
|
101
|
+
return $?
|
|
102
|
+
fi
|
|
103
|
+
return 0 # Can't verify, assume exists
|
|
104
|
+
}
|
|
105
|
+
|
|
79
106
|
bt_run_gates() {
|
|
80
107
|
local require_any=0
|
|
81
108
|
if [[ "${1:-}" == "--require-any" ]]; then
|
|
@@ -93,14 +120,35 @@ bt_run_gates() {
|
|
|
93
120
|
|
|
94
121
|
while IFS= read -r line; do
|
|
95
122
|
[[ -n "$line" ]] || continue
|
|
96
|
-
any=1
|
|
97
123
|
k="${line%%=*}"
|
|
98
124
|
v="${line#*=}"
|
|
99
125
|
|
|
126
|
+
# Skip gates explicitly set to "skip" or "" (disabled)
|
|
127
|
+
if [[ "$v" == "skip" || -z "$v" ]]; then
|
|
128
|
+
bt_warn "gate disabled: $k"
|
|
129
|
+
local entry k_json v_json
|
|
130
|
+
k_json="$(bt__json_escape "$k")"
|
|
131
|
+
v_json="$(bt__json_escape "$v")"
|
|
132
|
+
printf -v entry '"%s": {"cmd": "%s", "status": -1, "skipped": true}' "$k_json" "$v_json"
|
|
133
|
+
if [[ -z "$results_json" ]]; then results_json="$entry"; else results_json="$results_json, $entry"; fi
|
|
134
|
+
continue
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Skip gates whose scripts don't exist
|
|
138
|
+
if ! bt__gate_script_exists "$v"; then
|
|
139
|
+
bt_warn "gate skipped (script missing): $k ($v)"
|
|
140
|
+
local entry k_json v_json
|
|
141
|
+
k_json="$(bt__json_escape "$k")"
|
|
142
|
+
v_json="$(bt__json_escape "$v")"
|
|
143
|
+
printf -v entry '"%s": {"cmd": "%s", "status": -1, "skipped": true}' "$k_json" "$v_json"
|
|
144
|
+
if [[ -z "$results_json" ]]; then results_json="$entry"; else results_json="$results_json, $entry"; fi
|
|
145
|
+
continue
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
any=1
|
|
149
|
+
|
|
100
150
|
bt_info "gate: $k ($v)"
|
|
101
151
|
local status=0
|
|
102
|
-
# Use bash -lc for interactivity if needed, but we typically want it non-interactive.
|
|
103
|
-
# The original used bash -lc "$v". We'll stick to that but capture status.
|
|
104
152
|
if ! (cd "$BT_PROJECT_ROOT" && bash -lc "$v"); then
|
|
105
153
|
bt_err "gate failed: $k"
|
|
106
154
|
status=1
|
package/lib/state.sh
CHANGED
|
@@ -14,10 +14,29 @@ bt_ensure_dirs() {
|
|
|
14
14
|
mkdir -p "$(bt_specs_path)" "$BT_PROJECT_ROOT/$BT_STATE_DIR"
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
bt_sanitize_feature() {
|
|
18
|
+
local s="${1:-}"
|
|
19
|
+
# Replace characters not in [A-Za-z0-9._-] with underscore
|
|
20
|
+
s="${s//[^A-Za-z0-9._-]/_}"
|
|
21
|
+
# Ensure it doesn't start with a dot or dash and isn't empty
|
|
22
|
+
if [[ "$s" =~ ^[._-] ]]; then
|
|
23
|
+
s="f$s"
|
|
24
|
+
fi
|
|
25
|
+
if [[ -z "$s" ]]; then
|
|
26
|
+
s="feature"
|
|
27
|
+
fi
|
|
28
|
+
printf '%s\n' "$s"
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
bt_require_feature() {
|
|
18
32
|
local feature="${1:-${BT_FEATURE:-}}"
|
|
19
33
|
[[ -n "$feature" ]] || bt_die "feature required (pass as first arg or set BT_FEATURE)"
|
|
20
34
|
|
|
35
|
+
# Apply sanitization if it doesn't already pass validation
|
|
36
|
+
if [[ "$feature" == *"/"* || "$feature" == *".."* || ! "$feature" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
|
37
|
+
feature="$(bt_sanitize_feature "$feature")"
|
|
38
|
+
fi
|
|
39
|
+
|
|
21
40
|
# Prevent path traversal and keep on-disk state predictable.
|
|
22
41
|
[[ "$feature" != *"/"* ]] || bt_die "invalid feature (must not contain '/'): $feature"
|
|
23
42
|
[[ "$feature" != *".."* ]] || bt_die "invalid feature (must not contain '..'): $feature"
|