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 CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- BT_VERSION="0.1.0"
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) bt_usage; return 0 ;;
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
- bt_err "missing or unapproved $plan_review"
67
- bt_err "run: bt plan-review $feature"
68
- bt_die "loop hard-fails without approved PLAN_REVIEW verdict before implement/review"
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 (( ${#gate_args[@]} > 0 )); then
80
- if ! bt_run_gates "${gate_args[@]}"; then
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 (( ${#gate_args[@]} > 0 )); then
225
- if ! bt_run_gates "${gate_args[@]}"; then
226
- gates_ok=0
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
- if ! bt_run_gates; then
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"
@@ -29,26 +29,20 @@ EOF
29
29
 
30
30
  local out="$dir/PLAN_REVIEW.md"
31
31
 
32
- if bt_codex_available; then
33
- bt_info "running codex (full-auto) using prompts/plan-review.md"
34
- local artifacts_dir codex_logf
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
- Verdict: APPROVED_PLAN
50
- EOF
51
- bt_info "wrote $out"
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
@@ -161,6 +161,7 @@ bt_cmd_pr() {
161
161
  bt_err "feature name is required"
162
162
  return 2
163
163
  fi
164
+ feature="$(bt_require_feature "$feature")"
164
165
 
165
166
  # Ensure BT_PROJECT_ROOT reflects BT_TARGET_DIR / BT_ENV_FILE, and operate within it.
166
167
  bt_env_load || true
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
- repo="$(bt_repo_resolve "$BT_PROJECT_ROOT")"
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")
@@ -75,7 +75,7 @@ EOF
75
75
 
76
76
  bt_env_load || true
77
77
 
78
- echo "bt v0.1.0"
78
+ echo "bt v${BT_VERSION:-unknown}"
79
79
  echo "project_root: $BT_PROJECT_ROOT"
80
80
  echo "env_file: ${BT_ENV_FILE:-<none>}"
81
81
  echo "specs_dir: $BT_SPECS_DIR"
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
- "$bin" exec --full-auto -C "$BT_PROJECT_ROOT" "$(cat "$prompt_file")" 2>&1 | tee -a "$log_file"
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biotonomy",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "description": "Codex-native autonomous development loop CLI",
5
5
  "license": "MIT",
6
6
  "repository": {