biotonomy 0.2.2 → 0.2.4
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/loop.sh +80 -15
- package/commands/plan-review.sh +6 -1
- package/commands/pr.sh +14 -22
- package/commands/spec.sh +81 -30
- package/lib/gates.sh +9 -0
- package/package.json +1 -1
package/commands/loop.sh
CHANGED
|
@@ -59,15 +59,6 @@ bt_cmd_loop() {
|
|
|
59
59
|
bt_env_load || true
|
|
60
60
|
bt_ensure_dirs
|
|
61
61
|
|
|
62
|
-
bt_info "starting loop for: $feature (max iterations: $max_iter)"
|
|
63
|
-
|
|
64
|
-
bt_info "running preflight gates..."
|
|
65
|
-
if ! bt_run_gates; then
|
|
66
|
-
bt_err "preflight gates failed; aborting before implement/review"
|
|
67
|
-
return 1
|
|
68
|
-
fi
|
|
69
|
-
bt_info "preflight gates: PASS"
|
|
70
|
-
|
|
71
62
|
local feat_dir
|
|
72
63
|
feat_dir="$(bt_feature_dir "$feature")"
|
|
73
64
|
local plan_review="$feat_dir/PLAN_REVIEW.md"
|
|
@@ -77,6 +68,27 @@ bt_cmd_loop() {
|
|
|
77
68
|
bt_die "loop hard-fails without approved PLAN_REVIEW verdict before implement/review"
|
|
78
69
|
fi
|
|
79
70
|
|
|
71
|
+
bt_info "starting loop for: $feature (max iterations: $max_iter)"
|
|
72
|
+
|
|
73
|
+
local -a gate_args=()
|
|
74
|
+
if [[ "${BT_LOOP_REQUIRE_GATES:-0}" == "1" ]]; then
|
|
75
|
+
gate_args=(--require-any)
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
bt_info "running preflight gates..."
|
|
79
|
+
if (( ${#gate_args[@]} > 0 )); then
|
|
80
|
+
if ! bt_run_gates "${gate_args[@]}"; then
|
|
81
|
+
bt_err "preflight gates failed (or none configured); aborting before implement/review"
|
|
82
|
+
return 1
|
|
83
|
+
fi
|
|
84
|
+
else
|
|
85
|
+
if ! bt_run_gates; then
|
|
86
|
+
bt_err "preflight gates failed (or none configured); aborting before implement/review"
|
|
87
|
+
return 1
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
bt_info "preflight gates: PASS"
|
|
91
|
+
|
|
80
92
|
# Source required commands so we can call them directly
|
|
81
93
|
# shellcheck source=/dev/null
|
|
82
94
|
source "$BT_ROOT/commands/implement.sh"
|
|
@@ -92,10 +104,53 @@ bt_cmd_loop() {
|
|
|
92
104
|
review_file="$feat_dir/REVIEW.md"
|
|
93
105
|
local history_dir="$feat_dir/history"
|
|
94
106
|
local progress_file="$feat_dir/loop-progress.json"
|
|
107
|
+
local resume_iter=0
|
|
108
|
+
local should_resume=0
|
|
95
109
|
|
|
96
|
-
# Initialize progress
|
|
97
110
|
mkdir -p "$history_dir"
|
|
98
|
-
|
|
111
|
+
if [[ -f "$progress_file" ]]; then
|
|
112
|
+
local resume_meta
|
|
113
|
+
resume_meta="$(python3 - <<PY
|
|
114
|
+
import json
|
|
115
|
+
p = "$progress_file"
|
|
116
|
+
max_iter = $max_iter
|
|
117
|
+
try:
|
|
118
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
119
|
+
d = json.load(f)
|
|
120
|
+
except Exception:
|
|
121
|
+
print("0 0")
|
|
122
|
+
raise SystemExit(0)
|
|
123
|
+
result = str(d.get("result", ""))
|
|
124
|
+
completed = d.get("completedIterations", 0)
|
|
125
|
+
try:
|
|
126
|
+
completed = int(completed)
|
|
127
|
+
except Exception:
|
|
128
|
+
completed = 0
|
|
129
|
+
if result in {"in-progress", "implement-failed"} and completed < max_iter:
|
|
130
|
+
d["feature"] = "$feature"
|
|
131
|
+
d["maxIterations"] = max_iter
|
|
132
|
+
d["completedIterations"] = completed
|
|
133
|
+
d["result"] = "in-progress"
|
|
134
|
+
if not isinstance(d.get("iterations"), list):
|
|
135
|
+
d["iterations"] = []
|
|
136
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
137
|
+
json.dump(d, f, indent=2)
|
|
138
|
+
print(f"1 {completed}")
|
|
139
|
+
else:
|
|
140
|
+
print("0 0")
|
|
141
|
+
PY
|
|
142
|
+
)"
|
|
143
|
+
if [[ "$resume_meta" =~ ^1[[:space:]]+([0-9]+)$ ]]; then
|
|
144
|
+
should_resume=1
|
|
145
|
+
resume_iter="${BASH_REMATCH[1]}"
|
|
146
|
+
fi
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
if [[ "$should_resume" == "1" ]]; then
|
|
150
|
+
iter="$resume_iter"
|
|
151
|
+
bt_info "resuming loop from iteration $((iter + 1)) / $max_iter"
|
|
152
|
+
else
|
|
153
|
+
cat > "$progress_file" <<EOF
|
|
99
154
|
{
|
|
100
155
|
"feature": "$feature",
|
|
101
156
|
"maxIterations": $max_iter,
|
|
@@ -104,6 +159,7 @@ bt_cmd_loop() {
|
|
|
104
159
|
"iterations": []
|
|
105
160
|
}
|
|
106
161
|
EOF
|
|
162
|
+
fi
|
|
107
163
|
|
|
108
164
|
# Ensure subcommands return non-zero instead of exiting the entire loop process.
|
|
109
165
|
export BT_DIE_MODE="return"
|
|
@@ -165,11 +221,20 @@ PY
|
|
|
165
221
|
# bt_cmd_implement/fix return non-zero if gates fail, but we capture the status.
|
|
166
222
|
# We call bt_run_gates here just to be sure of the final state post-review.
|
|
167
223
|
local gates_ok=1
|
|
168
|
-
if
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
171
231
|
else
|
|
172
|
-
|
|
232
|
+
if ! bt_run_gates; then
|
|
233
|
+
gates_ok=0
|
|
234
|
+
bt_info "gates: FAIL"
|
|
235
|
+
else
|
|
236
|
+
bt_info "gates: PASS"
|
|
237
|
+
fi
|
|
173
238
|
fi
|
|
174
239
|
|
|
175
240
|
local fix_status="SKIP"
|
package/commands/plan-review.sh
CHANGED
|
@@ -31,7 +31,12 @@ EOF
|
|
|
31
31
|
|
|
32
32
|
if bt_codex_available; then
|
|
33
33
|
bt_info "running codex (full-auto) using prompts/plan-review.md"
|
|
34
|
-
|
|
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
|
|
35
40
|
:
|
|
36
41
|
else
|
|
37
42
|
bt_die "codex failed (plan-review)"
|
package/commands/pr.sh
CHANGED
|
@@ -189,20 +189,19 @@ bt_cmd_pr() {
|
|
|
189
189
|
fi
|
|
190
190
|
|
|
191
191
|
# 2. Fail-loud preflight for unstaged expected files (before tests/commit flow).
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
fi
|
|
192
|
+
# P0 #18: fail-loud even with --no-commit
|
|
193
|
+
local unstaged=""
|
|
194
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
195
|
+
# Protect all repo files, not only a fixed allowlist of directories.
|
|
196
|
+
unstaged="$(git ls-files --others --modified --exclude-standard 2>/dev/null || true)"
|
|
197
|
+
else
|
|
198
|
+
# Outside a git repo, treat present files (except .git internals) as unstaged by definition.
|
|
199
|
+
unstaged="$(find . -path './.git' -prune -o -type f -print 2>/dev/null | sed 's#^\./##' | LC_ALL=C sort || true)"
|
|
200
|
+
fi
|
|
201
|
+
if [[ -n "$unstaged" ]]; then
|
|
202
|
+
bt_err "Found unstaged files that might be required for this feature:"
|
|
203
|
+
printf '%s\n' "$unstaged" >&2
|
|
204
|
+
bt_die "Abort: ship requires all feature files to be staged. Use git add and try again."
|
|
206
205
|
fi
|
|
207
206
|
|
|
208
207
|
if [[ "$run_mode" == "dry-run" ]]; then
|
|
@@ -255,14 +254,7 @@ bt_cmd_pr() {
|
|
|
255
254
|
# 4. Commit changes if requested
|
|
256
255
|
if [[ "$commit" == "1" ]]; then
|
|
257
256
|
bt_info "committing changes..."
|
|
258
|
-
|
|
259
|
-
local check_paths=(tests lib commands scripts specs prompts)
|
|
260
|
-
unstaged="$(git status --porcelain -- "${check_paths[@]}" 2>/dev/null || true)"
|
|
261
|
-
if [[ -n "$unstaged" ]]; then
|
|
262
|
-
bt_err "Found unstaged files that might be required for this feature:"
|
|
263
|
-
printf '%s\n' "$unstaged" >&2
|
|
264
|
-
bt_die "Abort: ship requires all feature files to be staged. Use git add and try again."
|
|
265
|
-
fi
|
|
257
|
+
# (Unstaged check removed here as it is now redundant with global T0 check)
|
|
266
258
|
|
|
267
259
|
if ! git diff --cached --quiet; then
|
|
268
260
|
git commit -m "feat($feature): ship implementation"
|
package/commands/spec.sh
CHANGED
|
@@ -38,6 +38,84 @@ NODE
|
|
|
38
38
|
)"
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
bt__stories_from_issue_json() {
|
|
42
|
+
bt__require_cmd node
|
|
43
|
+
node -e "$(
|
|
44
|
+
cat <<'NODE'
|
|
45
|
+
const fs = require("fs");
|
|
46
|
+
const j = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
|
|
47
|
+
|
|
48
|
+
function clean(s) {
|
|
49
|
+
return String(s || "").replace(/\s+/g, " ").trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const title = clean(j.title);
|
|
53
|
+
const body = String(j.body || "").replace(/\r/g, "");
|
|
54
|
+
const lines = body.split("\n");
|
|
55
|
+
|
|
56
|
+
let inAcceptance = false;
|
|
57
|
+
const acceptanceBullets = [];
|
|
58
|
+
const allBullets = [];
|
|
59
|
+
for (const rawLine of lines) {
|
|
60
|
+
const line = rawLine || "";
|
|
61
|
+
if (/^\s*#{1,6}\s*acceptance\b/i.test(line) || /^\s*acceptance criteria\s*:?\s*$/i.test(line)) {
|
|
62
|
+
inAcceptance = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (/^\s*#{1,6}\s+/.test(line) && inAcceptance) {
|
|
66
|
+
inAcceptance = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const m = line.match(/^\s*[-*]\s+(?:\[[ xX]\]\s*)?(.+?)\s*$/);
|
|
70
|
+
if (!m) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const bullet = clean(m[1]);
|
|
74
|
+
if (!bullet) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
allBullets.push(bullet);
|
|
78
|
+
if (inAcceptance) {
|
|
79
|
+
acceptanceBullets.push(bullet);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const selectedBullets = acceptanceBullets.length > 0 ? acceptanceBullets : allBullets;
|
|
84
|
+
const storyTitles = [];
|
|
85
|
+
if (title) {
|
|
86
|
+
storyTitles.push(title);
|
|
87
|
+
}
|
|
88
|
+
for (const bullet of selectedBullets) {
|
|
89
|
+
if (storyTitles.length >= 5) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
storyTitles.push(bullet);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (storyTitles.length === 0) {
|
|
96
|
+
const fallback = clean(body).slice(0, 120);
|
|
97
|
+
if (fallback) {
|
|
98
|
+
storyTitles.push(fallback);
|
|
99
|
+
} else {
|
|
100
|
+
storyTitles.push("Capture issue requirements");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let out = "";
|
|
105
|
+
for (let i = 0; i < storyTitles.length; i += 1) {
|
|
106
|
+
const id = i + 1;
|
|
107
|
+
const titleText = storyTitles[i];
|
|
108
|
+
out += "## [ID:S" + id + "] " + titleText + "\n";
|
|
109
|
+
out += "- **status:** draft\n";
|
|
110
|
+
out += "- **priority:** " + (id <= 3 ? 1 : 2) + "\n";
|
|
111
|
+
out += "- **acceptance:** " + titleText + "\n";
|
|
112
|
+
out += "- **tests:**\n\n";
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(out);
|
|
115
|
+
NODE
|
|
116
|
+
)"
|
|
117
|
+
}
|
|
118
|
+
|
|
41
119
|
bt_cmd_spec() {
|
|
42
120
|
local force=0
|
|
43
121
|
local arg=""
|
|
@@ -132,8 +210,9 @@ EOF
|
|
|
132
210
|
[[ -n "$title" ]] || title="(untitled)"
|
|
133
211
|
[[ -n "$url" ]] || url="https://github.com/$repo/issues/$issue"
|
|
134
212
|
|
|
135
|
-
local summary
|
|
213
|
+
local summary stories
|
|
136
214
|
summary="$(bt__summarize_body "$body")"
|
|
215
|
+
stories="$(printf '%s' "$json" | bt__stories_from_issue_json)"
|
|
137
216
|
|
|
138
217
|
cat >"$spec" <<EOF
|
|
139
218
|
---
|
|
@@ -154,35 +233,7 @@ $summary
|
|
|
154
233
|
|
|
155
234
|
# Stories
|
|
156
235
|
|
|
157
|
-
|
|
158
|
-
- **status:** draft
|
|
159
|
-
- **priority:** 1
|
|
160
|
-
- **acceptance:** bt can determine repo slug from git remote origin; otherwise requires BT_REPO
|
|
161
|
-
- **tests:**
|
|
162
|
-
|
|
163
|
-
## [ID:S2] Fetch issue details via gh
|
|
164
|
-
- **status:** draft
|
|
165
|
-
- **priority:** 1
|
|
166
|
-
- **acceptance:** bt spec <issue#> uses gh to retrieve title/body/url and handles errors clearly
|
|
167
|
-
- **tests:**
|
|
168
|
-
|
|
169
|
-
## [ID:S3] Generate a SPEC.md with frontmatter + problem summary
|
|
170
|
-
- **status:** draft
|
|
171
|
-
- **priority:** 1
|
|
172
|
-
- **acceptance:** SPEC includes required frontmatter, a Problem section, and a Stories section (3-7 stories)
|
|
173
|
-
- **tests:**
|
|
174
|
-
|
|
175
|
-
## [ID:S4] Record exact gh commands used in SPEC footer
|
|
176
|
-
- **status:** draft
|
|
177
|
-
- **priority:** 2
|
|
178
|
-
- **acceptance:** SPEC footer includes the exact gh command(s) executed
|
|
179
|
-
- **tests:**
|
|
180
|
-
|
|
181
|
-
## [ID:S5] Add tests stubbing gh via PATH
|
|
182
|
-
- **status:** draft
|
|
183
|
-
- **priority:** 1
|
|
184
|
-
- **acceptance:** tests run offline and validate SPEC content generation
|
|
185
|
-
- **tests:**
|
|
236
|
+
${stories}
|
|
186
237
|
|
|
187
238
|
---
|
|
188
239
|
|
package/lib/gates.sh
CHANGED
|
@@ -77,6 +77,12 @@ bt_get_gate_config() {
|
|
|
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
79
|
bt_run_gates() {
|
|
80
|
+
local require_any=0
|
|
81
|
+
if [[ "${1:-}" == "--require-any" ]]; then
|
|
82
|
+
require_any=1
|
|
83
|
+
shift
|
|
84
|
+
fi
|
|
85
|
+
|
|
80
86
|
local config
|
|
81
87
|
config="$(bt_get_gate_config)"
|
|
82
88
|
|
|
@@ -115,6 +121,9 @@ bt_run_gates() {
|
|
|
115
121
|
if [[ "$any" == "0" ]]; then
|
|
116
122
|
bt_warn "no gates ran"
|
|
117
123
|
printf '{"ts": "%s", "results": {}}\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
|
124
|
+
if [[ "$require_any" == "1" ]]; then
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
118
127
|
return 0
|
|
119
128
|
fi
|
|
120
129
|
|