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/commands/pr.sh ADDED
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env bash
2
+ # biotonomy pr command
3
+
4
+ bt_pr_usage() {
5
+ cat <<EOF
6
+ Usage: bt pr <feature-name> [options]
7
+
8
+ Ship a feature by running tests, committing changes, pushing, and creating a PR.
9
+
10
+ Options:
11
+ --run Actually execute push and gh pr create (default: dry-run)
12
+ --dry-run Print push and gh commands without executing
13
+ --draft Create the PR as a draft
14
+ --base <ref> Base branch for the PR (default: remote's HEAD or main)
15
+ --remote <n> Remote to push to (default: origin)
16
+ --no-commit Skip committing changes (expects they are already committed)
17
+ -h, --help Show this help
18
+ EOF
19
+ }
20
+
21
+ bt_pr_file_size_bytes() {
22
+ local f="$1"
23
+ local sz=""
24
+ # macOS/BSD stat
25
+ if sz="$(stat -f%z "$f" 2>/dev/null)"; then
26
+ printf '%s\n' "$sz"
27
+ return 0
28
+ fi
29
+ # GNU stat
30
+ if sz="$(stat -c%s "$f" 2>/dev/null)"; then
31
+ printf '%s\n' "$sz"
32
+ return 0
33
+ fi
34
+ # Fallback
35
+ wc -c <"$f" | tr -d ' '
36
+ }
37
+
38
+ bt_pr_is_text_file() {
39
+ local f="$1"
40
+ # grep -I treats binary as non-matching and exits 1; empty files also exit 1, so handle empties elsewhere.
41
+ LC_ALL=C grep -Iq . "$f" 2>/dev/null
42
+ }
43
+
44
+ bt_pr_append_artifact_section() {
45
+ local out_file="$1"
46
+ local relpath="$2"
47
+ local abspath="$3"
48
+ local max_inline_bytes="$4"
49
+
50
+ {
51
+ # shellcheck disable=SC2016
52
+ printf '\n### `%s`\n' "$relpath"
53
+ # shellcheck disable=SC2016
54
+ printf 'Path: [`%s`](%s)\n\n' "$relpath" "$relpath"
55
+
56
+ if [[ ! -f "$abspath" ]]; then
57
+ printf "_missing_\\n"
58
+ return 0
59
+ fi
60
+
61
+ local size
62
+ size="$(bt_pr_file_size_bytes "$abspath")"
63
+
64
+ if [[ "$size" -ge "$max_inline_bytes" ]]; then
65
+ printf "_not inlined (size: %s bytes)_\\n" "$size"
66
+ return 0
67
+ fi
68
+
69
+ if [[ "$size" -gt 0 ]] && ! bt_pr_is_text_file "$abspath"; then
70
+ printf "_not inlined (binary or non-text; size: %s bytes)_\\n" "$size"
71
+ return 0
72
+ fi
73
+
74
+ local fence='```'
75
+ if grep -q '```' "$abspath" 2>/dev/null; then
76
+ fence='````'
77
+ fi
78
+
79
+ printf "%s\\n" "$fence"
80
+ cat "$abspath"
81
+ # Ensure trailing newline so the closing fence is on its own line.
82
+ printf "\\n%s\\n" "$fence"
83
+ } >>"$out_file"
84
+ }
85
+
86
+ bt_pr_write_artifacts_comment() {
87
+ local feature="$1"
88
+ local specs_dir="$2"
89
+ local out_file="$3"
90
+
91
+ local max_inline_bytes=$((20 * 1024))
92
+ local spec_rel="$specs_dir/$feature/SPEC.md"
93
+ local review_rel="$specs_dir/$feature/REVIEW.md"
94
+ local artifacts_dir_rel="$specs_dir/$feature/.artifacts"
95
+
96
+ : >"$out_file"
97
+ {
98
+ printf "## Artifacts\\n\\n"
99
+ # shellcheck disable=SC2016
100
+ printf 'Feature: `%s`\n' "$feature"
101
+ } >>"$out_file"
102
+
103
+ bt_pr_append_artifact_section "$out_file" "$spec_rel" "$spec_rel" "$max_inline_bytes"
104
+
105
+ if [[ -f "$review_rel" ]]; then
106
+ bt_pr_append_artifact_section "$out_file" "$review_rel" "$review_rel" "$max_inline_bytes"
107
+ fi
108
+
109
+ if [[ -d "$artifacts_dir_rel" ]]; then
110
+ local f
111
+ while IFS= read -r f; do
112
+ [[ -n "$f" ]] || continue
113
+ bt_pr_append_artifact_section "$out_file" "$f" "$f" "$max_inline_bytes"
114
+ done < <(find "$artifacts_dir_rel" -type f -print | LC_ALL=C sort)
115
+ fi
116
+ }
117
+
118
+ bt_cmd_pr() {
119
+ local feature=""
120
+ local run_mode="dry-run"
121
+ local draft=0
122
+ local base=""
123
+ local remote="origin"
124
+ local commit=1
125
+
126
+ while [[ $# -gt 0 ]]; do
127
+ case "${1:-}" in
128
+ -h|--help) bt_pr_usage; return 0 ;;
129
+ --run) run_mode="run"; shift ;;
130
+ --dry-run) run_mode="dry-run"; shift ;;
131
+ --draft) draft=1; shift ;;
132
+ --base) base="${2:-}"; shift 2 ;;
133
+ --remote) remote="${2:-}"; shift 2 ;;
134
+ --no-commit) commit=0; shift ;;
135
+ -*)
136
+ bt_err "unknown flag: $1"
137
+ return 2
138
+ ;;
139
+ *)
140
+ feature="$1"
141
+ shift
142
+ ;;
143
+ esac
144
+ done
145
+
146
+ if [[ -z "$feature" ]]; then
147
+ bt_err "feature name is required"
148
+ return 2
149
+ fi
150
+
151
+ # Ensure BT_PROJECT_ROOT reflects BT_TARGET_DIR / BT_ENV_FILE, and operate within it.
152
+ bt_env_load || true
153
+ [[ -n "${BT_PROJECT_ROOT:-}" ]] || bt_die "missing BT_PROJECT_ROOT"
154
+ cd "$BT_PROJECT_ROOT" || bt_die "failed to cd into BT_PROJECT_ROOT: $BT_PROJECT_ROOT"
155
+
156
+ bt_info "shipping feature: $feature"
157
+
158
+ # 1. Run Tests & Lint
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
165
+ local specs_dir="${BT_SPECS_DIR:-specs}"
166
+ local spec_file="$specs_dir/$feature/SPEC.md"
167
+ local branch="feat/$feature"
168
+ local repo=""
169
+ local issue=""
170
+
171
+ if [[ -f "$spec_file" ]]; then
172
+ local b
173
+ b="$(awk -F': *' '$1=="branch"{print $2; exit}' "$spec_file" | tr -d '\r')"
174
+ local r
175
+ r="$(awk -F': *' '$1=="repo"{print $2; exit}' "$spec_file" | tr -d '\r')"
176
+ local i
177
+ i="$(awk -F': *' '$1=="issue"{print $2; exit}' "$spec_file" | tr -d '\r')"
178
+ [[ -n "${b:-}" ]] && branch="$b"
179
+ [[ -n "${r:-}" ]] && repo="$r"
180
+ [[ -n "${i:-}" ]] && issue="$i"
181
+ fi
182
+
183
+ # 3. Create branch if needed
184
+ if git show-ref --verify --quiet "refs/heads/$branch"; then
185
+ bt_info "branch $branch already exists, checking it out..."
186
+ git checkout "$branch"
187
+ else
188
+ bt_info "creating and checking out branch $branch..."
189
+ git checkout -b "$branch"
190
+ fi
191
+
192
+ # 4. Commit changes if requested
193
+ if [[ "$commit" == "1" ]]; then
194
+ bt_info "committing changes..."
195
+ # Add SPEC.md and any tests/implementations related to this feature
196
+ # We use explicit paths to avoid staging unrelated items
197
+ local paths_to_add=()
198
+ [[ -d "$specs_dir/$feature" ]] && paths_to_add+=("$specs_dir/$feature")
199
+ [[ -d "tests" ]] && paths_to_add+=("tests")
200
+ [[ -d "lib" ]] && paths_to_add+=("lib")
201
+ [[ -d "commands" ]] && paths_to_add+=("commands")
202
+ [[ -d "scripts" ]] && paths_to_add+=("scripts")
203
+
204
+ if [[ ${#paths_to_add[@]} -gt 0 ]]; then
205
+ git add "${paths_to_add[@]}"
206
+ fi
207
+
208
+ if ! git diff --cached --quiet; then
209
+ git commit -m "feat($feature): ship implementation"
210
+ else
211
+ bt_info "nothing to commit"
212
+ fi
213
+ fi
214
+
215
+ # 5. Push
216
+ bt_info "pushing to $remote/$branch..."
217
+ if [[ "$run_mode" == "run" ]]; then
218
+ git push -u "$remote" "$branch"
219
+ else
220
+ bt_info "[dry-run] git push -u $remote $branch"
221
+ fi
222
+
223
+ # 6. Open PR via gh
224
+ if [[ -z "$base" ]]; then
225
+ local ref
226
+ ref="$(git symbolic-ref -q "refs/remotes/$remote/HEAD" 2>/dev/null || true)"
227
+ if [[ -n "$ref" ]]; then
228
+ # refs/remotes/<remote>/<branch> -> <branch>
229
+ base="${ref##*/}"
230
+ else
231
+ base="main"
232
+ fi
233
+ fi
234
+
235
+ local title="feat: $feature"
236
+ local body="Feature: $feature"
237
+ [[ -n "$repo" && -n "$issue" ]] && body+=$'\n'"Issue: https://github.com/$repo/issues/$issue"
238
+ [[ -f "$spec_file" ]] && body+=$'\n'"Spec: $spec_file"
239
+
240
+ bt_info "opening PR on $base..."
241
+ local pr_args=(pr create --head "$branch" --base "$base" --title "$title" --body "$body")
242
+ [[ "$draft" == "1" ]] && pr_args+=(--draft)
243
+
244
+ if [[ "$run_mode" == "run" ]]; then
245
+ local pr_out
246
+ pr_out="$(gh "${pr_args[@]}")"
247
+ # gh pr create typically prints the PR URL on success.
248
+ local pr_url
249
+ pr_url="$(printf '%s\n' "$pr_out" | tail -n 1 | tr -d '\r')"
250
+ if [[ -n "$pr_url" ]]; then
251
+ bt_info "posting artifacts comment..."
252
+ local comment_file
253
+ comment_file="$(mktemp "${TMPDIR:-/tmp}/bt-pr-comment.XXXXXX")"
254
+ bt_pr_write_artifacts_comment "$feature" "$specs_dir" "$comment_file"
255
+ gh pr comment "$pr_url" --body-file "$comment_file"
256
+ rm -f "$comment_file"
257
+ else
258
+ bt_err "gh pr create did not return a PR URL; skipping artifacts comment"
259
+ fi
260
+ else
261
+ bt_info "[dry-run] gh ${pr_args[*]}"
262
+ fi
263
+
264
+ bt_info "ship complete for $feature"
265
+ }
@@ -0,0 +1,80 @@
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_research() {
8
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
9
+ cat <<'EOF'
10
+ Usage:
11
+ bt research <feature>
12
+
13
+ Runs Codex in full-auto using prompts/research.md and produces specs/<feature>/RESEARCH.md.
14
+ EOF
15
+ return 0
16
+ fi
17
+
18
+ bt_env_load || true
19
+ bt_ensure_dirs
20
+
21
+ local feature
22
+ feature="$(bt_require_feature "${1:-}")"
23
+
24
+ local dir
25
+ dir="$(bt_feature_dir "$feature")"
26
+ [[ -d "$dir" ]] || bt_die "missing feature dir: $dir (run: bt spec ...)"
27
+ mkdir -p "$dir/history"
28
+
29
+ local out="$dir/RESEARCH.md"
30
+ bt_codex_available || bt_die "codex required for research (set BT_CODEX_BIN or install codex)"
31
+
32
+ bt_info "running codex (read-only) using prompts/research.md -> $out"
33
+ local codex_ec=0 codex_logf artifacts_dir
34
+ artifacts_dir="$dir/.artifacts"
35
+ mkdir -p "$artifacts_dir"
36
+ # Deterministic path for reproducible runs and CI artifacts (no mktemp randomness).
37
+ codex_logf="$artifacts_dir/codex-research.log"
38
+ : >"$codex_logf"
39
+ if ! BT_FEATURE="$feature" BT_CODEX_LOG_FILE="$codex_logf" bt_codex_exec_read_only "$BT_ROOT/prompts/research.md" "$out"; then
40
+ codex_ec=$?
41
+ bt_warn "codex exited non-zero (research): $codex_ec"
42
+ fi
43
+
44
+ if [[ ! -f "$out" ]]; then
45
+ local err_tail
46
+ err_tail="$(tail -n 80 "$codex_logf" 2>/dev/null || true)"
47
+ cat >"$out" <<EOF
48
+ # Research: $feature
49
+
50
+ Codex did not create \`$out\`. A stub was generated so the loop can continue.
51
+
52
+ - codex_exit: $codex_ec
53
+ - feature_dir: $dir
54
+ - prompt: $BT_ROOT/prompts/research.md
55
+ - bt_cmd: bt research $feature
56
+
57
+ ## Codex stderr (tail)
58
+
59
+ ${err_tail:-"(no stderr captured)"}
60
+
61
+ Next:
62
+ - Run Codex against \`$BT_ROOT/prompts/research.md\`
63
+ - Capture findings here (patterns, pitfalls, test/lint commands, etc)
64
+ EOF
65
+ fi
66
+
67
+ bt_progress_append "$feature" "research: bt research $feature (codex_exit=$codex_ec)"
68
+ bt_history_write "$feature" "research" "$(cat <<EOF
69
+ # Research Run: $feature
70
+
71
+ - when: $(date +'%Y-%m-%d %H:%M:%S')
72
+ - bt_cmd: bt research $feature
73
+ - prompt: prompts/research.md
74
+ - codex_exit: $codex_ec
75
+ - output: specs/$feature/RESEARCH.md
76
+ EOF
77
+ )"
78
+ [[ -f "$out" ]] && bt_info "wrote $out"
79
+ bt_notify "bt research complete for $feature"
80
+ }
@@ -0,0 +1,37 @@
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_reset() {
8
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
9
+ cat <<'EOF'
10
+ Usage:
11
+ bt reset
12
+
13
+ Removes Biotonomy ephemeral state (`.bt/`) and any `specs/**/.lock` files.
14
+
15
+ Note: v0.1.0 does NOT modify your git working tree.
16
+ EOF
17
+ return 0
18
+ fi
19
+
20
+ bt_env_load || true
21
+ bt_ensure_dirs
22
+
23
+ local state="$BT_PROJECT_ROOT/$BT_STATE_DIR"
24
+ if [[ -d "$state" ]]; then
25
+ rm -rf "$state"
26
+ bt_info "removed $state"
27
+ fi
28
+
29
+ local specs_path
30
+ specs_path="$(bt_specs_path)"
31
+ if [[ -d "$specs_path" ]]; then
32
+ find "$specs_path" -type f -name ".lock" -print -delete 2>/dev/null || true
33
+ fi
34
+
35
+ bt_notify "bt reset complete in $BT_PROJECT_ROOT"
36
+ }
37
+
@@ -0,0 +1,84 @@
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_review() {
8
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
9
+ cat <<'EOF'
10
+ Usage:
11
+ bt review <feature>
12
+
13
+ Runs Codex in read-only using prompts/review.md and produces specs/<feature>/REVIEW.md.
14
+ EOF
15
+ return 0
16
+ fi
17
+
18
+ bt_env_load || true
19
+ bt_ensure_dirs
20
+
21
+ local feature
22
+ feature="$(bt_require_feature "${1:-}")"
23
+
24
+ local dir
25
+ dir="$(bt_feature_dir "$feature")"
26
+ [[ -d "$dir" ]] || bt_die "missing feature dir: $dir (run: bt spec ...)"
27
+
28
+ local out="$dir/REVIEW.md"
29
+ local artifacts_dir codex_logf
30
+ artifacts_dir="$dir/.artifacts"
31
+ mkdir -p "$artifacts_dir"
32
+ codex_logf="$artifacts_dir/codex-review.log"
33
+ : >"$codex_logf"
34
+ local codex_ec=0
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=$?
37
+ bt_warn "codex exited non-zero (review): $codex_ec"
38
+ fi
39
+
40
+ if [[ ! -f "$out" ]]; then
41
+ cat >"$out" <<EOF
42
+ # Review: $feature
43
+
44
+ Verdict: NEEDS_CHANGES
45
+
46
+ Codex did not produce \`$out\`. A stub was generated so the loop can continue.
47
+
48
+ - codex_exit: $codex_ec
49
+ - feature_dir: $dir
50
+ - prompt: $BT_ROOT/prompts/review.md
51
+ - bt_cmd: bt review $feature
52
+ EOF
53
+ elif ! grep -qi '^Verdict:' "$out" 2>/dev/null; then
54
+ local tmp artifacts_dir
55
+ artifacts_dir="$dir/.artifacts"
56
+ mkdir -p "$artifacts_dir"
57
+ # Deterministic temp path to keep outputs reproducible and scoped to the feature dir.
58
+ tmp="$artifacts_dir/review.rewrite.tmp.md"
59
+ cat >"$tmp" <<EOF
60
+ # Review: $feature
61
+
62
+ Verdict: NEEDS_CHANGES
63
+
64
+ Codex output was missing the required \`Verdict:\` line. Content preserved below.
65
+
66
+ EOF
67
+ cat "$out" >>"$tmp" 2>/dev/null || true
68
+ mv "$tmp" "$out"
69
+ fi
70
+
71
+ bt_progress_append "$feature" "review: bt review $feature (codex_exit=$codex_ec)"
72
+ bt_history_write "$feature" "review" "$(cat <<EOF
73
+ # Review Run: $feature
74
+
75
+ - when: $(date +'%Y-%m-%d %H:%M:%S')
76
+ - bt_cmd: bt review $feature
77
+ - prompt: prompts/review.md
78
+ - codex_exit: $codex_ec
79
+ - output: specs/$feature/REVIEW.md
80
+ EOF
81
+ )"
82
+ bt_info "wrote $out"
83
+ bt_notify "bt review complete for $feature"
84
+ }
@@ -0,0 +1,196 @@
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__require_cmd() {
8
+ local cmd="$1"
9
+ command -v "$cmd" >/dev/null 2>&1 || bt_die "missing required command: $cmd"
10
+ }
11
+
12
+ bt__summarize_body() {
13
+ local s="${1:-}"
14
+ s="${s//$'\r'/}"
15
+ # Collapse whitespace and cap length so the SPEC stays readable.
16
+ s="$(printf '%s' "$s" | sed -e 's/[[:space:]][[:space:]]*/ /g' -e 's/^ *//; s/ *$//')"
17
+ if (( ${#s} > 900 )); then
18
+ s="${s:0:897}..."
19
+ fi
20
+ printf '%s\n' "$s"
21
+ }
22
+
23
+ bt__json_fields_sep() {
24
+ # Reads JSON on stdin and prints: title<US>url<US>body (US = 0x1f).
25
+ # Avoid NUL: bash variables can't reliably hold it.
26
+ bt__require_cmd node
27
+ node -e "$(
28
+ cat <<'NODE'
29
+ const fs = require("fs");
30
+ const j = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
31
+ const title = (j.title ?? "").toString();
32
+ const url = (j.url ?? "").toString();
33
+ // Make body newline-free so bash `read` (used by the caller) does not truncate it.
34
+ const body = (j.body ?? "").toString().replace(/[\r\n]+/g, " ");
35
+ // Ensure a terminating newline so bash `read` does not exit non-zero at EOF.
36
+ process.stdout.write(title + "\x1f" + url + "\x1f" + body + "\n");
37
+ NODE
38
+ )"
39
+ }
40
+
41
+ bt_cmd_spec() {
42
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
43
+ cat <<'EOF'
44
+ Usage:
45
+ bt spec <issue#>
46
+ bt spec <feature>
47
+
48
+ For an <issue#>, requires `gh` and creates `specs/issue-<n>/SPEC.md` using the issue title/body.
49
+ For a <feature>, creates `specs/<feature>/SPEC.md` with a minimal, parseable story list.
50
+ EOF
51
+ return 0
52
+ fi
53
+
54
+ bt_env_load || true
55
+ bt_ensure_dirs
56
+
57
+ local arg="${1:-}"
58
+ [[ -n "$arg" ]] || bt_die "spec requires <issue#> or <feature>"
59
+
60
+ local feature issue
61
+ issue=""
62
+ if [[ "$arg" =~ ^[0-9]+$ ]]; then
63
+ issue="$arg"
64
+ feature="issue-$arg"
65
+ else
66
+ feature="$arg"
67
+ fi
68
+
69
+ local dir
70
+ dir="$(bt_feature_dir "$feature")"
71
+ mkdir -p "$dir/history"
72
+
73
+ local spec="$dir/SPEC.md"
74
+ if [[ -f "$spec" ]]; then
75
+ bt_info "SPEC already exists: $spec"
76
+ return 0
77
+ fi
78
+
79
+ if [[ -n "$issue" ]]; then
80
+ bt__require_cmd gh
81
+
82
+ local repo
83
+ repo="$(bt_repo_resolve "$BT_PROJECT_ROOT")"
84
+
85
+ local -a gh_cmd
86
+ gh_cmd=(gh issue view "$issue" -R "$repo" --json "title,body,url")
87
+
88
+ local json errf artifacts_dir
89
+ artifacts_dir="$dir/.artifacts"
90
+ mkdir -p "$artifacts_dir"
91
+ # Deterministic stderr capture for reproducible runs (avoid mktemp randomness).
92
+ errf="$artifacts_dir/gh.stderr"
93
+ : >"$errf"
94
+ if ! json="$("${gh_cmd[@]}" 2>"$errf")"; then
95
+ local ec=$?
96
+ local err
97
+ err="$(cat "$errf" 2>/dev/null || true)"
98
+ bt_die "failed to fetch issue #$issue via gh (exit $ec): $err"
99
+ fi
100
+ rm -f "$errf" || true
101
+
102
+ local title url body
103
+ IFS=$'\x1f' read -r title url body < <(printf '%s' "$json" | bt__json_fields_sep)
104
+ [[ -n "$title" ]] || title="(untitled)"
105
+ [[ -n "$url" ]] || url="https://github.com/$repo/issues/$issue"
106
+
107
+ local summary
108
+ summary="$(bt__summarize_body "$body")"
109
+
110
+ cat >"$spec" <<EOF
111
+ ---
112
+ name: $feature
113
+ branch: feat/$feature
114
+ issue: $issue
115
+ repo: $repo
116
+ ---
117
+
118
+ # Problem
119
+
120
+ ## $title
121
+
122
+ - **issue:** #$issue
123
+ - **link:** $url
124
+
125
+ $summary
126
+
127
+ # Stories
128
+
129
+ ## [ID:S1] Confirm repo resolution and env fallback
130
+ - **status:** draft
131
+ - **priority:** 1
132
+ - **acceptance:** bt can determine repo slug from git remote origin; otherwise requires BT_REPO
133
+ - **tests:**
134
+
135
+ ## [ID:S2] Fetch issue details via gh
136
+ - **status:** draft
137
+ - **priority:** 1
138
+ - **acceptance:** bt spec <issue#> uses gh to retrieve title/body/url and handles errors clearly
139
+ - **tests:**
140
+
141
+ ## [ID:S3] Generate a SPEC.md with frontmatter + problem summary
142
+ - **status:** draft
143
+ - **priority:** 1
144
+ - **acceptance:** SPEC includes required frontmatter, a Problem section, and a Stories section (3-7 stories)
145
+ - **tests:**
146
+
147
+ ## [ID:S4] Record exact gh commands used in SPEC footer
148
+ - **status:** draft
149
+ - **priority:** 2
150
+ - **acceptance:** SPEC footer includes the exact gh command(s) executed
151
+ - **tests:**
152
+
153
+ ## [ID:S5] Add tests stubbing gh via PATH
154
+ - **status:** draft
155
+ - **priority:** 1
156
+ - **acceptance:** tests run offline and validate SPEC content generation
157
+ - **tests:**
158
+
159
+ ---
160
+
161
+ ## Footer
162
+
163
+ ### gh
164
+ - \`${gh_cmd[*]}\`
165
+ EOF
166
+
167
+ bt_progress_append "$feature" "spec created (from gh issue $repo#$issue)"
168
+ bt_history_write "$feature" "spec" "Created SPEC.md from $repo#$issue."
169
+ bt_info "wrote $spec"
170
+ bt_notify "bt spec created for $feature"
171
+ return 0
172
+ fi
173
+
174
+ cat >"$spec" <<EOF
175
+ ---
176
+ name: $feature
177
+ branch: feat/$feature
178
+ issue: ${issue:-}
179
+ repo:
180
+ ---
181
+
182
+ # Stories
183
+
184
+ ## [ID:S1] Define acceptance criteria
185
+ - **status:** pending
186
+ - **priority:** 1
187
+ - **acceptance:** SPEC.md is filled out with real stories and tests
188
+ - **tests:**
189
+
190
+ EOF
191
+
192
+ bt_progress_append "$feature" "spec created"
193
+ bt_history_write "$feature" "spec" "Created SPEC.md for $feature."
194
+ bt_info "wrote $spec"
195
+ bt_notify "bt spec created for $feature"
196
+ }