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
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
|
+
}
|
package/commands/spec.sh
ADDED
|
@@ -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
|
+
}
|