forge-workflow 0.0.8 → 0.0.10
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/.claude/commands/dev.md +2 -2
- package/.claude/commands/plan.md +2 -2
- package/.claude/commands/premerge.md +2 -2
- package/.claude/commands/review.md +5 -2
- package/.claude/commands/ship.md +6 -5
- package/.claude/commands/status.md +4 -4
- package/.claude/rules/greptile-review-process.md +4 -4
- package/.cline/workflows/dev.md +2 -2
- package/.cline/workflows/plan.md +2 -2
- package/.cline/workflows/premerge.md +2 -2
- package/.cline/workflows/review.md +5 -2
- package/.cline/workflows/ship.md +6 -5
- package/.cline/workflows/status.md +4 -4
- package/.codex/skills/dev/SKILL.md +2 -2
- package/.codex/skills/plan/SKILL.md +2 -2
- package/.codex/skills/premerge/SKILL.md +2 -2
- package/.codex/skills/review/SKILL.md +5 -2
- package/.codex/skills/ship/SKILL.md +6 -5
- package/.codex/skills/status/SKILL.md +4 -4
- package/.cursor/commands/dev.md +2 -2
- package/.cursor/commands/plan.md +2 -2
- package/.cursor/commands/premerge.md +2 -2
- package/.cursor/commands/review.md +5 -2
- package/.cursor/commands/ship.md +6 -5
- package/.cursor/commands/status.md +4 -4
- package/.github/prompts/dev.prompt.md +2 -2
- package/.github/prompts/plan.prompt.md +2 -2
- package/.github/prompts/premerge.prompt.md +2 -2
- package/.github/prompts/review.prompt.md +5 -2
- package/.github/prompts/ship.prompt.md +6 -5
- package/.github/prompts/status.prompt.md +4 -4
- package/.github/workflows/beads-to-github.yml +44 -11
- package/.github/workflows/github-to-beads.yml +11 -8
- package/.kilocode/workflows/dev.md +2 -2
- package/.kilocode/workflows/plan.md +2 -2
- package/.kilocode/workflows/premerge.md +2 -2
- package/.kilocode/workflows/review.md +5 -2
- package/.kilocode/workflows/ship.md +6 -5
- package/.kilocode/workflows/status.md +4 -4
- package/.opencode/commands/dev.md +2 -2
- package/.opencode/commands/plan.md +2 -2
- package/.opencode/commands/premerge.md +2 -2
- package/.opencode/commands/review.md +5 -2
- package/.opencode/commands/ship.md +6 -5
- package/.opencode/commands/status.md +4 -4
- package/.roo/commands/dev.md +2 -2
- package/.roo/commands/plan.md +2 -2
- package/.roo/commands/premerge.md +2 -2
- package/.roo/commands/review.md +5 -2
- package/.roo/commands/ship.md +6 -5
- package/.roo/commands/status.md +4 -4
- package/AGENTS.md +10 -9
- package/CLAUDE.md +12 -0
- package/README.md +12 -6
- package/bin/forge.js +24 -8
- package/docs/BEADS_GITHUB_SYNC.md +32 -2
- package/docs/EXAMPLES.md +22 -22
- package/docs/ROADMAP.md +3 -3
- package/docs/TOOLCHAIN.md +150 -160
- package/lib/agents/codex.plugin.json +3 -0
- package/lib/agents-config.js +18 -12
- package/lib/beads-bootstrap.js +225 -0
- package/lib/beads-health-check.js +55 -10
- package/lib/beads-setup.js +104 -28
- package/lib/beads-sync-scaffold.js +11 -6
- package/lib/codex-skills.js +54 -1
- package/lib/commands/_issue.js +11 -1
- package/lib/commands/issues.js +49 -0
- package/lib/commands/plan.js +5 -2
- package/lib/commands/recommend.js +22 -1
- package/lib/commands/setup.js +247 -27
- package/lib/commands/ship.js +188 -5
- package/lib/commands/status.js +187 -19
- package/lib/commands/team.js +11 -1
- package/lib/commands/test.js +125 -25
- package/lib/commands/validate.js +232 -9
- package/lib/commands/worktree.js +27 -54
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/detect-worktree.js +9 -10
- package/lib/forge-issues.js +326 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/lefthook-check.js +5 -2
- package/lib/project-memory.js +564 -0
- package/lib/runtime-health.js +100 -12
- package/lib/setup-action-log.js +2 -0
- package/lib/setup-summary-renderer.js +15 -11
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +177 -0
- package/lib/status/beads-snapshot.js +102 -0
- package/lib/status/presenter.js +65 -0
- package/lib/workflow/enforce-stage.js +14 -8
- package/lib/workflow/state-manager.js +349 -0
- package/package.json +12 -4
- package/scripts/beads-context.sh +124 -5
- package/scripts/beads-context.test.js +21 -4
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +263 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +45 -222
- package/scripts/forge-team/lib/hooks.sh +1 -1
- package/scripts/forge-team/lib/sync-github.sh +160 -28
- package/scripts/forge-team/lib/verify.sh +1 -1
- package/scripts/forge-team/lib/workload.sh +56 -27
- package/scripts/forge-team/tests/sync-github.test.sh +195 -58
- package/scripts/forge-team/tests/workload.test.sh +35 -4
- package/scripts/github-beads-sync/index.mjs +122 -98
- package/scripts/github-beads-sync/mapping.mjs +54 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
- package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
- package/scripts/github-beads-sync/run-bd.mjs +4 -2
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/preflight.sh +181 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +84 -330
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +39 -0
- package/scripts/test-ci-shard.js +244 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +335 -38
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
|
@@ -77,15 +77,38 @@ _status_icon() {
|
|
|
77
77
|
esac
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
_json_escape() {
|
|
81
|
+
local value="${1:-}"
|
|
82
|
+
value="${value//\\/\\\\}"
|
|
83
|
+
value="${value//\"/\\\"}"
|
|
84
|
+
value="${value//$'\n'/\\n}"
|
|
85
|
+
value="${value//$'\r'/\\r}"
|
|
86
|
+
value="${value//$'\t'/\\t}"
|
|
87
|
+
printf '%s' "$value"
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
# _collect_issues — Gather all open/in_progress issues with details
|
|
81
91
|
# Outputs lines: id|title|status|owner|updated|depends_on
|
|
82
92
|
_collect_issues() {
|
|
83
93
|
local bd_cmd="${BD_CMD:-bd}"
|
|
84
94
|
local list_output
|
|
85
|
-
|
|
95
|
+
local combined_exit=0
|
|
96
|
+
|
|
97
|
+
# beads 0.62 rejects comma-separated status filters, so fall back to
|
|
98
|
+
# separate queries when the combined form fails.
|
|
99
|
+
list_output="$("$bd_cmd" list --status=open,in_progress 2>/dev/null)"
|
|
100
|
+
combined_exit=$?
|
|
101
|
+
if [[ "$combined_exit" -ne 0 ]]; then
|
|
102
|
+
list_output="$(
|
|
103
|
+
{
|
|
104
|
+
"$bd_cmd" list --status=open 2>/dev/null || true
|
|
105
|
+
"$bd_cmd" list --status=in_progress 2>/dev/null || true
|
|
106
|
+
} | awk 'NF && !seen[$0]++'
|
|
107
|
+
)"
|
|
108
|
+
fi
|
|
86
109
|
|
|
87
110
|
# Filter empty lines
|
|
88
|
-
if [[ -z "$list_output" ]] || [[ -z "$(
|
|
111
|
+
if [[ -z "$list_output" ]] || [[ -z "$(printf '%s' "$list_output" | tr -d '[:space:]')" ]]; then
|
|
89
112
|
return 0
|
|
90
113
|
fi
|
|
91
114
|
|
|
@@ -95,7 +118,7 @@ _collect_issues() {
|
|
|
95
118
|
|
|
96
119
|
# Extract issue id: first field like "forge-xxx" after optional icon
|
|
97
120
|
local issue_id
|
|
98
|
-
issue_id="$(echo "$line" | grep -oE 'forge-[a-zA-Z0-9]+' | head -1)"
|
|
121
|
+
issue_id="$(echo "$line" | grep -oE 'forge-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*' | head -1)"
|
|
99
122
|
[[ -z "$issue_id" ]] && continue
|
|
100
123
|
|
|
101
124
|
# Get details via bd show
|
|
@@ -141,9 +164,10 @@ cmd_workload() {
|
|
|
141
164
|
|
|
142
165
|
# Parse arguments
|
|
143
166
|
while [[ $# -gt 0 ]]; do
|
|
144
|
-
|
|
167
|
+
local arg="${1%$'\r'}"
|
|
168
|
+
case "$arg" in
|
|
145
169
|
--developer=*)
|
|
146
|
-
filter_developer="${
|
|
170
|
+
filter_developer="${arg#--developer=}"
|
|
147
171
|
shift
|
|
148
172
|
;;
|
|
149
173
|
--me)
|
|
@@ -160,11 +184,11 @@ cmd_workload() {
|
|
|
160
184
|
shift
|
|
161
185
|
;;
|
|
162
186
|
--format=*)
|
|
163
|
-
_workload_error "Unsupported format: ${
|
|
187
|
+
_workload_error "Unsupported format: ${arg#--format=}"
|
|
164
188
|
return 1
|
|
165
189
|
;;
|
|
166
190
|
*)
|
|
167
|
-
_workload_error "Unknown argument: $
|
|
191
|
+
_workload_error "Unknown argument: $arg"
|
|
168
192
|
return 1
|
|
169
193
|
;;
|
|
170
194
|
esac
|
|
@@ -232,16 +256,13 @@ cmd_workload() {
|
|
|
232
256
|
[[ -n "$stale_flag" ]] && stale_bool="true"
|
|
233
257
|
local blocked_ids=""
|
|
234
258
|
[[ -n "$depends_on" ]] && blocked_ids="$depends_on"
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
--argjson stale "$stale_bool" \
|
|
243
|
-
--arg blocked_by "$blocked_ids" \
|
|
244
|
-
'{id: $id, title: $title, status: $status, updated: $updated, stale: $stale, blocked_by: $blocked_by}' \
|
|
259
|
+
printf '{"id":"%s","title":"%s","status":"%s","updated":"%s","stale":%s,"blocked_by":"%s"}\n' \
|
|
260
|
+
"$(_json_escape "$issue_id")" \
|
|
261
|
+
"$(_json_escape "$title")" \
|
|
262
|
+
"$(_json_escape "$status")" \
|
|
263
|
+
"$(_json_escape "$updated")" \
|
|
264
|
+
"$stale_bool" \
|
|
265
|
+
"$(_json_escape "$blocked_ids")" \
|
|
245
266
|
>> "$work_dir/json/${owner}"
|
|
246
267
|
fi
|
|
247
268
|
done <<< "$issues_data"
|
|
@@ -260,22 +281,30 @@ cmd_workload() {
|
|
|
260
281
|
# Output
|
|
261
282
|
if [[ "$format" == "json" ]]; then
|
|
262
283
|
# Build JSON object: { "devone": [...], "devtwo": [...] }
|
|
263
|
-
local
|
|
264
|
-
|
|
284
|
+
local first_dev="true"
|
|
285
|
+
printf '{'
|
|
265
286
|
for dev_file in "$work_dir/json"/*; do
|
|
266
287
|
local dev_name
|
|
267
288
|
dev_name="$(basename "$dev_file")"
|
|
268
|
-
local
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
first=false
|
|
289
|
+
local first_issue="true"
|
|
290
|
+
if [[ "$first_dev" == "true" ]]; then
|
|
291
|
+
first_dev="false"
|
|
272
292
|
else
|
|
273
|
-
|
|
293
|
+
printf ','
|
|
274
294
|
fi
|
|
275
|
-
|
|
295
|
+
printf '"%s":[' "$(_json_escape "$dev_name")"
|
|
296
|
+
while IFS= read -r issue_json; do
|
|
297
|
+
[[ -z "$issue_json" ]] && continue
|
|
298
|
+
if [[ "$first_issue" == "true" ]]; then
|
|
299
|
+
first_issue="false"
|
|
300
|
+
else
|
|
301
|
+
printf ','
|
|
302
|
+
fi
|
|
303
|
+
printf '%s' "$issue_json"
|
|
304
|
+
done < "$dev_file"
|
|
305
|
+
printf ']'
|
|
276
306
|
done
|
|
277
|
-
|
|
278
|
-
echo "$json_result"
|
|
307
|
+
printf '}\n'
|
|
279
308
|
else
|
|
280
309
|
local first_dev=true
|
|
281
310
|
for dev_file in "$work_dir/devs"/*; do
|
|
@@ -3,16 +3,17 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
5
|
|
|
6
|
-
# ── Setup temp dir for test isolation ────────────────────────────────────
|
|
7
6
|
TEST_TMP="$(mktemp -d)"
|
|
8
7
|
trap 'rm -rf "$TEST_TMP" "$mock_dir"' EXIT
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
mock_dir="$(mktemp -d)"
|
|
10
|
+
portable_bin="$(mktemp -d)"
|
|
12
11
|
log_file="$mock_dir/calls.log"
|
|
13
12
|
touch "$log_file"
|
|
13
|
+
REAL_GREP="$(command -v grep)"
|
|
14
|
+
ORIGINAL_PATH="$PATH"
|
|
15
|
+
export REAL_GREP
|
|
14
16
|
|
|
15
|
-
# ── Mock gh ──────────────────────────────────────────────────────────────
|
|
16
17
|
cat > "$mock_dir/gh" << 'MOCK'
|
|
17
18
|
#!/usr/bin/env bash
|
|
18
19
|
echo "$*" >> "$LOG_FILE"
|
|
@@ -23,45 +24,69 @@ case "$1 $2" in
|
|
|
23
24
|
"issue comment") echo ""; exit 0 ;;
|
|
24
25
|
"api user") echo "testuser"; exit 0 ;;
|
|
25
26
|
esac
|
|
26
|
-
echo "unknown gh command: $*" >&2
|
|
27
|
+
echo "unknown gh command: $*" >&2
|
|
28
|
+
exit 1
|
|
27
29
|
MOCK
|
|
28
30
|
chmod +x "$mock_dir/gh"
|
|
29
31
|
|
|
30
|
-
# ── Mock bd ──────────────────────────────────────────────────────────────
|
|
31
32
|
cat > "$mock_dir/bd" << 'MOCK'
|
|
32
33
|
#!/usr/bin/env bash
|
|
33
34
|
echo "$*" >> "$LOG_FILE"
|
|
34
35
|
case "$1" in
|
|
35
36
|
show)
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
if [[ "${3:-}" == "--json" ]]; then
|
|
38
|
+
cat <<'JSON'
|
|
39
|
+
{"id":"beads-001","github":{"number":42,"nodeId":"I_kwDOForge42","url":"https://github.com/test/repo/issues/42"},"shared":{"title":"Test Issue"}}
|
|
40
|
+
JSON
|
|
41
|
+
else
|
|
42
|
+
echo "o $2 - Test Issue"
|
|
43
|
+
echo "Title: Test Issue"
|
|
44
|
+
fi
|
|
38
45
|
;;
|
|
39
46
|
set-state) echo "State set" ;;
|
|
40
47
|
esac
|
|
41
48
|
MOCK
|
|
42
49
|
chmod +x "$mock_dir/bd"
|
|
43
50
|
|
|
44
|
-
# ── Mock bd that returns NO github_issue ─────────────────────────────────
|
|
45
51
|
cat > "$mock_dir/bd-no-gh" << 'MOCK'
|
|
46
52
|
#!/usr/bin/env bash
|
|
47
53
|
echo "$*" >> "$LOG_FILE"
|
|
48
54
|
case "$1" in
|
|
49
55
|
show)
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
if [[ "${3:-}" == "--json" ]]; then
|
|
57
|
+
cat <<'JSON'
|
|
58
|
+
{"id":"beads-999","shared":{"title":"Test Issue"}}
|
|
59
|
+
JSON
|
|
60
|
+
else
|
|
61
|
+
echo "o $2 - Test Issue"
|
|
62
|
+
echo "Title: Test Issue"
|
|
63
|
+
fi
|
|
52
64
|
;;
|
|
53
65
|
set-state) echo "State set" ;;
|
|
54
66
|
esac
|
|
55
67
|
MOCK
|
|
56
68
|
chmod +x "$mock_dir/bd-no-gh"
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
cat > "$portable_bin/grep" << 'MOCK'
|
|
71
|
+
#!/usr/bin/env bash
|
|
72
|
+
for arg in "$@"; do
|
|
73
|
+
case "$arg" in
|
|
74
|
+
-P|--perl-regexp|-*P*)
|
|
75
|
+
echo "grep: PCRE support disabled for test" >&2
|
|
76
|
+
exit 2
|
|
77
|
+
;;
|
|
78
|
+
esac
|
|
79
|
+
done
|
|
80
|
+
exec "$REAL_GREP" "$@"
|
|
81
|
+
MOCK
|
|
82
|
+
chmod +x "$portable_bin/grep"
|
|
83
|
+
|
|
59
84
|
export LOG_FILE="$log_file"
|
|
60
85
|
export GH_CMD="$mock_dir/gh"
|
|
61
86
|
export BD_CMD="$mock_dir/bd"
|
|
62
87
|
export TEAM_MAP_ROOT="$TEST_TMP"
|
|
88
|
+
mapping_file="$TEAM_MAP_ROOT/.github/beads-mapping.json"
|
|
63
89
|
|
|
64
|
-
# Source the library under test
|
|
65
90
|
source "$SCRIPT_DIR/lib/sync-github.sh"
|
|
66
91
|
|
|
67
92
|
PASS=0
|
|
@@ -70,119 +95,193 @@ FAIL=0
|
|
|
70
95
|
assert_eq() {
|
|
71
96
|
local label="$1" expected="$2" actual="$3"
|
|
72
97
|
if [[ "$actual" == "$expected" ]]; then
|
|
73
|
-
PASS=$((PASS + 1))
|
|
98
|
+
PASS=$((PASS + 1))
|
|
99
|
+
echo " PASS: $label"
|
|
74
100
|
else
|
|
75
|
-
FAIL=$((FAIL + 1))
|
|
101
|
+
FAIL=$((FAIL + 1))
|
|
102
|
+
echo " FAIL: $label (expected '$expected', got '$actual')"
|
|
76
103
|
fi
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
assert_contains() {
|
|
80
107
|
local label="$1" expected="$2" actual="$3"
|
|
81
108
|
if [[ "$actual" == *"$expected"* ]]; then
|
|
82
|
-
PASS=$((PASS + 1))
|
|
109
|
+
PASS=$((PASS + 1))
|
|
110
|
+
echo " PASS: $label"
|
|
111
|
+
else
|
|
112
|
+
FAIL=$((FAIL + 1))
|
|
113
|
+
echo " FAIL: $label (expected to contain '$expected', got '$actual')"
|
|
114
|
+
fi
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
assert_not_contains() {
|
|
118
|
+
local label="$1" unexpected="$2" actual="$3"
|
|
119
|
+
if [[ "$actual" == *"$unexpected"* ]]; then
|
|
120
|
+
FAIL=$((FAIL + 1))
|
|
121
|
+
echo " FAIL: $label (did not expect '$unexpected', got '$actual')"
|
|
83
122
|
else
|
|
84
|
-
|
|
123
|
+
PASS=$((PASS + 1))
|
|
124
|
+
echo " PASS: $label"
|
|
85
125
|
fi
|
|
86
126
|
}
|
|
87
127
|
|
|
88
128
|
assert_exit() {
|
|
89
129
|
local label="$1" expected="$2" actual="$3"
|
|
90
130
|
if [[ "$actual" -eq "$expected" ]]; then
|
|
91
|
-
PASS=$((PASS + 1))
|
|
131
|
+
PASS=$((PASS + 1))
|
|
132
|
+
echo " PASS: $label"
|
|
92
133
|
else
|
|
93
|
-
FAIL=$((FAIL + 1))
|
|
134
|
+
FAIL=$((FAIL + 1))
|
|
135
|
+
echo " FAIL: $label (expected exit $expected, got $actual)"
|
|
94
136
|
fi
|
|
95
137
|
}
|
|
96
138
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
> "$log_file" # clear log
|
|
139
|
+
echo "== Test 1: sync_issue_create =="
|
|
140
|
+
> "$log_file"
|
|
100
141
|
rc=0
|
|
101
142
|
sync_issue_create "beads-001" || rc=$?
|
|
102
143
|
assert_exit "exit code 0" 0 "$rc"
|
|
103
|
-
# Check gh was called with issue create
|
|
104
144
|
log_contents="$(cat "$log_file")"
|
|
105
145
|
assert_contains "gh issue create called" "issue create" "$log_contents"
|
|
106
146
|
assert_contains "title includes Test Issue" "Test Issue" "$log_contents"
|
|
107
147
|
assert_contains "body includes beads-001" "beads-001" "$log_contents"
|
|
108
|
-
|
|
109
|
-
assert_contains "
|
|
110
|
-
assert_contains "
|
|
148
|
+
assert_contains "sync create writes legacy github_issue state" "github_issue=42" "$log_contents"
|
|
149
|
+
assert_contains "sync create calls set-state for hook compatibility" "set-state" "$log_contents"
|
|
150
|
+
assert_contains "sync create persists mapping entry" '"42": "beads-001"' "$(cat "$mapping_file")"
|
|
111
151
|
|
|
112
|
-
|
|
113
|
-
echo ""
|
|
114
|
-
echo "── Test 2: sync_issue_claim ──"
|
|
152
|
+
echo
|
|
153
|
+
echo "== Test 2: sync_issue_claim =="
|
|
115
154
|
> "$log_file"
|
|
116
155
|
unset _GITHUB_USER_CACHE
|
|
117
156
|
rc=0
|
|
118
157
|
sync_issue_claim "beads-001" || rc=$?
|
|
119
158
|
assert_exit "exit code 0" 0 "$rc"
|
|
120
159
|
log_contents="$(cat "$log_file")"
|
|
160
|
+
assert_contains "canonical lookup uses --json" "--json" "$log_contents"
|
|
121
161
|
assert_contains "gh issue edit called" "issue edit" "$log_contents"
|
|
122
162
|
assert_contains "add-assignee used" "--add-assignee" "$log_contents"
|
|
123
163
|
assert_contains "assignee is testuser" "testuser" "$log_contents"
|
|
124
164
|
|
|
125
|
-
|
|
126
|
-
echo ""
|
|
127
|
-
echo "── Test 3: sync_issue_status ──"
|
|
165
|
+
echo
|
|
166
|
+
echo "== Test 3: sync_issue_status =="
|
|
128
167
|
> "$log_file"
|
|
129
168
|
rc=0
|
|
130
169
|
sync_issue_status "beads-001" "in_progress" || rc=$?
|
|
131
170
|
assert_exit "exit code 0" 0 "$rc"
|
|
132
171
|
log_contents="$(cat "$log_file")"
|
|
172
|
+
assert_contains "canonical lookup uses --json" "--json" "$log_contents"
|
|
133
173
|
assert_contains "gh issue edit called" "issue edit" "$log_contents"
|
|
134
174
|
assert_contains "removes status/open" "--remove-label status/open" "$log_contents"
|
|
135
175
|
assert_contains "adds status/in-progress" "--add-label status/in-progress" "$log_contents"
|
|
136
176
|
|
|
137
|
-
|
|
138
|
-
echo ""
|
|
139
|
-
echo "── Test 4: sync_issue_close ──"
|
|
177
|
+
echo
|
|
178
|
+
echo "== Test 4: sync_issue_close =="
|
|
140
179
|
> "$log_file"
|
|
141
180
|
rc=0
|
|
142
181
|
sync_issue_close "beads-001" || rc=$?
|
|
143
182
|
assert_exit "exit code 0" 0 "$rc"
|
|
144
183
|
log_contents="$(cat "$log_file")"
|
|
184
|
+
assert_contains "canonical lookup uses --json" "--json" "$log_contents"
|
|
145
185
|
assert_contains "gh issue close called" "issue close" "$log_contents"
|
|
146
186
|
assert_contains "closes issue 42" " 42" "$log_contents"
|
|
147
187
|
|
|
148
|
-
|
|
149
|
-
echo ""
|
|
150
|
-
echo "── Test 5: sync_issue_deps ──"
|
|
188
|
+
echo
|
|
189
|
+
echo "== Test 5: sync_issue_deps =="
|
|
151
190
|
> "$log_file"
|
|
152
191
|
rc=0
|
|
153
192
|
sync_issue_deps "beads-001" "beads-002" || rc=$?
|
|
154
193
|
assert_exit "exit code 0" 0 "$rc"
|
|
155
194
|
log_contents="$(cat "$log_file")"
|
|
195
|
+
assert_contains "canonical lookup uses --json" "--json" "$log_contents"
|
|
156
196
|
assert_contains "gh issue comment called" "issue comment" "$log_contents"
|
|
157
197
|
assert_contains "Blocked by referenced" "Blocked by #42" "$log_contents"
|
|
158
198
|
|
|
159
|
-
|
|
160
|
-
echo ""
|
|
161
|
-
echo "── Test 6: _get_github_issue_number ──"
|
|
199
|
+
echo
|
|
200
|
+
echo "== Test 6: _get_github_issue_number =="
|
|
162
201
|
result="$(_get_github_issue_number "beads-001")"
|
|
163
202
|
assert_eq "extracts issue number 42" "42" "$result"
|
|
203
|
+
log_contents="$(cat "$log_file")"
|
|
204
|
+
assert_contains "_get_github_issue_number uses JSON" "--json" "$log_contents"
|
|
164
205
|
|
|
165
|
-
|
|
166
|
-
echo ""
|
|
167
|
-
|
|
206
|
+
echo
|
|
207
|
+
echo "== Test 7: Mapping fallback when canonical GitHub number missing =="
|
|
208
|
+
mkdir -p "$(dirname "$mapping_file")"
|
|
209
|
+
cat > "$mapping_file" << 'JSON'
|
|
210
|
+
{
|
|
211
|
+
"77": "beads-999"
|
|
212
|
+
}
|
|
213
|
+
JSON
|
|
214
|
+
export BD_CMD="$mock_dir/bd-no-gh"
|
|
215
|
+
result="$(_get_github_issue_number "beads-999")"
|
|
216
|
+
assert_eq "mapping fallback returns issue number 77" "77" "$result"
|
|
217
|
+
export BD_CMD="$mock_dir/bd"
|
|
218
|
+
|
|
219
|
+
echo
|
|
220
|
+
echo "== Test 8: Mapping fallback returns newest issue number =="
|
|
221
|
+
cat > "$mapping_file" << 'JSON'
|
|
222
|
+
{
|
|
223
|
+
"77": "beads-999",
|
|
224
|
+
"91": "beads-999",
|
|
225
|
+
"88": "beads-other"
|
|
226
|
+
}
|
|
227
|
+
JSON
|
|
228
|
+
export BD_CMD="$mock_dir/bd-no-gh"
|
|
229
|
+
result="$(_get_github_issue_number "beads-999")"
|
|
230
|
+
assert_eq "mapping fallback returns newest matching issue number" "91" "$result"
|
|
231
|
+
export BD_CMD="$mock_dir/bd"
|
|
232
|
+
|
|
233
|
+
echo
|
|
234
|
+
echo "== Test 9: Missing canonical GitHub number =="
|
|
235
|
+
rm -f "$mapping_file"
|
|
168
236
|
export BD_CMD="$mock_dir/bd-no-gh"
|
|
169
237
|
rc=0
|
|
170
238
|
result="$(_get_github_issue_number "beads-999" 2>/dev/null)" || rc=$?
|
|
171
|
-
assert_exit "returns error when no
|
|
172
|
-
|
|
239
|
+
assert_exit "returns error when no canonical github number" 1 "$rc"
|
|
240
|
+
export BD_CMD="$mock_dir/bd"
|
|
241
|
+
|
|
242
|
+
echo
|
|
243
|
+
echo "== Test 10: Legacy github_issue state fallback =="
|
|
244
|
+
cat > "$mock_dir/bd-legacy" << 'MOCK'
|
|
245
|
+
#!/usr/bin/env bash
|
|
246
|
+
echo "$*" >> "$LOG_FILE"
|
|
247
|
+
case "$1" in
|
|
248
|
+
show)
|
|
249
|
+
if [[ "${3:-}" == "--json" ]]; then
|
|
250
|
+
cat <<'JSON'
|
|
251
|
+
{"id":"beads-legacy","github_issue":43,"shared":{"title":"Legacy Issue"}}
|
|
252
|
+
JSON
|
|
253
|
+
else
|
|
254
|
+
echo "o $2 - Legacy Issue"
|
|
255
|
+
echo "github_issue:43"
|
|
256
|
+
echo "Title: Legacy Issue"
|
|
257
|
+
fi
|
|
258
|
+
;;
|
|
259
|
+
set-state) echo "State set" ;;
|
|
260
|
+
esac
|
|
261
|
+
MOCK
|
|
262
|
+
chmod +x "$mock_dir/bd-legacy"
|
|
263
|
+
export BD_CMD="$mock_dir/bd-legacy"
|
|
264
|
+
rc=0
|
|
265
|
+
result="$(_get_github_issue_number "beads-legacy" 2>/dev/null)" || rc=$?
|
|
266
|
+
assert_exit "legacy github_issue exits successfully" 0 "$rc"
|
|
267
|
+
assert_eq "legacy github_issue returns number" "43" "$result"
|
|
173
268
|
export BD_CMD="$mock_dir/bd"
|
|
174
269
|
|
|
175
|
-
|
|
176
|
-
echo ""
|
|
177
|
-
echo "── Test 8: Injection in title sanitized ──"
|
|
178
|
-
# Create a bd mock that returns injection-laden title
|
|
270
|
+
echo
|
|
271
|
+
echo "== Test 11: Injection in title sanitized =="
|
|
179
272
|
cat > "$mock_dir/bd-inject" << 'MOCK'
|
|
180
273
|
#!/usr/bin/env bash
|
|
181
274
|
echo "$*" >> "$LOG_FILE"
|
|
182
275
|
case "$1" in
|
|
183
276
|
show)
|
|
184
|
-
|
|
185
|
-
|
|
277
|
+
if [[ "${3:-}" == "--json" ]]; then
|
|
278
|
+
cat <<'JSON'
|
|
279
|
+
{"id":"beads-inject","github":{"number":99,"url":"https://github.com/test/repo/issues/99"},"shared":{"title":"Evil $(rm -rf /) Issue"}}
|
|
280
|
+
JSON
|
|
281
|
+
else
|
|
282
|
+
echo 'o beads-inject - Evil $(rm -rf /) Issue'
|
|
283
|
+
echo 'Title: Evil $(rm -rf /) Issue'
|
|
284
|
+
fi
|
|
186
285
|
;;
|
|
187
286
|
set-state) echo "State set" ;;
|
|
188
287
|
esac
|
|
@@ -194,16 +293,54 @@ rc=0
|
|
|
194
293
|
sync_issue_create "beads-inject" || rc=$?
|
|
195
294
|
assert_exit "exit code 0 with injection" 0 "$rc"
|
|
196
295
|
log_contents="$(cat "$log_file")"
|
|
197
|
-
# The $(...) pattern must NOT appear in the gh call
|
|
198
296
|
if [[ "$log_contents" == *'$(rm'* ]]; then
|
|
199
|
-
FAIL=$((FAIL + 1))
|
|
297
|
+
FAIL=$((FAIL + 1))
|
|
298
|
+
echo " FAIL: injection NOT sanitized - \$\(rm found in gh call"
|
|
200
299
|
else
|
|
201
|
-
PASS=$((PASS + 1))
|
|
300
|
+
PASS=$((PASS + 1))
|
|
301
|
+
echo " PASS: injection sanitized - no \$\(rm in gh call"
|
|
202
302
|
fi
|
|
203
|
-
# Restore normal bd mock
|
|
204
303
|
export BD_CMD="$mock_dir/bd"
|
|
205
304
|
|
|
206
|
-
|
|
207
|
-
echo ""
|
|
305
|
+
echo
|
|
306
|
+
echo "== Test 11: sync_issue_create without grep -P =="
|
|
307
|
+
export PATH="$portable_bin:$ORIGINAL_PATH"
|
|
308
|
+
> "$log_file"
|
|
309
|
+
rc=0
|
|
310
|
+
sync_issue_create "beads-001" || rc=$?
|
|
311
|
+
assert_exit "sync_issue_create works when grep -P is unavailable" 0 "$rc"
|
|
312
|
+
log_contents="$(cat "$log_file")"
|
|
313
|
+
assert_contains "gh issue create called without PCRE grep" "issue create" "$log_contents"
|
|
314
|
+
export PATH="$ORIGINAL_PATH"
|
|
315
|
+
|
|
316
|
+
echo
|
|
317
|
+
echo "== Test 12: sync_issue_create with summary-only bd show =="
|
|
318
|
+
cat > "$mock_dir/bd-summary-only" << 'MOCK'
|
|
319
|
+
#!/usr/bin/env bash
|
|
320
|
+
echo "$*" >> "$LOG_FILE"
|
|
321
|
+
case "$1" in
|
|
322
|
+
show)
|
|
323
|
+
if [[ "${3:-}" == "--json" ]]; then
|
|
324
|
+
cat <<'JSON'
|
|
325
|
+
{"id":"beads-summary","github":{"number":77,"url":"https://github.com/test/repo/issues/77"},"shared":{"title":"Summary Only Title"}}
|
|
326
|
+
JSON
|
|
327
|
+
else
|
|
328
|
+
echo "o $2 - Summary Only Title"
|
|
329
|
+
fi
|
|
330
|
+
;;
|
|
331
|
+
set-state) echo "State set" ;;
|
|
332
|
+
esac
|
|
333
|
+
MOCK
|
|
334
|
+
chmod +x "$mock_dir/bd-summary-only"
|
|
335
|
+
export BD_CMD="$mock_dir/bd-summary-only"
|
|
336
|
+
> "$log_file"
|
|
337
|
+
rc=0
|
|
338
|
+
sync_issue_create "beads-summary" || rc=$?
|
|
339
|
+
assert_exit "sync_issue_create works with summary-only bd show" 0 "$rc"
|
|
340
|
+
log_contents="$(cat "$log_file")"
|
|
341
|
+
assert_contains "summary-only title is passed to gh issue create" "Summary Only Title" "$log_contents"
|
|
342
|
+
export BD_CMD="$mock_dir/bd"
|
|
343
|
+
|
|
344
|
+
echo
|
|
208
345
|
echo "Results: $PASS passed, $FAIL failed"
|
|
209
346
|
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|
|
@@ -32,9 +32,15 @@ cat > "$mock_dir/bd" << 'MOCK'
|
|
|
32
32
|
#!/usr/bin/env bash
|
|
33
33
|
case "$1 $2" in
|
|
34
34
|
"list --status=open,in_progress")
|
|
35
|
-
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
"list --status=open")
|
|
36
38
|
echo "○ forge-bbb · Feature B"
|
|
39
|
+
;;
|
|
40
|
+
"list --status=in_progress")
|
|
41
|
+
echo "◐ forge-aaa · Feature A"
|
|
37
42
|
echo "◐ forge-ccc · Feature C"
|
|
43
|
+
echo "◐ forge-m1n8.6 · Sub-feature X (dotted ID)"
|
|
38
44
|
;;
|
|
39
45
|
"show forge-aaa")
|
|
40
46
|
echo "◐ forge-aaa · Feature A [● P2 · IN_PROGRESS]"
|
|
@@ -54,6 +60,19 @@ case "$1 $2" in
|
|
|
54
60
|
echo "DEPENDS ON"
|
|
55
61
|
echo " → forge-aaa: Feature A"
|
|
56
62
|
;;
|
|
63
|
+
"show forge-m1n8.6")
|
|
64
|
+
# Dotted sub-ID — bd show must receive the full ID including .6
|
|
65
|
+
echo "◐ forge-m1n8.6 · Sub-feature X [● P2 · IN_PROGRESS]"
|
|
66
|
+
echo "Owner: devthree"
|
|
67
|
+
echo "Updated: 2026-03-27T08:00:00Z"
|
|
68
|
+
;;
|
|
69
|
+
"show forge-m1n8")
|
|
70
|
+
# Parent epic — if a buggy parser truncates forge-m1n8.6 to forge-m1n8,
|
|
71
|
+
# the workload would get this WRONG owner and trigger the guard below.
|
|
72
|
+
echo "◐ forge-m1n8 · PARENT EPIC (WRONG — dotted ID was truncated)"
|
|
73
|
+
echo "Owner: WRONG_OWNER_DO_NOT_USE"
|
|
74
|
+
echo "Updated: 2026-03-20T10:00:00Z"
|
|
75
|
+
;;
|
|
57
76
|
esac
|
|
58
77
|
MOCK
|
|
59
78
|
chmod +x "$mock_dir/bd"
|
|
@@ -65,6 +84,12 @@ case "$1 $2" in
|
|
|
65
84
|
"list --status=open,in_progress")
|
|
66
85
|
echo ""
|
|
67
86
|
;;
|
|
87
|
+
"list --status=open")
|
|
88
|
+
echo "○ forge-should-not-appear · Open fallback sentinel"
|
|
89
|
+
;;
|
|
90
|
+
"list --status=in_progress")
|
|
91
|
+
echo "◐ forge-should-not-appear-2 · In-progress fallback sentinel"
|
|
92
|
+
;;
|
|
68
93
|
esac
|
|
69
94
|
MOCK
|
|
70
95
|
chmod +x "$mock_dir/bd-empty"
|
|
@@ -125,6 +150,11 @@ assert_contains "shows devtwo header" "Developer: devtwo" "$output"
|
|
|
125
150
|
assert_contains "shows forge-aaa" "forge-aaa" "$output"
|
|
126
151
|
assert_contains "shows forge-bbb" "forge-bbb" "$output"
|
|
127
152
|
assert_contains "shows forge-ccc" "forge-ccc" "$output"
|
|
153
|
+
# Dotted ID regression (forge-hpev): the full forge-m1n8.6 must be extracted,
|
|
154
|
+
# not truncated to forge-m1n8. devthree owns the sub-issue, not WRONG_OWNER.
|
|
155
|
+
assert_contains "shows devthree (dotted ID owner)" "Developer: devthree" "$output"
|
|
156
|
+
assert_contains "shows dotted ID forge-m1n8.6 in full" "forge-m1n8.6" "$output"
|
|
157
|
+
assert_not_contains "does NOT attribute to WRONG_OWNER (parent epic)" "WRONG_OWNER" "$output"
|
|
128
158
|
|
|
129
159
|
# ── Test 2: --developer=devone filters correctly ─────────────────────────
|
|
130
160
|
echo ""
|
|
@@ -158,6 +188,7 @@ rc=0
|
|
|
158
188
|
output="$(cmd_workload 2>/dev/null)" || rc=$?
|
|
159
189
|
assert_exit "exits 0" 0 "$rc"
|
|
160
190
|
assert_contains "shows no active work message" "No active work" "$output"
|
|
191
|
+
assert_not_contains "does not fall back when combined query succeeds with no issues" "forge-should-not-appear" "$output"
|
|
161
192
|
export BD_CMD="$mock_dir/bd"
|
|
162
193
|
|
|
163
194
|
# ── Test 5: Stale assignment flagged (>48h) ──────────────────────────────
|
|
@@ -178,18 +209,18 @@ rc=0
|
|
|
178
209
|
output="$(cmd_workload --format=json 2>/dev/null)" || rc=$?
|
|
179
210
|
assert_exit "exits 0" 0 "$rc"
|
|
180
211
|
# Validate it's parseable JSON
|
|
181
|
-
if echo "$output" |
|
|
212
|
+
if echo "$output" | node -e 'JSON.parse(require("fs").readFileSync(0, "utf8"));' >/dev/null 2>&1; then
|
|
182
213
|
PASS=$((PASS + 1)); echo " PASS: output is valid JSON"
|
|
183
214
|
else
|
|
184
215
|
FAIL=$((FAIL + 1)); echo " FAIL: output is not valid JSON: $output"
|
|
185
216
|
fi
|
|
186
217
|
# Check JSON has developer keys
|
|
187
|
-
if echo "$output" |
|
|
218
|
+
if echo "$output" | node -e 'const data = JSON.parse(require("fs").readFileSync(0, "utf8")); process.exit(data.devone ? 0 : 1);' >/dev/null 2>&1; then
|
|
188
219
|
PASS=$((PASS + 1)); echo " PASS: JSON contains devone key"
|
|
189
220
|
else
|
|
190
221
|
FAIL=$((FAIL + 1)); echo " FAIL: JSON missing devone key"
|
|
191
222
|
fi
|
|
192
|
-
if echo "$output" |
|
|
223
|
+
if echo "$output" | node -e 'const data = JSON.parse(require("fs").readFileSync(0, "utf8")); process.exit(data.devtwo ? 0 : 1);' >/dev/null 2>&1; then
|
|
193
224
|
PASS=$((PASS + 1)); echo " PASS: JSON contains devtwo key"
|
|
194
225
|
else
|
|
195
226
|
FAIL=$((FAIL + 1)); echo " FAIL: JSON missing devtwo key"
|