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.
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/bt +5 -0
- package/bt.sh +134 -0
- package/commands/bootstrap.sh +47 -0
- package/commands/compound.sh +37 -0
- package/commands/design.sh +33 -0
- package/commands/fix.sh +70 -0
- package/commands/gates.sh +40 -0
- package/commands/implement.sh +70 -0
- package/commands/pr.sh +265 -0
- package/commands/research.sh +80 -0
- package/commands/reset.sh +37 -0
- package/commands/review.sh +84 -0
- package/commands/spec.sh +196 -0
- package/commands/status.sh +114 -0
- package/hooks/telegram.sh +31 -0
- package/lib/codex.sh +39 -0
- package/lib/env.sh +96 -0
- package/lib/gates.sh +109 -0
- package/lib/log.sh +48 -0
- package/lib/notify.sh +13 -0
- package/lib/path.sh +36 -0
- package/lib/repo.sh +70 -0
- package/lib/state.sh +60 -0
- package/package.json +39 -0
- package/prompts/fix.md +17 -0
- package/prompts/implement.md +19 -0
- package/prompts/research.md +12 -0
- package/prompts/review.md +15 -0
- package/scripts/demo-issue-3-real-loop.sh +221 -0
- package/scripts/gh-pr.sh +118 -0
- package/scripts/lint-shell.sh +41 -0
- package/scripts/verify-pack.mjs +54 -0
|
@@ -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
|
+
|