forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
package/scripts/beads-context.sh
DELETED
|
@@ -1,577 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# beads-context.sh — Helper script to manage issue context
|
|
3
|
-
# for the Forge 7-stage workflow.
|
|
4
|
-
#
|
|
5
|
-
# Subcommands:
|
|
6
|
-
# set-design <issue-id> <task-count> <task-file-path>
|
|
7
|
-
# set-acceptance <issue-id> "<criteria-text>"
|
|
8
|
-
# update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
|
|
9
|
-
# parse-progress <issue-id>
|
|
10
|
-
# stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."] [--workflow-state "{...}"]
|
|
11
|
-
# validate <issue-id>
|
|
12
|
-
#
|
|
13
|
-
# Cross-platform: works on Windows (Git Bash), macOS, and Linux.
|
|
14
|
-
# OWASP A03: All variables properly quoted to prevent shell injection.
|
|
15
|
-
|
|
16
|
-
set -euo pipefail
|
|
17
|
-
|
|
18
|
-
# Source shared sanitize library
|
|
19
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
-
if [[ -f "$SCRIPT_DIR/bootstrap-windows-tools.sh" ]]; then
|
|
21
|
-
source "$SCRIPT_DIR/bootstrap-windows-tools.sh"
|
|
22
|
-
fi
|
|
23
|
-
source "$SCRIPT_DIR/lib/sanitize.sh"
|
|
24
|
-
|
|
25
|
-
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
usage() {
|
|
28
|
-
cat >&2 <<'EOF'
|
|
29
|
-
Usage: beads-context.sh <subcommand> [args...]
|
|
30
|
-
|
|
31
|
-
Subcommands:
|
|
32
|
-
set-design <issue-id> <task-count> <task-file-path>
|
|
33
|
-
set-acceptance <issue-id> "<criteria-text>"
|
|
34
|
-
update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
|
|
35
|
-
parse-progress <issue-id>
|
|
36
|
-
stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."] [--workflow-state "{...}"]
|
|
37
|
-
validate <issue-id>
|
|
38
|
-
EOF
|
|
39
|
-
exit 1
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
die() {
|
|
43
|
-
echo "Error: $1" >&2
|
|
44
|
-
exit 1
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
# Resolve a Windows forge.exe installation for bash-based helper flows.
|
|
48
|
-
# This covers WSL/Git Bash cases where PowerShell can run forge.exe but bash PATH
|
|
49
|
-
# does not include the Windows install directory.
|
|
50
|
-
convert_windows_path() {
|
|
51
|
-
local raw="${1%$'\r'}"
|
|
52
|
-
|
|
53
|
-
if [[ -z "$raw" ]]; then
|
|
54
|
-
return 1
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
if [[ ! "$raw" =~ ^[A-Za-z]:\\ ]]; then
|
|
58
|
-
printf '%s' "$raw"
|
|
59
|
-
return 0
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
if command -v wslpath >/dev/null 2>&1; then
|
|
63
|
-
wslpath -u "$raw" 2>/dev/null && return 0
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
if command -v cygpath >/dev/null 2>&1; then
|
|
67
|
-
cygpath -u "$raw" 2>/dev/null && return 0
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
local drive rest
|
|
71
|
-
drive="$(printf '%s' "$raw" | cut -c1 | tr '[:upper:]' '[:lower:]')"
|
|
72
|
-
rest="${raw:2}"
|
|
73
|
-
rest="${rest//\\//}"
|
|
74
|
-
printf '/mnt/%s%s' "$drive" "$rest"
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
is_runnable_forge_candidate() {
|
|
78
|
-
local candidate="${1:-}"
|
|
79
|
-
|
|
80
|
-
if [[ -z "$candidate" || ! -f "$candidate" ]]; then
|
|
81
|
-
return 1
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
[[ -x "$candidate" || "$candidate" == *.exe ]]
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
resolve_forge_cmd() {
|
|
88
|
-
local candidate=""
|
|
89
|
-
local converted=""
|
|
90
|
-
|
|
91
|
-
if [[ -n "${FORGE_CMD:-}" ]]; then
|
|
92
|
-
if [[ "${FORGE_CMD}" == *"/"* || "${FORGE_CMD}" == *"\\"* ]]; then
|
|
93
|
-
converted="$(convert_windows_path "$FORGE_CMD")"
|
|
94
|
-
if is_runnable_forge_candidate "$converted"; then
|
|
95
|
-
printf '%s' "$converted"
|
|
96
|
-
return 0
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
is_runnable_forge_candidate "$FORGE_CMD" || return 1
|
|
100
|
-
fi
|
|
101
|
-
printf '%s' "$FORGE_CMD"
|
|
102
|
-
return 0
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
if command -v forge >/dev/null 2>&1; then
|
|
106
|
-
printf '%s' "forge"
|
|
107
|
-
return 0
|
|
108
|
-
fi
|
|
109
|
-
|
|
110
|
-
if command -v forge.exe >/dev/null 2>&1; then
|
|
111
|
-
printf '%s' "forge.exe"
|
|
112
|
-
return 0
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
for candidate in \
|
|
116
|
-
"$HOME/.local/bin/forge" \
|
|
117
|
-
"$HOME/.local/bin/forge.exe" \
|
|
118
|
-
"$HOME/.bun/bin/forge" \
|
|
119
|
-
"$HOME/.bun/bin/forge.exe"
|
|
120
|
-
do
|
|
121
|
-
if is_runnable_forge_candidate "$candidate"; then
|
|
122
|
-
printf '%s' "$candidate"
|
|
123
|
-
return 0
|
|
124
|
-
fi
|
|
125
|
-
done
|
|
126
|
-
|
|
127
|
-
if command -v where.exe >/dev/null 2>&1; then
|
|
128
|
-
while IFS= read -r candidate; do
|
|
129
|
-
candidate="${candidate%$'\r'}"
|
|
130
|
-
[[ -z "$candidate" ]] && continue
|
|
131
|
-
|
|
132
|
-
converted="$(convert_windows_path "$candidate")"
|
|
133
|
-
if is_runnable_forge_candidate "$converted"; then
|
|
134
|
-
printf '%s' "$converted"
|
|
135
|
-
return 0
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
if is_runnable_forge_candidate "$candidate"; then
|
|
139
|
-
printf '%s' "$candidate"
|
|
140
|
-
return 0
|
|
141
|
-
fi
|
|
142
|
-
done < <(where.exe forge 2>/dev/null || true)
|
|
143
|
-
fi
|
|
144
|
-
|
|
145
|
-
return 1
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
FORGE=""
|
|
149
|
-
|
|
150
|
-
get_forge_cmd() {
|
|
151
|
-
if [[ -z "$FORGE" ]]; then
|
|
152
|
-
FORGE="$(resolve_forge_cmd)" || die "forge is required but not found"
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
printf '%s' "$FORGE"
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
# Run forge issue update and check for errors in both exit code and output.
|
|
159
|
-
# The update operation exits 0 even for non-existent issues, so we check stdout too.
|
|
160
|
-
forge_update() {
|
|
161
|
-
local forge_cmd
|
|
162
|
-
forge_cmd="$(get_forge_cmd)"
|
|
163
|
-
local output
|
|
164
|
-
output="$("$forge_cmd" issue update "$@" 2>&1)"
|
|
165
|
-
local rc=$?
|
|
166
|
-
|
|
167
|
-
if [[ $rc -ne 0 ]]; then
|
|
168
|
-
echo "$output" >&2
|
|
169
|
-
return 1
|
|
170
|
-
fi
|
|
171
|
-
|
|
172
|
-
# Forge prints "Error resolving/updating ..." to stdout for non-existent issues
|
|
173
|
-
# Use specific patterns to avoid false positives from data containing "error"
|
|
174
|
-
if printf '%s' "$output" | grep -Eqi '^Error|Error resolving|Error updating'; then
|
|
175
|
-
echo "$output" >&2
|
|
176
|
-
return 1
|
|
177
|
-
fi
|
|
178
|
-
|
|
179
|
-
echo "$output"
|
|
180
|
-
return 0
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
# Run forge issue comment and check for errors similarly.
|
|
184
|
-
forge_comment() {
|
|
185
|
-
local forge_cmd
|
|
186
|
-
forge_cmd="$(get_forge_cmd)"
|
|
187
|
-
local output
|
|
188
|
-
output="$("$forge_cmd" issue comment "$@" 2>&1)"
|
|
189
|
-
local rc=$?
|
|
190
|
-
|
|
191
|
-
if [[ $rc -ne 0 ]]; then
|
|
192
|
-
echo "$output" >&2
|
|
193
|
-
return 1
|
|
194
|
-
fi
|
|
195
|
-
|
|
196
|
-
# Use specific error patterns to avoid false positives from data containing "error"
|
|
197
|
-
if printf '%s' "$output" | grep -Eqi '^Error|Error resolving|Error adding'; then
|
|
198
|
-
echo "$output" >&2
|
|
199
|
-
return 1
|
|
200
|
-
fi
|
|
201
|
-
|
|
202
|
-
echo "$output"
|
|
203
|
-
return 0
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
# Extract concatenated comment text from a `forge issue show --json` payload.
|
|
207
|
-
# Forge/issue show returns comments as an array of objects ({text:...}) — matching
|
|
208
|
-
# the issue-state contract in lib/workflow/state-manager.js — or, defensively, a
|
|
209
|
-
# raw string. jq is preferred; a grep/sed fallback captures "text" fields.
|
|
210
|
-
extract_comments_text() {
|
|
211
|
-
local json="$1"
|
|
212
|
-
|
|
213
|
-
if command -v jq &>/dev/null; then
|
|
214
|
-
printf '%s' "$json" | jq -r '
|
|
215
|
-
((if type == "array" then .[0] else . end).comments // [])
|
|
216
|
-
| if type == "array" then (map(.text? // tostring) | join("\n"))
|
|
217
|
-
else tostring end
|
|
218
|
-
' 2>/dev/null || true
|
|
219
|
-
return 0
|
|
220
|
-
fi
|
|
221
|
-
|
|
222
|
-
printf '%s' "$json" | grep -o '"text": *"[^"]*"' | sed 's/^"text": *"//;s/"$//' || true
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
# ── Subcommands ──────────────────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
cmd_set_design() {
|
|
228
|
-
if [[ $# -lt 3 ]]; then
|
|
229
|
-
echo "Usage: beads-context.sh set-design <issue-id> <task-count> <task-file-path>" >&2
|
|
230
|
-
exit 1
|
|
231
|
-
fi
|
|
232
|
-
|
|
233
|
-
local issue_id="$1"
|
|
234
|
-
local task_count
|
|
235
|
-
task_count="$(sanitize "$2")"
|
|
236
|
-
local task_file
|
|
237
|
-
task_file="$(sanitize "$3")"
|
|
238
|
-
|
|
239
|
-
local design_text="${task_count} tasks | ${task_file}"
|
|
240
|
-
|
|
241
|
-
if ! forge_update "$issue_id" --design "$design_text" > /dev/null; then
|
|
242
|
-
die "Failed to set design on issue ${issue_id}"
|
|
243
|
-
fi
|
|
244
|
-
|
|
245
|
-
echo "Design set on ${issue_id}: ${design_text}"
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
cmd_set_acceptance() {
|
|
249
|
-
if [[ $# -lt 2 ]]; then
|
|
250
|
-
echo "Usage: beads-context.sh set-acceptance <issue-id> \"<criteria-text>\"" >&2
|
|
251
|
-
exit 1
|
|
252
|
-
fi
|
|
253
|
-
|
|
254
|
-
local issue_id="$1"
|
|
255
|
-
local criteria
|
|
256
|
-
criteria="$(sanitize "$2")"
|
|
257
|
-
|
|
258
|
-
if ! forge_update "$issue_id" --acceptance "$criteria" > /dev/null; then
|
|
259
|
-
die "Failed to set acceptance on issue ${issue_id}"
|
|
260
|
-
fi
|
|
261
|
-
|
|
262
|
-
echo "Acceptance criteria set on ${issue_id}"
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
cmd_update_progress() {
|
|
266
|
-
if [[ $# -lt 7 ]]; then
|
|
267
|
-
echo "Usage: beads-context.sh update-progress <issue-id> <task-num> <total> \"<title>\" <commit-sha> <test-count> <gate-count>" >&2
|
|
268
|
-
exit 1
|
|
269
|
-
fi
|
|
270
|
-
|
|
271
|
-
local issue_id="$1"
|
|
272
|
-
local task_num
|
|
273
|
-
task_num="$(sanitize "$2")"
|
|
274
|
-
local total
|
|
275
|
-
total="$(sanitize "$3")"
|
|
276
|
-
local title
|
|
277
|
-
title="$(sanitize "$4")"
|
|
278
|
-
local commit_sha
|
|
279
|
-
commit_sha="$(sanitize "$5")"
|
|
280
|
-
local test_count
|
|
281
|
-
test_count="$(sanitize "$6")"
|
|
282
|
-
local gate_count
|
|
283
|
-
gate_count="$(sanitize "$7")"
|
|
284
|
-
|
|
285
|
-
# Strip backslashes from progress fields only — these are read back via
|
|
286
|
-
# printf '%b' in parse-progress, where \n/\t would expand into real escapes.
|
|
287
|
-
# Other commands (set-design, set-acceptance, stage-transition) keep backslashes
|
|
288
|
-
# intact to preserve Windows-style paths like docs\plans\tasks.md.
|
|
289
|
-
task_num="${task_num//\\/}"
|
|
290
|
-
total="${total//\\/}"
|
|
291
|
-
title="${title//\\/}"
|
|
292
|
-
commit_sha="${commit_sha//\\/}"
|
|
293
|
-
test_count="${test_count//\\/}"
|
|
294
|
-
gate_count="${gate_count//\\/}"
|
|
295
|
-
|
|
296
|
-
local note="Task ${task_num}/${total} done: ${title} | ${test_count} tests | ${commit_sha} | ${gate_count} gates"
|
|
297
|
-
|
|
298
|
-
if ! forge_update "$issue_id" --append-notes "$note" > /dev/null; then
|
|
299
|
-
die "Failed to update progress on issue ${issue_id}"
|
|
300
|
-
fi
|
|
301
|
-
|
|
302
|
-
echo "Progress updated on ${issue_id}: Task ${task_num}/${total}"
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
cmd_parse_progress() {
|
|
306
|
-
if [[ $# -lt 1 ]]; then
|
|
307
|
-
echo "Usage: beads-context.sh parse-progress <issue-id>" >&2
|
|
308
|
-
exit 1
|
|
309
|
-
fi
|
|
310
|
-
|
|
311
|
-
local issue_id="$1"
|
|
312
|
-
local forge_cmd
|
|
313
|
-
forge_cmd="$(get_forge_cmd)"
|
|
314
|
-
|
|
315
|
-
# Get the issue JSON — detect non-existent issues
|
|
316
|
-
local json
|
|
317
|
-
json="$("$forge_cmd" issue show "$issue_id" --json 2>&1)" || die "Failed to show issue ${issue_id}"
|
|
318
|
-
|
|
319
|
-
# show may exit 0 but print an error for non-existent issues
|
|
320
|
-
# Match specific error patterns, not the word "error" in data fields
|
|
321
|
-
if printf '%s' "$json" | grep -Eqi '^Error (fetching|resolving)'; then
|
|
322
|
-
die "Issue not found: ${issue_id}"
|
|
323
|
-
fi
|
|
324
|
-
|
|
325
|
-
# show returns "[]" or empty for non-existent issues
|
|
326
|
-
if [[ -z "$json" || "$json" == "[]" || "$json" == "null" ]]; then
|
|
327
|
-
die "Issue not found: ${issue_id}"
|
|
328
|
-
fi
|
|
329
|
-
|
|
330
|
-
# Extract notes field from JSON — prefer jq if available, fall back to grep/sed
|
|
331
|
-
local notes
|
|
332
|
-
if command -v jq &>/dev/null; then
|
|
333
|
-
# Handle both array ([{...}]) and object ({...}) show responses
|
|
334
|
-
notes="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].notes else .notes end // empty' 2>/dev/null || true)"
|
|
335
|
-
else
|
|
336
|
-
notes="$(printf '%s' "$json" | grep -o '"notes": *"[^"]*"' | head -1 | sed 's/^"notes": *"//;s/"$//' || true)"
|
|
337
|
-
fi
|
|
338
|
-
|
|
339
|
-
if [[ -z "$notes" ]]; then
|
|
340
|
-
echo "No progress data"
|
|
341
|
-
return 0
|
|
342
|
-
fi
|
|
343
|
-
|
|
344
|
-
# Split on literal \n (two chars: backslash + n) and count task lines
|
|
345
|
-
# Use printf to expand the \n into actual newlines for grep (POSIX-compliant)
|
|
346
|
-
local expanded
|
|
347
|
-
expanded="$(printf '%b' "$notes")"
|
|
348
|
-
|
|
349
|
-
local task_lines
|
|
350
|
-
task_lines="$(echo "$expanded" | grep -c 'Task [0-9]*/[0-9]* done:' || true)"
|
|
351
|
-
|
|
352
|
-
if [[ "$task_lines" -eq 0 ]]; then
|
|
353
|
-
echo "No progress data"
|
|
354
|
-
return 0
|
|
355
|
-
fi
|
|
356
|
-
|
|
357
|
-
# Get the last task line
|
|
358
|
-
local last_line
|
|
359
|
-
last_line="$(echo "$expanded" | grep 'Task [0-9]*/[0-9]* done:' | tail -1 || true)"
|
|
360
|
-
|
|
361
|
-
# Extract total (N/M) from last line
|
|
362
|
-
local total
|
|
363
|
-
total="$(echo "$last_line" | grep -o '[0-9]*/[0-9]*' | head -1 | cut -d/ -f2)"
|
|
364
|
-
|
|
365
|
-
# Extract last task title (between "done: " and " |")
|
|
366
|
-
local last_title
|
|
367
|
-
last_title="$(echo "$last_line" | sed 's/.*done: //;s/ |.*//')"
|
|
368
|
-
|
|
369
|
-
# Extract commit sha — 4th pipe-separated field
|
|
370
|
-
# Format: "Task N/M done: <title> | <test_count> tests | <sha> | <gate_count> gates"
|
|
371
|
-
local last_sha
|
|
372
|
-
# Use [|] character class for portable literal pipe matching across gawk and BSD awk
|
|
373
|
-
last_sha="$(echo "$last_line" | awk -F ' [|] ' '{print $3}' | sed 's/^ *//;s/ *$//')"
|
|
374
|
-
|
|
375
|
-
echo "${task_lines}/${total} tasks done | Last: ${last_title} (${last_sha})"
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
cmd_stage_transition() {
|
|
379
|
-
if [[ $# -lt 3 ]]; then
|
|
380
|
-
echo "Usage: beads-context.sh stage-transition <issue-id> <completed-stage> <next-stage> [--summary \"...\"] [--decisions \"...\"] [--artifacts \"...\"] [--next \"...\"] [--workflow-state \"{...}\"]" >&2
|
|
381
|
-
exit 1
|
|
382
|
-
fi
|
|
383
|
-
|
|
384
|
-
local issue_id="$1"
|
|
385
|
-
local completed
|
|
386
|
-
completed="$(sanitize "$2")"
|
|
387
|
-
local next
|
|
388
|
-
next="$(sanitize "$3")"
|
|
389
|
-
shift 3
|
|
390
|
-
|
|
391
|
-
# Parse optional flags
|
|
392
|
-
local flag_summary="" flag_decisions="" flag_artifacts="" flag_next="" flag_workflow_state=""
|
|
393
|
-
while [[ $# -gt 0 ]]; do
|
|
394
|
-
case "$1" in
|
|
395
|
-
--summary)
|
|
396
|
-
shift
|
|
397
|
-
flag_summary="$(sanitize "${1:-}")"
|
|
398
|
-
;;
|
|
399
|
-
--decisions)
|
|
400
|
-
shift
|
|
401
|
-
flag_decisions="$(sanitize "${1:-}")"
|
|
402
|
-
;;
|
|
403
|
-
--artifacts)
|
|
404
|
-
shift
|
|
405
|
-
flag_artifacts="$(sanitize "${1:-}")"
|
|
406
|
-
;;
|
|
407
|
-
--next)
|
|
408
|
-
shift
|
|
409
|
-
flag_next="$(sanitize "${1:-}")"
|
|
410
|
-
;;
|
|
411
|
-
--workflow-state)
|
|
412
|
-
shift
|
|
413
|
-
flag_workflow_state="$(sanitize_config_value "${1:-}")"
|
|
414
|
-
;;
|
|
415
|
-
*)
|
|
416
|
-
# Ignore unknown flags for forward compatibility
|
|
417
|
-
;;
|
|
418
|
-
esac
|
|
419
|
-
shift
|
|
420
|
-
done
|
|
421
|
-
|
|
422
|
-
# Build comment: header line always present
|
|
423
|
-
local comment="Stage: ${completed} complete → ready for ${next}"
|
|
424
|
-
|
|
425
|
-
# Append structured fields only if provided
|
|
426
|
-
if [[ -n "$flag_summary" ]]; then
|
|
427
|
-
comment="${comment}
|
|
428
|
-
Summary: ${flag_summary}"
|
|
429
|
-
fi
|
|
430
|
-
if [[ -n "$flag_decisions" ]]; then
|
|
431
|
-
comment="${comment}
|
|
432
|
-
Decisions: ${flag_decisions}"
|
|
433
|
-
fi
|
|
434
|
-
if [[ -n "$flag_artifacts" ]]; then
|
|
435
|
-
comment="${comment}
|
|
436
|
-
Artifacts: ${flag_artifacts}"
|
|
437
|
-
fi
|
|
438
|
-
if [[ -n "$flag_next" ]]; then
|
|
439
|
-
comment="${comment}
|
|
440
|
-
Next: ${flag_next}"
|
|
441
|
-
fi
|
|
442
|
-
if [[ -n "$flag_workflow_state" ]]; then
|
|
443
|
-
comment="${comment}
|
|
444
|
-
WorkflowState: ${flag_workflow_state}"
|
|
445
|
-
fi
|
|
446
|
-
|
|
447
|
-
if ! forge_comment "$issue_id" "$comment" > /dev/null; then
|
|
448
|
-
die "Failed to record stage transition on issue ${issue_id}"
|
|
449
|
-
fi
|
|
450
|
-
|
|
451
|
-
echo "Stage transition recorded on ${issue_id}: ${completed} → ${next}"
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
cmd_validate() {
|
|
455
|
-
if [[ $# -lt 1 ]]; then
|
|
456
|
-
echo "Usage: beads-context.sh validate <issue-id>" >&2
|
|
457
|
-
exit 1
|
|
458
|
-
fi
|
|
459
|
-
|
|
460
|
-
local issue_id="$1"
|
|
461
|
-
local warnings=0
|
|
462
|
-
local forge_cmd
|
|
463
|
-
forge_cmd="$(get_forge_cmd)"
|
|
464
|
-
|
|
465
|
-
# Get issue JSON
|
|
466
|
-
local json
|
|
467
|
-
json="$("$forge_cmd" issue show "$issue_id" --json 2>&1)" || {
|
|
468
|
-
echo "Error: Failed to retrieve issue ${issue_id}" >&2
|
|
469
|
-
exit 1
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
# Check for error patterns
|
|
473
|
-
if printf '%s' "$json" | grep -Eqi '^Error|Error resolving|Error fetching'; then
|
|
474
|
-
echo "Error: Issue not found: ${issue_id}" >&2
|
|
475
|
-
exit 1
|
|
476
|
-
fi
|
|
477
|
-
|
|
478
|
-
if [[ -z "$json" || "$json" == "[]" || "$json" == "null" ]]; then
|
|
479
|
-
echo "Error: Issue not found: ${issue_id}" >&2
|
|
480
|
-
exit 1
|
|
481
|
-
fi
|
|
482
|
-
|
|
483
|
-
# Extract fields — prefer jq, fall back to grep/sed
|
|
484
|
-
local description="" design=""
|
|
485
|
-
if command -v jq &>/dev/null; then
|
|
486
|
-
description="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].description else .description end // empty' 2>/dev/null || true)"
|
|
487
|
-
design="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].design else .design end // empty' 2>/dev/null || true)"
|
|
488
|
-
else
|
|
489
|
-
description="$(printf '%s' "$json" | grep -o '"description": *"[^"]*"' | head -1 | sed 's/^"description": *"//;s/"$//' || true)"
|
|
490
|
-
design="$(printf '%s' "$json" | grep -o '"design": *"[^"]*"' | head -1 | sed 's/^"design": *"//;s/"$//' || true)"
|
|
491
|
-
fi
|
|
492
|
-
|
|
493
|
-
# Check 1: Issue has description
|
|
494
|
-
if [[ -z "$description" ]]; then
|
|
495
|
-
echo "Warning: Issue ${issue_id} has no description"
|
|
496
|
-
warnings=$((warnings + 1))
|
|
497
|
-
fi
|
|
498
|
-
|
|
499
|
-
# The issue payload returned by `forge issue show --json` already includes its
|
|
500
|
-
# comment history, so the stage-transition checks read comments from that single
|
|
501
|
-
# response rather than issuing a separate comment-list query.
|
|
502
|
-
local comments
|
|
503
|
-
comments="$(extract_comments_text "$json")"
|
|
504
|
-
|
|
505
|
-
# Check 2: At least one stage transition exists
|
|
506
|
-
local has_transition=false
|
|
507
|
-
if echo "$comments" | grep -q 'Stage:.*complete'; then
|
|
508
|
-
has_transition=true
|
|
509
|
-
fi
|
|
510
|
-
|
|
511
|
-
if [[ "$has_transition" == "false" ]]; then
|
|
512
|
-
echo "Warning: No stage transition found on issue ${issue_id}"
|
|
513
|
-
warnings=$((warnings + 1))
|
|
514
|
-
fi
|
|
515
|
-
|
|
516
|
-
# Check 3: Most recent transition has summary
|
|
517
|
-
# Isolate the last transition block to avoid false-positives from earlier summaries
|
|
518
|
-
if [[ "$has_transition" == "true" ]]; then
|
|
519
|
-
local has_summary=false
|
|
520
|
-
local last_block
|
|
521
|
-
last_block="$(echo "$comments" | awk '/Stage:.*complete/{found=1; block=""} found{block=block"\n"$0} END{print block}')"
|
|
522
|
-
if echo "$last_block" | grep -q 'Summary:'; then
|
|
523
|
-
has_summary=true
|
|
524
|
-
fi
|
|
525
|
-
if [[ "$has_summary" == "false" ]]; then
|
|
526
|
-
echo "Warning: Most recent stage transition has no summary on issue ${issue_id}"
|
|
527
|
-
warnings=$((warnings + 1))
|
|
528
|
-
fi
|
|
529
|
-
fi
|
|
530
|
-
|
|
531
|
-
# Check 4: Design metadata is set if past plan stage
|
|
532
|
-
# "Past plan" means there's a transition FROM plan or any later stage
|
|
533
|
-
local past_plan=false
|
|
534
|
-
if echo "$comments" | grep -q 'Stage: plan complete'; then
|
|
535
|
-
past_plan=true
|
|
536
|
-
fi
|
|
537
|
-
if echo "$comments" | grep -Eq 'Stage: (dev|validate|ship|review|premerge) complete'; then
|
|
538
|
-
past_plan=true
|
|
539
|
-
fi
|
|
540
|
-
|
|
541
|
-
if [[ "$past_plan" == "true" && -z "$design" ]]; then
|
|
542
|
-
echo "Warning: Design metadata not set on issue ${issue_id} (past plan stage)"
|
|
543
|
-
warnings=$((warnings + 1))
|
|
544
|
-
fi
|
|
545
|
-
|
|
546
|
-
# Summary
|
|
547
|
-
if [[ $warnings -eq 0 ]]; then
|
|
548
|
-
echo "All context fields present on issue ${issue_id}"
|
|
549
|
-
else
|
|
550
|
-
echo "${warnings} warning(s) found on issue ${issue_id}"
|
|
551
|
-
fi
|
|
552
|
-
|
|
553
|
-
# Always exit 0 — advisory only
|
|
554
|
-
exit 0
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
# ── Main dispatcher ──────────────────────────────────────────────────────
|
|
558
|
-
|
|
559
|
-
if [[ $# -lt 1 ]]; then
|
|
560
|
-
usage
|
|
561
|
-
fi
|
|
562
|
-
|
|
563
|
-
subcommand="$1"
|
|
564
|
-
shift
|
|
565
|
-
|
|
566
|
-
case "$subcommand" in
|
|
567
|
-
set-design) cmd_set_design "$@" ;;
|
|
568
|
-
set-acceptance) cmd_set_acceptance "$@" ;;
|
|
569
|
-
update-progress) cmd_update_progress "$@" ;;
|
|
570
|
-
parse-progress) cmd_parse_progress "$@" ;;
|
|
571
|
-
stage-transition) cmd_stage_transition "$@" ;;
|
|
572
|
-
validate) cmd_validate "$@" ;;
|
|
573
|
-
*)
|
|
574
|
-
echo "Error: Unknown subcommand '${subcommand}'" >&2
|
|
575
|
-
usage
|
|
576
|
-
;;
|
|
577
|
-
esac
|