biotonomy 0.1.0

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.
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt__count_statuses() {
5
+ local spec="$1"
6
+ awk '
7
+ BEGIN { pending=0; in_progress=0; done=0; failed=0; blocked=0; total=0 }
8
+ /^\- \*\*status:\*\*/ {
9
+ s=$0
10
+ sub(/.*\*\*status:\*\* /,"",s)
11
+ gsub(/[[:space:]]+/,"",s)
12
+ total++
13
+ if (s=="pending") pending++
14
+ else if (s=="in_progress") in_progress++
15
+ else if (s=="done") done++
16
+ else if (s=="failed") failed++
17
+ else if (s=="blocked") blocked++
18
+ }
19
+ END {
20
+ printf("stories=%d pending=%d in_progress=%d done=%d failed=%d blocked=%d\n",
21
+ total,pending,in_progress,done,failed,blocked)
22
+ }
23
+ ' "$spec" 2>/dev/null || true
24
+ }
25
+
26
+ # New bt__show_gates for the new JSON format:
27
+ # {"ts": "2026-02-15T17:38:00Z", "results": {"lint": {"cmd": "...", "status": 0}, ...}}
28
+ bt__show_gates() {
29
+ local json_file="$1"
30
+ [[ -f "$json_file" ]] || return 0
31
+
32
+ local json
33
+ json="$(cat "$json_file")"
34
+
35
+ local ts
36
+ ts=$(printf '%s' "$json" | grep -oE '"ts": "[^"]+"' | cut -d'"' -f4 || echo "unknown")
37
+
38
+ # A simple heuristic to check if any status is non-zero
39
+ # We look for "status": N where N > 0
40
+ local fails
41
+ fails=$(printf '%s' "$json" | grep -oE '"status": [1-9][0-9]*' | wc -l | xargs)
42
+
43
+ local status="pass"
44
+ [[ "$fails" -gt 0 ]] && status="fail"
45
+
46
+ # Also list which ones failed if any
47
+ local detail=""
48
+ if [[ "$fails" -gt 0 ]]; then
49
+ # Extremely primitive extraction of keys with non-zero status
50
+ # Assumes format "key": {"cmd": "...", "status": N}
51
+ detail=" ("
52
+ local k
53
+ # This regex is a bit fragile but works for the predictable format we write
54
+ for k in "lint" "typecheck" "test"; do
55
+ if echo "$json" | grep -qE "\"$k\": \{[^\}]*\"status\": [1-9][0-9]*"; then
56
+ detail="${detail}${k} "
57
+ fi
58
+ done
59
+ detail="${detail% })"
60
+ fi
61
+
62
+ printf " [gates:%s %s%s]" "$status" "$ts" "$detail"
63
+ }
64
+
65
+ bt_cmd_status() {
66
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
67
+ cat <<'EOF'
68
+ Usage:
69
+ bt status
70
+
71
+ Shows basic Biotonomy configuration and SPEC.md progress summary.
72
+ EOF
73
+ return 0
74
+ fi
75
+
76
+ bt_env_load || true
77
+
78
+ echo "bt v0.1.0"
79
+ echo "project_root: $BT_PROJECT_ROOT"
80
+ echo "env_file: ${BT_ENV_FILE:-<none>}"
81
+ echo "specs_dir: $BT_SPECS_DIR"
82
+ echo "state_dir: $BT_STATE_DIR"
83
+ echo "notify_hook: ${BT_NOTIFY_HOOK:-<none>}"
84
+
85
+ local state_dir="$BT_PROJECT_ROOT/$BT_STATE_DIR/state"
86
+ [[ -f "$state_dir/gates.json" ]] && echo "global:$(bt__show_gates "$state_dir/gates.json")"
87
+
88
+ local specs_path="$BT_PROJECT_ROOT/$BT_SPECS_DIR"
89
+ if [[ ! -d "$specs_path" ]]; then
90
+ echo "specs: <missing> ($specs_path)"
91
+ return 0
92
+ fi
93
+
94
+ local any=0
95
+ local d
96
+ for d in "$specs_path"/*; do
97
+ [[ -d "$d" ]] || continue
98
+ any=1
99
+ local feat
100
+ feat="$(basename "$d")"
101
+ local spec="$d/SPEC.md"
102
+ local summary
103
+ if [[ -f "$spec" ]]; then
104
+ summary="$(bt__count_statuses "$spec")"
105
+ else
106
+ summary="SPEC.md=<missing>"
107
+ fi
108
+ local gates_sum
109
+ gates_sum="$(bt__show_gates "$d/gates.json")"
110
+ echo "feature: $feat $summary$gates_sum"
111
+ done
112
+
113
+ [[ "$any" == "1" ]] || echo "features: <none>"
114
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Minimal Telegram notification hook.
5
+ #
6
+ # Required env:
7
+ # TELEGRAM_BOT_TOKEN
8
+ # TELEGRAM_CHAT_ID
9
+ #
10
+ # Usage:
11
+ # BT_NOTIFY_HOOK=./hooks/telegram.sh bt status
12
+
13
+ msg="$*"
14
+ [[ -n "${msg:-}" ]] || exit 0
15
+
16
+ if [[ -z "${TELEGRAM_BOT_TOKEN:-}" || -z "${TELEGRAM_CHAT_ID:-}" ]]; then
17
+ echo "telegram hook: missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID" >&2
18
+ exit 0
19
+ fi
20
+
21
+ if ! command -v curl >/dev/null 2>&1; then
22
+ echo "telegram hook: curl not found" >&2
23
+ exit 0
24
+ fi
25
+
26
+ curl -fsS \
27
+ -X POST \
28
+ -d "chat_id=${TELEGRAM_CHAT_ID}" \
29
+ --data-urlencode "text=${msg}" \
30
+ "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" >/dev/null
31
+
package/lib/codex.sh ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt_codex_bin() {
5
+ printf '%s\n' "${BT_CODEX_BIN:-codex}"
6
+ }
7
+
8
+ bt_codex_available() {
9
+ command -v "$(bt_codex_bin)" >/dev/null 2>&1
10
+ }
11
+
12
+ bt_codex_exec_full_auto() {
13
+ local prompt_file="$1"
14
+ if ! bt_codex_available; then
15
+ bt_warn "codex not found; skipping (set BT_CODEX_BIN or install codex)"
16
+ return 0
17
+ fi
18
+ local bin
19
+ bin="$(bt_codex_bin)"
20
+ local log_file
21
+ log_file="${BT_CODEX_LOG_FILE:-/dev/null}"
22
+ "$bin" exec --full-auto -C "$BT_PROJECT_ROOT" "$(cat "$prompt_file")" > >(tee -a "$log_file") 2>&1
23
+ }
24
+
25
+ bt_codex_exec_read_only() {
26
+ local prompt_file="$1"
27
+ local out_file="$2"
28
+ if ! bt_codex_available; then
29
+ bt_warn "codex not found; writing stub output to $out_file"
30
+ printf '%s\n' "Codex unavailable; v0.1.0 stub." >"$out_file"
31
+ return 0
32
+ fi
33
+ local bin
34
+ bin="$(bt_codex_bin)"
35
+ local log_file
36
+ log_file="${BT_CODEX_LOG_FILE:-/dev/null}"
37
+ "$bin" exec -s read-only -C "$BT_PROJECT_ROOT" -o "$out_file" "$(cat "$prompt_file")" > >(tee -a "$log_file") 2>&1
38
+ }
39
+
package/lib/env.sh ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt__export_kv() {
5
+ local key="$1"
6
+ local val="$2"
7
+
8
+ [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || return 0
9
+ # Export without eval; keep value literal even if it contains spaces/symbols.
10
+ export "$key=$val"
11
+ }
12
+
13
+ bt_env_load_file() {
14
+ local f="$1"
15
+ [[ -f "$f" ]] || return 1
16
+
17
+ bt_debug "loading env: $f"
18
+
19
+ # Parse KEY=VALUE lines (no eval), ignore comments/blank lines.
20
+ # Supports single/double quoted values, strips surrounding quotes.
21
+ while IFS= read -r line || [[ -n "$line" ]]; do
22
+ [[ "$line" =~ ^[[:space:]]*$ ]] && continue
23
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
24
+ line="${line#"${line%%[![:space:]]*}"}"
25
+
26
+ # Allow common `.env` style: `export KEY=VALUE`
27
+ if [[ "$line" =~ ^export[[:space:]]+ ]]; then
28
+ line="${line#export}"
29
+ line="${line#"${line%%[![:space:]]*}"}"
30
+ fi
31
+
32
+ [[ "$line" == *"="* ]] || continue
33
+ local key="${line%%=*}"
34
+ local val="${line#*=}"
35
+ key="${key%"${key##*[![:space:]]}"}"
36
+ val="${val#"${val%%[![:space:]]*}"}"
37
+ val="${val%"${val##*[![:space:]]}"}"
38
+
39
+ if [[ "$val" =~ ^\".*\"$ ]]; then
40
+ val="${val:1:${#val}-2}"
41
+ elif [[ "$val" =~ ^\'.*\'$ ]]; then
42
+ val="${val:1:${#val}-2}"
43
+ else
44
+ # Strip trailing inline comments for unquoted values: `KEY=VAL # comment`
45
+ val="${val%%[[:space:]]#*}"
46
+ val="${val%"${val##*[![:space:]]}"}"
47
+ fi
48
+
49
+ bt__export_kv "$key" "$val"
50
+ done <"$f"
51
+
52
+ export BT_ENV_FILE="$f"
53
+ }
54
+
55
+ bt_env_load() {
56
+ # Optional: run bt from anywhere, but operate on a specific target repo.
57
+ # When set, BT_TARGET_DIR becomes the effective BT_PROJECT_ROOT for all commands.
58
+ if [[ -n "${BT_TARGET_DIR:-}" ]]; then
59
+ local td
60
+ td="$(bt_realpath "$BT_TARGET_DIR")"
61
+ [[ -e "$td" ]] || bt_die "BT_TARGET_DIR does not exist: $BT_TARGET_DIR"
62
+ [[ -d "$td" ]] || bt_die "BT_TARGET_DIR is not a directory: $BT_TARGET_DIR"
63
+ export BT_TARGET_DIR="$td"
64
+ fi
65
+
66
+ local env_file="${BT_ENV_FILE:-}"
67
+ if [[ -n "$env_file" ]]; then
68
+ env_file="$(bt_realpath "$env_file")"
69
+ bt_env_load_file "$env_file" || bt_die "failed to load BT_ENV_FILE=$env_file"
70
+ else
71
+ # Prefer project config from the caller's current working directory.
72
+ if [[ -f "$PWD/.bt.env" ]]; then
73
+ bt_env_load_file "$PWD/.bt.env" || bt_die "failed to load env: $PWD/.bt.env"
74
+ # If running with a target repo, fall back to that repo's .bt.env.
75
+ elif [[ -n "${BT_TARGET_DIR:-}" && -f "$BT_TARGET_DIR/.bt.env" ]]; then
76
+ bt_env_load_file "$BT_TARGET_DIR/.bt.env" || bt_die "failed to load env: $BT_TARGET_DIR/.bt.env"
77
+ fi
78
+ fi
79
+
80
+ # Defaults
81
+ export BT_PROJECT_ROOT
82
+ if [[ -n "${BT_TARGET_DIR:-}" ]]; then
83
+ BT_PROJECT_ROOT="$BT_TARGET_DIR"
84
+ elif [[ -n "${BT_ENV_FILE:-}" ]]; then
85
+ BT_PROJECT_ROOT="$(cd "$(dirname "$BT_ENV_FILE")" && pwd)"
86
+ else
87
+ BT_PROJECT_ROOT="$PWD"
88
+ fi
89
+
90
+ export BT_SPECS_DIR="${BT_SPECS_DIR:-specs}"
91
+ export BT_STATE_DIR="${BT_STATE_DIR:-.bt}"
92
+ export BT_NOTIFY_HOOK="${BT_NOTIFY_HOOK:-}"
93
+ export BT_GATE_LINT="${BT_GATE_LINT:-}"
94
+ export BT_GATE_TYPECHECK="${BT_GATE_TYPECHECK:-}"
95
+ export BT_GATE_TEST="${BT_GATE_TEST:-}"
96
+ }
package/lib/gates.sh ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt__gate_detect() {
5
+ # Output: lint|typecheck|test command strings (one per line) for the detected ecosystem.
6
+ # Empty output => no auto-detection.
7
+ if [[ -f "$BT_PROJECT_ROOT/pnpm-lock.yaml" ]]; then
8
+ printf '%s\n' "lint=pnpm lint" "typecheck=pnpm typecheck" "test=pnpm test"
9
+ return 0
10
+ fi
11
+ if [[ -f "$BT_PROJECT_ROOT/yarn.lock" ]]; then
12
+ printf '%s\n' "lint=yarn lint" "typecheck=yarn typecheck" "test=yarn test"
13
+ return 0
14
+ fi
15
+ if [[ -f "$BT_PROJECT_ROOT/package-lock.json" ]]; then
16
+ printf '%s\n' "lint=npm run lint" "typecheck=npm run typecheck" "test=npm test"
17
+ return 0
18
+ fi
19
+ if [[ -f "$BT_PROJECT_ROOT/Makefile" ]]; then
20
+ printf '%s\n' "lint=make lint" "typecheck=make typecheck" "test=make test"
21
+ return 0
22
+ fi
23
+ return 1
24
+ }
25
+
26
+ bt__gate_cmd() {
27
+ local gate="$1"
28
+ case "$gate" in
29
+ lint) printf '%s\n' "${BT_GATE_LINT:-}" ;;
30
+ typecheck) printf '%s\n' "${BT_GATE_TYPECHECK:-}" ;;
31
+ test) printf '%s\n' "${BT_GATE_TEST:-}" ;;
32
+ *) return 1 ;;
33
+ esac
34
+ }
35
+
36
+ # Returns gate config (key=cmd) for those available.
37
+ bt_get_gate_config() {
38
+ local detected
39
+ detected="$(bt__gate_detect 2>/dev/null || true)"
40
+
41
+ local lint typecheck test
42
+ lint="$(bt__gate_cmd lint)"
43
+ typecheck="$(bt__gate_cmd typecheck)"
44
+ test="$(bt__gate_cmd test)"
45
+
46
+ if [[ -z "$lint" || -z "$typecheck" || -z "$test" ]]; then
47
+ local line k v
48
+ while IFS= read -r line; do
49
+ [[ "$line" == *"="* ]] || continue
50
+ k="${line%%=*}"
51
+ v="${line#*=}"
52
+ case "$k" in
53
+ lint) [[ -n "$lint" ]] || lint="$v" ;;
54
+ typecheck) [[ -n "$typecheck" ]] || typecheck="$v" ;;
55
+ test) [[ -n "$test" ]] || test="$v" ;;
56
+ esac
57
+ done <<<"$detected"
58
+ fi
59
+
60
+ [[ -n "$lint" ]] && printf 'lint=%s\n' "$lint"
61
+ [[ -n "$typecheck" ]] && printf 'typecheck=%s\n' "$typecheck"
62
+ [[ -n "$test" ]] && printf 'test=%s\n' "$test"
63
+ }
64
+
65
+ # Runs gates and returns a JSON string fragment with results.
66
+ # Writes logs to stderr. Returns 0 if all gates passed, 1 otherwise.
67
+ bt_run_gates() {
68
+ local config
69
+ config="$(bt_get_gate_config)"
70
+
71
+ local line k v
72
+ local results_json=""
73
+ local overall_ok=0
74
+ local any=0
75
+
76
+ while IFS= read -r line; do
77
+ [[ -n "$line" ]] || continue
78
+ any=1
79
+ k="${line%%=*}"
80
+ v="${line#*=}"
81
+
82
+ bt_info "gate: $k ($v)"
83
+ local status=0
84
+ # Use bash -lc for interactivity if needed, but we typically want it non-interactive.
85
+ # The original used bash -lc "$v". We'll stick to that but capture status.
86
+ if ! (cd "$BT_PROJECT_ROOT" && bash -lc "$v"); then
87
+ bt_err "gate failed: $k"
88
+ status=1
89
+ overall_ok=1
90
+ fi
91
+
92
+ local entry
93
+ printf -v entry '"%s": {"cmd": "%s", "status": %d}' "$k" "$v" "$status"
94
+ if [[ -z "$results_json" ]]; then
95
+ results_json="$entry"
96
+ else
97
+ results_json="$results_json, $entry"
98
+ fi
99
+ done <<<"$config"
100
+
101
+ if [[ "$any" == "0" ]]; then
102
+ bt_warn "no gates ran"
103
+ printf '{"ts": "%s", "results": {}}\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
104
+ return 0
105
+ fi
106
+
107
+ printf '{"ts": "%s", "results": {%s}}\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$results_json"
108
+ return "$overall_ok"
109
+ }
package/lib/log.sh ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt__is_tty() { [[ -t 2 ]]; }
5
+
6
+ bt__color() {
7
+ local code="$1"
8
+ if [[ "${BT_NO_COLOR:-0}" == "1" ]] || ! bt__is_tty; then
9
+ printf ''
10
+ else
11
+ printf '\033[%sm' "$code"
12
+ fi
13
+ }
14
+
15
+ bt__reset() {
16
+ if [[ "${BT_NO_COLOR:-0}" == "1" ]] || ! bt__is_tty; then
17
+ printf ''
18
+ else
19
+ printf '\033[0m'
20
+ fi
21
+ }
22
+
23
+ bt__ts() {
24
+ date +"%Y-%m-%d %H:%M:%S"
25
+ }
26
+
27
+ bt_info() {
28
+ printf '%s %sinfo%s %s\n' "$(bt__ts)" "$(bt__color 32)" "$(bt__reset)" "$*" >&2
29
+ }
30
+
31
+ bt_warn() {
32
+ printf '%s %swarn%s %s\n' "$(bt__ts)" "$(bt__color 33)" "$(bt__reset)" "$*" >&2
33
+ }
34
+
35
+ bt_err() {
36
+ printf '%s %serr%s %s\n' "$(bt__ts)" "$(bt__color 31)" "$(bt__reset)" "$*" >&2
37
+ }
38
+
39
+ bt_debug() {
40
+ [[ "${BT_DEBUG:-0}" == "1" ]] || return 0
41
+ printf '%s %sdbg%s %s\n' "$(bt__ts)" "$(bt__color 90)" "$(bt__reset)" "$*" >&2
42
+ }
43
+
44
+ bt_die() {
45
+ bt_err "$*"
46
+ exit 1
47
+ }
48
+
package/lib/notify.sh ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt_notify() {
5
+ local msg="$*"
6
+ [[ -n "${BT_NOTIFY_HOOK:-}" ]] || return 0
7
+ [[ -x "${BT_NOTIFY_HOOK:-}" ]] || {
8
+ bt_warn "BT_NOTIFY_HOOK is set but not executable: $BT_NOTIFY_HOOK"
9
+ return 0
10
+ }
11
+ "$BT_NOTIFY_HOOK" "$msg" || bt_warn "notify hook failed: $BT_NOTIFY_HOOK"
12
+ }
13
+
package/lib/path.sh ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt_realpath() {
5
+ local p="$1"
6
+ if command -v realpath >/dev/null 2>&1; then
7
+ realpath "$p"
8
+ return 0
9
+ fi
10
+ if command -v python3 >/dev/null 2>&1; then
11
+ python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$p" 2>/dev/null && return 0
12
+ fi
13
+
14
+ # Best-effort fallback: not fully resolving .. or symlinks.
15
+ case "$p" in
16
+ /*) printf '%s\n' "$p" ;;
17
+ *) printf '%s/%s\n' "$(pwd)" "$p" ;;
18
+ esac
19
+ }
20
+
21
+ bt_find_up() {
22
+ local name="$1"
23
+ local start="${2:-$PWD}"
24
+
25
+ local d
26
+ d="$(cd "$start" && pwd)"
27
+
28
+ while :; do
29
+ if [[ -e "$d/$name" ]]; then
30
+ printf '%s\n' "$d/$name"
31
+ return 0
32
+ fi
33
+ [[ "$d" == "/" ]] && return 1
34
+ d="$(dirname "$d")"
35
+ done
36
+ }
package/lib/repo.sh ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt_is_valid_repo_slug() {
5
+ local slug="${1:-}"
6
+ # Keep this strict and predictable: owner/repo with common GitHub-safe chars.
7
+ [[ -n "$slug" ]] || return 1
8
+ [[ "$slug" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || return 1
9
+ return 0
10
+ }
11
+
12
+ bt_parse_repo_from_remote_url() {
13
+ local url="${1:-}"
14
+ [[ -n "$url" ]] || return 1
15
+
16
+ local slug=""
17
+ case "$url" in
18
+ git@github.com:*)
19
+ slug="${url#git@github.com:}"
20
+ ;;
21
+ ssh://git@github.com/*)
22
+ slug="${url#ssh://git@github.com/}"
23
+ ;;
24
+ https://github.com/*)
25
+ slug="${url#https://github.com/}"
26
+ ;;
27
+ http://github.com/*)
28
+ slug="${url#http://github.com/}"
29
+ ;;
30
+ *)
31
+ return 1
32
+ ;;
33
+ esac
34
+
35
+ slug="${slug%.git}"
36
+ bt_is_valid_repo_slug "$slug" || return 1
37
+ printf '%s\n' "$slug"
38
+ }
39
+
40
+ bt_repo_from_git_origin() {
41
+ # Best-effort: only rely on origin if we're in a git worktree and origin is set.
42
+ local root="${1:-$PWD}"
43
+ command -v git >/dev/null 2>&1 || return 1
44
+
45
+ git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 1
46
+ local url
47
+ url="$(git -C "$root" config --get remote.origin.url 2>/dev/null || true)"
48
+ [[ -n "$url" ]] || return 1
49
+ bt_parse_repo_from_remote_url "$url"
50
+ }
51
+
52
+ bt_repo_resolve() {
53
+ local root="${1:-${BT_PROJECT_ROOT:-$PWD}}"
54
+
55
+ local slug=""
56
+ slug="$(bt_repo_from_git_origin "$root" 2>/dev/null || true)"
57
+ if [[ -n "$slug" ]]; then
58
+ printf '%s\n' "$slug"
59
+ return 0
60
+ fi
61
+
62
+ if [[ -n "${BT_REPO:-}" ]]; then
63
+ bt_is_valid_repo_slug "$BT_REPO" || bt_die "invalid BT_REPO (expected owner/repo): $BT_REPO"
64
+ printf '%s\n' "$BT_REPO"
65
+ return 0
66
+ fi
67
+
68
+ bt_die "repo resolution failed: set BT_REPO=owner/repo in .bt.env (no usable git remote origin found)"
69
+ }
70
+
package/lib/state.sh ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bt_specs_path() {
5
+ printf '%s/%s' "$BT_PROJECT_ROOT" "$BT_SPECS_DIR"
6
+ }
7
+
8
+ bt_feature_dir() {
9
+ local feature="$1"
10
+ printf '%s/%s' "$(bt_specs_path)" "$feature"
11
+ }
12
+
13
+ bt_ensure_dirs() {
14
+ mkdir -p "$(bt_specs_path)" "$BT_PROJECT_ROOT/$BT_STATE_DIR"
15
+ }
16
+
17
+ bt_require_feature() {
18
+ local feature="${1:-${BT_FEATURE:-}}"
19
+ [[ -n "$feature" ]] || bt_die "feature required (pass as first arg or set BT_FEATURE)"
20
+
21
+ # Prevent path traversal and keep on-disk state predictable.
22
+ [[ "$feature" != *"/"* ]] || bt_die "invalid feature (must not contain '/'): $feature"
23
+ [[ "$feature" != *".."* ]] || bt_die "invalid feature (must not contain '..'): $feature"
24
+ [[ "$feature" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || bt_die "invalid feature (allowed: A-Z a-z 0-9 . _ -): $feature"
25
+ printf '%s\n' "$feature"
26
+ }
27
+
28
+ bt_progress_append() {
29
+ local feature="$1"
30
+ local msg="$2"
31
+ local dir
32
+ dir="$(bt_feature_dir "$feature")"
33
+ mkdir -p "$dir/history"
34
+ printf '%s %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$msg" >>"$dir/progress.txt"
35
+ }
36
+
37
+ bt_history_write() {
38
+ local feature="$1"
39
+ local stage="$2"
40
+ local content="$3"
41
+ local dir
42
+ dir="$(bt_feature_dir "$feature")"
43
+ mkdir -p "$dir/history"
44
+
45
+ local max=0 f base n
46
+ shopt -s nullglob
47
+ for f in "$dir/history/"[0-9][0-9][0-9]-*.md; do
48
+ base="$(basename "$f")"
49
+ n="${base%%-*}"
50
+ [[ "$n" =~ ^[0-9]{3}$ ]] || continue
51
+ ((10#$n > max)) && max=$((10#$n))
52
+ done
53
+ shopt -u nullglob
54
+ n="$((max + 1))"
55
+ printf -v n '%03d' "$n"
56
+
57
+ local out="$dir/history/${n}-${stage}.md"
58
+ printf '%s\n' "$content" >"$out"
59
+ printf '%s\n' "$out"
60
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "biotonomy",
3
+ "version": "0.1.0",
4
+ "description": "Codex-native autonomous development loop CLI",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/archive-dot-com/biotonomy.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/archive-dot-com/biotonomy/issues"
12
+ },
13
+ "homepage": "https://github.com/archive-dot-com/biotonomy#readme",
14
+ "bin": {
15
+ "bt": "bt"
16
+ },
17
+ "files": [
18
+ "bt",
19
+ "bt.sh",
20
+ "commands/",
21
+ "lib/",
22
+ "prompts/",
23
+ "hooks/",
24
+ "scripts/",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "scripts": {
29
+ "test": "node tests/run.mjs",
30
+ "lint": "bash scripts/lint-shell.sh",
31
+ "demo": "bash scripts/demo-issue-3-real-loop.sh",
32
+ "pr:open": "bash scripts/gh-pr.sh",
33
+ "verify:pack": "node scripts/verify-pack.mjs",
34
+ "prepublishOnly": "npm test && npm run lint && npm run verify:pack"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }
package/prompts/fix.md ADDED
@@ -0,0 +1,17 @@
1
+ # Fix (Biotonomy v0.1.0)
2
+
3
+ You are the fix agent. Apply targeted patches to address `REVIEW.md` findings only.
4
+
5
+ Rules:
6
+ - No rewrites. Keep changes minimal and localized.
7
+ - Update/add tests to prevent regressions.
8
+ - Re-run quality gates after changes.
9
+
10
+ Inputs:
11
+ - `specs/<feature>/REVIEW.md`
12
+ - `specs/<feature>/SPEC.md`
13
+
14
+ Outputs:
15
+ - Surgical code changes + tests
16
+ - Updated `SPEC.md` if story status changes
17
+