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
package/scripts/beads-context.sh
CHANGED
|
@@ -41,11 +41,124 @@ die() {
|
|
|
41
41
|
exit 1
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
# Resolve a Windows bd.exe installation for bash-based helper flows.
|
|
45
|
+
# This covers WSL/Git Bash cases where PowerShell can run bd.exe but bash PATH
|
|
46
|
+
# does not include the Windows install directory.
|
|
47
|
+
convert_windows_path() {
|
|
48
|
+
local raw="${1%$'\r'}"
|
|
49
|
+
|
|
50
|
+
if [[ -z "$raw" ]]; then
|
|
51
|
+
return 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [[ ! "$raw" =~ ^[A-Za-z]:\\ ]]; then
|
|
55
|
+
printf '%s' "$raw"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if command -v wslpath >/dev/null 2>&1; then
|
|
60
|
+
wslpath -u "$raw" 2>/dev/null && return 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
64
|
+
cygpath -u "$raw" 2>/dev/null && return 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
local drive rest
|
|
68
|
+
drive="$(printf '%s' "$raw" | cut -c1 | tr '[:upper:]' '[:lower:]')"
|
|
69
|
+
rest="${raw:2}"
|
|
70
|
+
rest="${rest//\\//}"
|
|
71
|
+
printf '/mnt/%s%s' "$drive" "$rest"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
is_runnable_bd_candidate() {
|
|
75
|
+
local candidate="${1:-}"
|
|
76
|
+
|
|
77
|
+
if [[ -z "$candidate" || ! -f "$candidate" ]]; then
|
|
78
|
+
return 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
[[ -x "$candidate" || "$candidate" == *.exe ]]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resolve_bd_cmd() {
|
|
85
|
+
local candidate=""
|
|
86
|
+
local converted=""
|
|
87
|
+
|
|
88
|
+
if [[ -n "${BD_CMD:-}" ]]; then
|
|
89
|
+
if [[ "${BD_CMD}" == *"/"* || "${BD_CMD}" == *"\\"* ]]; then
|
|
90
|
+
converted="$(convert_windows_path "$BD_CMD")"
|
|
91
|
+
if is_runnable_bd_candidate "$converted"; then
|
|
92
|
+
printf '%s' "$converted"
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
is_runnable_bd_candidate "$BD_CMD" || return 1
|
|
97
|
+
fi
|
|
98
|
+
printf '%s' "$BD_CMD"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if command -v bd >/dev/null 2>&1; then
|
|
103
|
+
printf '%s' "bd"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if command -v bd.exe >/dev/null 2>&1; then
|
|
108
|
+
printf '%s' "bd.exe"
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
for candidate in \
|
|
113
|
+
"$HOME/.local/bin/bd" \
|
|
114
|
+
"$HOME/.local/bin/bd.exe" \
|
|
115
|
+
"$HOME/.bun/bin/bd" \
|
|
116
|
+
"$HOME/.bun/bin/bd.exe"
|
|
117
|
+
do
|
|
118
|
+
if is_runnable_bd_candidate "$candidate"; then
|
|
119
|
+
printf '%s' "$candidate"
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
|
|
124
|
+
if command -v where.exe >/dev/null 2>&1; then
|
|
125
|
+
while IFS= read -r candidate; do
|
|
126
|
+
candidate="${candidate%$'\r'}"
|
|
127
|
+
[[ -z "$candidate" ]] && continue
|
|
128
|
+
|
|
129
|
+
converted="$(convert_windows_path "$candidate")"
|
|
130
|
+
if is_runnable_bd_candidate "$converted"; then
|
|
131
|
+
printf '%s' "$converted"
|
|
132
|
+
return 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if is_runnable_bd_candidate "$candidate"; then
|
|
136
|
+
printf '%s' "$candidate"
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
done < <(where.exe bd 2>/dev/null || true)
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
return 1
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
BD=""
|
|
146
|
+
|
|
147
|
+
get_bd_cmd() {
|
|
148
|
+
if [[ -z "$BD" ]]; then
|
|
149
|
+
BD="$(resolve_bd_cmd)" || die "bd is required but not found"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
printf '%s' "$BD"
|
|
153
|
+
}
|
|
154
|
+
|
|
44
155
|
# Run bd update and check for errors in both exit code and output.
|
|
45
156
|
# bd update exits 0 even for non-existent issues, so we check stdout too.
|
|
46
157
|
bd_update() {
|
|
158
|
+
local bd_cmd
|
|
159
|
+
bd_cmd="$(get_bd_cmd)"
|
|
47
160
|
local output
|
|
48
|
-
output="$(
|
|
161
|
+
output="$("$bd_cmd" update "$@" 2>&1)"
|
|
49
162
|
local rc=$?
|
|
50
163
|
|
|
51
164
|
if [[ $rc -ne 0 ]]; then
|
|
@@ -66,8 +179,10 @@ bd_update() {
|
|
|
66
179
|
|
|
67
180
|
# Run bd comments add and check for errors similarly.
|
|
68
181
|
bd_comment() {
|
|
182
|
+
local bd_cmd
|
|
183
|
+
bd_cmd="$(get_bd_cmd)"
|
|
69
184
|
local output
|
|
70
|
-
output="$(
|
|
185
|
+
output="$("$bd_cmd" comments add "$@" 2>&1)"
|
|
71
186
|
local rc=$?
|
|
72
187
|
|
|
73
188
|
if [[ $rc -ne 0 ]]; then
|
|
@@ -172,10 +287,12 @@ cmd_parse_progress() {
|
|
|
172
287
|
fi
|
|
173
288
|
|
|
174
289
|
local issue_id="$1"
|
|
290
|
+
local bd_cmd
|
|
291
|
+
bd_cmd="$(get_bd_cmd)"
|
|
175
292
|
|
|
176
293
|
# Get the issue JSON — detect non-existent issues
|
|
177
294
|
local json
|
|
178
|
-
json="$(
|
|
295
|
+
json="$("$bd_cmd" show "$issue_id" --json 2>&1)" || die "Failed to show issue ${issue_id}"
|
|
179
296
|
|
|
180
297
|
# bd show may exit 0 but print an error for non-existent issues
|
|
181
298
|
# Match specific bd error patterns, not the word "error" in data fields
|
|
@@ -320,10 +437,12 @@ cmd_validate() {
|
|
|
320
437
|
|
|
321
438
|
local issue_id="$1"
|
|
322
439
|
local warnings=0
|
|
440
|
+
local bd_cmd
|
|
441
|
+
bd_cmd="$(get_bd_cmd)"
|
|
323
442
|
|
|
324
443
|
# Get issue JSON
|
|
325
444
|
local json
|
|
326
|
-
json="$(
|
|
445
|
+
json="$("$bd_cmd" show "$issue_id" --json 2>&1)" || {
|
|
327
446
|
echo "Error: Failed to retrieve issue ${issue_id}" >&2
|
|
328
447
|
exit 1
|
|
329
448
|
}
|
|
@@ -357,7 +476,7 @@ cmd_validate() {
|
|
|
357
476
|
|
|
358
477
|
# Get comments to check for stage transitions
|
|
359
478
|
local comments
|
|
360
|
-
comments="$(
|
|
479
|
+
comments="$("$bd_cmd" comments "$issue_id" 2>/dev/null || true)"
|
|
361
480
|
|
|
362
481
|
# Check 2: At least one stage transition exists
|
|
363
482
|
local has_transition=false
|
|
@@ -22,16 +22,24 @@ function isBdAvailable() {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function shouldRunBeadsIntegration() {
|
|
26
|
+
return process.env.RUN_BEADS_INTEGRATION === '1' && isBdAvailable();
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
/**
|
|
26
30
|
* Helper: run the beads-context.sh script with given args.
|
|
27
31
|
* Returns { exitCode, stdout, stderr }.
|
|
28
32
|
*/
|
|
29
33
|
async function run(...args) {
|
|
34
|
+
return runWithEnv(args);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function runWithEnv(args, envOverrides = {}) {
|
|
30
38
|
const proc = Bun.spawn([resolveBashCommand(), SCRIPT_PATH, ...args], {
|
|
31
39
|
cwd: WORKTREE_ROOT,
|
|
32
40
|
stdout: 'pipe',
|
|
33
41
|
stderr: 'pipe',
|
|
34
|
-
env: { ...process.env },
|
|
42
|
+
env: { ...process.env, ...envOverrides },
|
|
35
43
|
});
|
|
36
44
|
const stdout = await new Response(proc.stdout).text();
|
|
37
45
|
const stderr = await new Response(proc.stderr).text();
|
|
@@ -55,7 +63,16 @@ async function bd(...args) {
|
|
|
55
63
|
return { exitCode, stdout, stderr };
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
describe
|
|
66
|
+
describe('scripts/beads-context.sh command resolution', () => {
|
|
67
|
+
test('script includes Windows-aware bd resolution fallback', () => {
|
|
68
|
+
const content = fs.readFileSync(SCRIPT_PATH, 'utf8');
|
|
69
|
+
expect(content).toContain('resolve_bd_cmd');
|
|
70
|
+
expect(content).toContain('where.exe');
|
|
71
|
+
expect(content).toContain('$HOME/.local/bin/bd.exe');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe.skipIf(!shouldRunBeadsIntegration())('scripts/beads-context.sh', () => {
|
|
59
76
|
let testIssueId;
|
|
60
77
|
|
|
61
78
|
// Create a temporary test issue for isolation
|
|
@@ -470,7 +487,7 @@ describe('/dev command integration with beads-context.sh', () => {
|
|
|
470
487
|
});
|
|
471
488
|
});
|
|
472
489
|
|
|
473
|
-
describe('/status command integration with smart-status
|
|
490
|
+
describe('/status command integration with smart-status', () => {
|
|
474
491
|
const STATUS_MD_PATH = path.join(
|
|
475
492
|
__dirname,
|
|
476
493
|
'..',
|
|
@@ -485,7 +502,7 @@ describe('/status command integration with smart-status.sh', () => {
|
|
|
485
502
|
});
|
|
486
503
|
|
|
487
504
|
test('status.md should reference smart-status.sh as the primary status command', () => {
|
|
488
|
-
expect(statusContent).toContain('smart-status.sh');
|
|
505
|
+
expect(statusContent).toContain('bash scripts/smart-status.sh');
|
|
489
506
|
});
|
|
490
507
|
|
|
491
508
|
test('status.md should include hint to use bd show for full context', () => {
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
BD_CMD="${BD_CMD:-bd}"
|
|
5
|
+
NODE_CMD="${NODE_CMD:-node}"
|
|
6
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}"
|
|
7
|
+
ARTIFACT_DIR="${BEADS_UPGRADE_SMOKE_ARTIFACT_DIR:-${PROJECT_ROOT}/.artifacts/beads-upgrade-smoke}"
|
|
8
|
+
COMMAND_LOG_PATH="${ARTIFACT_DIR}/commands.jsonl"
|
|
9
|
+
SUMMARY_PATH="${ARTIFACT_DIR}/summary.json"
|
|
10
|
+
CLEANUP_REASON="Beads upgrade smoke cleanup"
|
|
11
|
+
SMOKE_RUN_ID="${BEADS_UPGRADE_SMOKE_RUN_ID:-$(date +%s)-$$}"
|
|
12
|
+
PRIMARY_ISSUE_TITLE="Beads upgrade smoke primary (${SMOKE_RUN_ID})"
|
|
13
|
+
DEPENDENT_ISSUE_TITLE="Beads upgrade smoke dependent (${SMOKE_RUN_ID})"
|
|
14
|
+
|
|
15
|
+
mkdir -p "${ARTIFACT_DIR}"
|
|
16
|
+
: > "${COMMAND_LOG_PATH}"
|
|
17
|
+
|
|
18
|
+
created_issue_ids=()
|
|
19
|
+
closed_issue_ids=()
|
|
20
|
+
failed_step=""
|
|
21
|
+
failure_message=""
|
|
22
|
+
|
|
23
|
+
parse_issue_id() {
|
|
24
|
+
local output="$1"
|
|
25
|
+
if [[ "${output}" =~ Created[[:space:]]+issue:[[:space:]]*([[:alnum:]-]+) ]]; then
|
|
26
|
+
printf '%s' "${BASH_REMATCH[1]}"
|
|
27
|
+
return 0
|
|
28
|
+
fi
|
|
29
|
+
if [[ "${output}" =~ \"id\"[[:space:]]*:[[:space:]]*\"([[:alnum:]-]+)\" ]]; then
|
|
30
|
+
printf '%s' "${BASH_REMATCH[1]}"
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
return 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
find_issue_id_by_title() {
|
|
37
|
+
local issue_title="$1"
|
|
38
|
+
local list_output
|
|
39
|
+
list_output="$("${BD_CMD}" list --json --limit=50 2>/dev/null)" || return 1
|
|
40
|
+
|
|
41
|
+
"${NODE_CMD}" -e '
|
|
42
|
+
const fs = require("node:fs");
|
|
43
|
+
const [issueTitle] = process.argv.slice(1);
|
|
44
|
+
try {
|
|
45
|
+
const rows = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
46
|
+
if (!Array.isArray(rows)) {
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const match = rows.find((row) => row && row.title === issueTitle && typeof row.id === "string");
|
|
50
|
+
if (!match) {
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write(match.id);
|
|
54
|
+
} catch {
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
' "${issue_title}" <<< "${list_output}"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
append_command() {
|
|
61
|
+
local step="$1"
|
|
62
|
+
local command_name="$2"
|
|
63
|
+
local command_text="$3"
|
|
64
|
+
local exit_code="$4"
|
|
65
|
+
local stdout_path="$5"
|
|
66
|
+
local stderr_path="$6"
|
|
67
|
+
|
|
68
|
+
"${NODE_CMD}" -e '
|
|
69
|
+
const fs = require("node:fs");
|
|
70
|
+
const [logPath, step, commandName, commandText, exitCode, stdoutPath, stderrPath] = process.argv.slice(1);
|
|
71
|
+
const entry = {
|
|
72
|
+
step,
|
|
73
|
+
commandName,
|
|
74
|
+
commandText,
|
|
75
|
+
exitCode: Number(exitCode),
|
|
76
|
+
stdoutPath,
|
|
77
|
+
stderrPath,
|
|
78
|
+
};
|
|
79
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
80
|
+
' "${COMMAND_LOG_PATH}" "${step}" "${command_name}" "${command_text}" "${exit_code}" "${stdout_path}" "${stderr_path}"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
read_output_snippet() {
|
|
84
|
+
local file_path="$1"
|
|
85
|
+
if [[ ! -f "${file_path}" ]]; then
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
"${NODE_CMD}" -e '
|
|
90
|
+
const fs = require("node:fs");
|
|
91
|
+
const [filePath] = process.argv.slice(1);
|
|
92
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
93
|
+
process.stdout.write(text.slice(0, 4096));
|
|
94
|
+
' "${file_path}"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
write_summary() {
|
|
98
|
+
local ok="$1"
|
|
99
|
+
|
|
100
|
+
"${NODE_CMD}" -e '
|
|
101
|
+
const fs = require("node:fs");
|
|
102
|
+
const [
|
|
103
|
+
commandLogPath,
|
|
104
|
+
summaryPath,
|
|
105
|
+
okValue,
|
|
106
|
+
failedStep,
|
|
107
|
+
failureMessage,
|
|
108
|
+
artifactDir,
|
|
109
|
+
createdJson,
|
|
110
|
+
closedJson,
|
|
111
|
+
] = process.argv.slice(1);
|
|
112
|
+
const commands = fs.readFileSync(commandLogPath, "utf8")
|
|
113
|
+
.split(/\r?\n/)
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map((line) => JSON.parse(line));
|
|
116
|
+
const summary = {
|
|
117
|
+
ok: okValue === "true",
|
|
118
|
+
failedStep: failedStep || null,
|
|
119
|
+
failureMessage: failureMessage || null,
|
|
120
|
+
commands,
|
|
121
|
+
cleanup: {
|
|
122
|
+
createdIssueIds: JSON.parse(createdJson),
|
|
123
|
+
closedIssueIds: JSON.parse(closedJson),
|
|
124
|
+
},
|
|
125
|
+
failureArtifact: summaryPath,
|
|
126
|
+
artifactDir,
|
|
127
|
+
};
|
|
128
|
+
fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + "\n");
|
|
129
|
+
' "${COMMAND_LOG_PATH}" "${SUMMARY_PATH}" "${ok}" "${failed_step}" "${failure_message}" "${ARTIFACT_DIR}" "$(printf '%s\n' "${created_issue_ids[@]:-}" | "${NODE_CMD}" -e 'const fs=require("node:fs"); const lines=fs.readFileSync(0,"utf8").split(/\r?\n/).filter(Boolean); process.stdout.write(JSON.stringify(lines));')" "$(printf '%s\n' "${closed_issue_ids[@]:-}" | "${NODE_CMD}" -e 'const fs=require("node:fs"); const lines=fs.readFileSync(0,"utf8").split(/\r?\n/).filter(Boolean); process.stdout.write(JSON.stringify(lines));')"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
run_bd_step() {
|
|
133
|
+
local step="$1"
|
|
134
|
+
local command_name="$2"
|
|
135
|
+
shift 2
|
|
136
|
+
|
|
137
|
+
local stdout_file stderr_file exit_code command_text
|
|
138
|
+
stdout_file="${ARTIFACT_DIR}/$(printf '%s' "${step}" | tr '[:upper:]' '[:lower:]').stdout"
|
|
139
|
+
stderr_file="${ARTIFACT_DIR}/$(printf '%s' "${step}" | tr '[:upper:]' '[:lower:]').stderr"
|
|
140
|
+
command_text="$*"
|
|
141
|
+
|
|
142
|
+
if "${BD_CMD}" "$@" >"${stdout_file}" 2>"${stderr_file}"; then
|
|
143
|
+
exit_code=0
|
|
144
|
+
else
|
|
145
|
+
exit_code=$?
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
append_command "${step}" "${command_name}" "${command_text}" "${exit_code}" "${stdout_file}" "${stderr_file}"
|
|
149
|
+
|
|
150
|
+
if [[ "${exit_code}" -ne 0 ]]; then
|
|
151
|
+
failed_step="${command_name}"
|
|
152
|
+
local stderr_text stdout_text
|
|
153
|
+
stderr_text="$(read_output_snippet "${stderr_file}")"
|
|
154
|
+
stdout_text="$(read_output_snippet "${stdout_file}")"
|
|
155
|
+
failure_message="${stderr_text:-${stdout_text:-${command_text}}}"
|
|
156
|
+
return "${exit_code}"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
cat "${stdout_file}"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
is_closed_issue() {
|
|
163
|
+
local issue_id="$1"
|
|
164
|
+
local existing
|
|
165
|
+
for existing in "${closed_issue_ids[@]:-}"; do
|
|
166
|
+
if [[ "${existing}" == "${issue_id}" ]]; then
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
done
|
|
170
|
+
return 1
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
rollback_unclosed_issues() {
|
|
174
|
+
local idx issue_id
|
|
175
|
+
for (( idx=${#created_issue_ids[@]} - 1; idx>=0; idx-- )); do
|
|
176
|
+
issue_id="${created_issue_ids[idx]}"
|
|
177
|
+
if is_closed_issue "${issue_id}"; then
|
|
178
|
+
continue
|
|
179
|
+
fi
|
|
180
|
+
if run_bd_step "rollback-close-${issue_id}" "close" close "${issue_id}" "--reason=${CLEANUP_REASON}" >/dev/null; then
|
|
181
|
+
closed_issue_ids+=("${issue_id}")
|
|
182
|
+
fi
|
|
183
|
+
done
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
handle_issue_id_parse_failure() {
|
|
187
|
+
local issue_title="$1"
|
|
188
|
+
local message="$2"
|
|
189
|
+
local recovered_issue_id=""
|
|
190
|
+
|
|
191
|
+
failed_step="create"
|
|
192
|
+
failure_message="${message}"
|
|
193
|
+
|
|
194
|
+
recovered_issue_id="$(find_issue_id_by_title "${issue_title}" || true)"
|
|
195
|
+
if [[ -n "${recovered_issue_id}" ]]; then
|
|
196
|
+
created_issue_ids+=("${recovered_issue_id}")
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
rollback_unclosed_issues
|
|
200
|
+
write_summary "false"
|
|
201
|
+
exit 1
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
primary_output="$(run_bd_step "create-primary" "create" create --title="${PRIMARY_ISSUE_TITLE}" --type=task --priority=4)" || {
|
|
205
|
+
rollback_unclosed_issues
|
|
206
|
+
write_summary "false"
|
|
207
|
+
exit 1
|
|
208
|
+
}
|
|
209
|
+
primary_issue_id="$(parse_issue_id "${primary_output}")" || {
|
|
210
|
+
handle_issue_id_parse_failure "${PRIMARY_ISSUE_TITLE}" "Could not parse primary smoke issue ID"
|
|
211
|
+
}
|
|
212
|
+
created_issue_ids+=("${primary_issue_id}")
|
|
213
|
+
|
|
214
|
+
dependent_output="$(run_bd_step "create-dependent" "create" create --title="${DEPENDENT_ISSUE_TITLE}" --type=task --priority=4)" || {
|
|
215
|
+
rollback_unclosed_issues
|
|
216
|
+
write_summary "false"
|
|
217
|
+
exit 1
|
|
218
|
+
}
|
|
219
|
+
dependent_issue_id="$(parse_issue_id "${dependent_output}")" || {
|
|
220
|
+
handle_issue_id_parse_failure "${DEPENDENT_ISSUE_TITLE}" "Could not parse dependent smoke issue ID"
|
|
221
|
+
}
|
|
222
|
+
created_issue_ids+=("${dependent_issue_id}")
|
|
223
|
+
|
|
224
|
+
run_bd_step "list" "list" list --json --limit=50 >/dev/null || {
|
|
225
|
+
rollback_unclosed_issues
|
|
226
|
+
write_summary "false"
|
|
227
|
+
exit 1
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
run_bd_step "show-primary" "show" show "${primary_issue_id}" --json >/dev/null || {
|
|
231
|
+
rollback_unclosed_issues
|
|
232
|
+
write_summary "false"
|
|
233
|
+
exit 1
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
run_bd_step "dep-add" "dep" dep add "${primary_issue_id}" "${dependent_issue_id}" >/dev/null || {
|
|
237
|
+
rollback_unclosed_issues
|
|
238
|
+
write_summary "false"
|
|
239
|
+
exit 1
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
run_bd_step "close-dependent" "close" close "${dependent_issue_id}" "--reason=${CLEANUP_REASON}" >/dev/null || {
|
|
243
|
+
rollback_unclosed_issues
|
|
244
|
+
write_summary "false"
|
|
245
|
+
exit 1
|
|
246
|
+
}
|
|
247
|
+
closed_issue_ids+=("${dependent_issue_id}")
|
|
248
|
+
|
|
249
|
+
run_bd_step "close-primary" "close" close "${primary_issue_id}" "--reason=${CLEANUP_REASON}" >/dev/null || {
|
|
250
|
+
rollback_unclosed_issues
|
|
251
|
+
write_summary "false"
|
|
252
|
+
exit 1
|
|
253
|
+
}
|
|
254
|
+
closed_issue_ids+=("${primary_issue_id}")
|
|
255
|
+
|
|
256
|
+
run_bd_step "sync" "sync" sync >/dev/null || {
|
|
257
|
+
rollback_unclosed_issues
|
|
258
|
+
write_summary "false"
|
|
259
|
+
exit 1
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
write_summary "true"
|
|
263
|
+
printf '%s\n' "${SUMMARY_PATH}"
|
|
@@ -66,10 +66,25 @@ else
|
|
|
66
66
|
INPUT="$(cat)"
|
|
67
67
|
fi
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
extract_plan_text() {
|
|
70
|
+
if command -v node >/dev/null 2>&1; then
|
|
71
|
+
node -e "
|
|
72
|
+
let input = '';
|
|
73
|
+
process.stdin.setEncoding('utf8');
|
|
74
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
75
|
+
process.stdin.on('end', () => {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(input);
|
|
78
|
+
const value = parsed && parsed.plan_output;
|
|
79
|
+
if (value) process.stdout.write(String(value));
|
|
80
|
+
} catch (_) {}
|
|
81
|
+
});
|
|
82
|
+
" 2>/dev/null
|
|
83
|
+
return $?
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
87
|
+
python3 -c "
|
|
73
88
|
import sys, json
|
|
74
89
|
try:
|
|
75
90
|
d = json.load(sys.stdin)
|
|
@@ -78,10 +93,16 @@ try:
|
|
|
78
93
|
print(val)
|
|
79
94
|
except:
|
|
80
95
|
pass
|
|
81
|
-
" 2>/dev/null
|
|
82
|
-
|
|
83
|
-
PLAN_TEXT="$EXTRACTED"
|
|
96
|
+
" 2>/dev/null
|
|
97
|
+
return $?
|
|
84
98
|
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Extract plan_output text if input is JSON, otherwise use raw input
|
|
102
|
+
PLAN_TEXT="$INPUT"
|
|
103
|
+
EXTRACTED=$(printf '%s' "$INPUT" | extract_plan_text) || true
|
|
104
|
+
if [ -n "$EXTRACTED" ]; then
|
|
105
|
+
PLAN_TEXT="$EXTRACTED"
|
|
85
106
|
fi
|
|
86
107
|
|
|
87
108
|
# ─── Judge prompt ────────────────────────────────────────────────────────────
|
|
@@ -158,7 +179,22 @@ parse_scores() {
|
|
|
158
179
|
local response="$1"
|
|
159
180
|
# Extract the JSON object from the LLM response content
|
|
160
181
|
local content
|
|
161
|
-
|
|
182
|
+
if command -v node >/dev/null 2>&1; then
|
|
183
|
+
content=$(printf '%s' "$response" | node -e "
|
|
184
|
+
let input = '';
|
|
185
|
+
process.stdin.setEncoding('utf8');
|
|
186
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
187
|
+
process.stdin.on('end', () => {
|
|
188
|
+
try {
|
|
189
|
+
const data = JSON.parse(input);
|
|
190
|
+
const choice = (data.choices || [])[0] || {};
|
|
191
|
+
const message = choice.message || {};
|
|
192
|
+
process.stdout.write(String(message.content || ''));
|
|
193
|
+
} catch (_) {}
|
|
194
|
+
});
|
|
195
|
+
" 2>/dev/null) || true
|
|
196
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
197
|
+
content=$(printf '%s' "$response" | python3 -c "
|
|
162
198
|
import sys, json
|
|
163
199
|
try:
|
|
164
200
|
data = json.load(sys.stdin)
|
|
@@ -170,6 +206,9 @@ try:
|
|
|
170
206
|
except Exception as e:
|
|
171
207
|
pass
|
|
172
208
|
" 2>/dev/null) || true
|
|
209
|
+
else
|
|
210
|
+
content=""
|
|
211
|
+
fi
|
|
173
212
|
|
|
174
213
|
if [ -z "$content" ]; then
|
|
175
214
|
echo ""
|
|
@@ -178,7 +217,37 @@ except Exception as e:
|
|
|
178
217
|
|
|
179
218
|
# Parse scores from content JSON
|
|
180
219
|
local scores
|
|
181
|
-
|
|
220
|
+
if command -v node >/dev/null 2>&1; then
|
|
221
|
+
scores=$(printf '%s' "$content" | node -e "
|
|
222
|
+
let text = '';
|
|
223
|
+
process.stdin.setEncoding('utf8');
|
|
224
|
+
process.stdin.on('data', chunk => { text += chunk; });
|
|
225
|
+
process.stdin.on('end', () => {
|
|
226
|
+
function emit(value) {
|
|
227
|
+
const security = Number.parseInt(value.security, 10);
|
|
228
|
+
const tdd = Number.parseInt(value.tdd, 10);
|
|
229
|
+
const design = Number.parseInt(value.design, 10);
|
|
230
|
+
const structural = Number.parseInt(value.structural, 10);
|
|
231
|
+
if ([security, tdd, design, structural].every(score => Number.isInteger(score) && score >= 0 && score <= 5)) {
|
|
232
|
+
console.log([security, tdd, design, structural].join(' '));
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
if (emit(JSON.parse(text.trim()))) return;
|
|
239
|
+
} catch (_) {}
|
|
240
|
+
const match = text.match(/\{[^}]+\}/s);
|
|
241
|
+
if (match) {
|
|
242
|
+
try {
|
|
243
|
+
if (emit(JSON.parse(match[0]))) return;
|
|
244
|
+
} catch (_) {}
|
|
245
|
+
}
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
248
|
+
" 2>/dev/null) || { echo ""; return 1; }
|
|
249
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
250
|
+
scores=$(printf '%s' "$content" | python3 -c "
|
|
182
251
|
import sys, json, re
|
|
183
252
|
text = sys.stdin.read().strip()
|
|
184
253
|
# Try direct JSON parse first
|
|
@@ -209,6 +278,10 @@ if m:
|
|
|
209
278
|
pass
|
|
210
279
|
sys.exit(1)
|
|
211
280
|
" 2>/dev/null) || { echo ""; return 1; }
|
|
281
|
+
else
|
|
282
|
+
echo ""
|
|
283
|
+
return 1
|
|
284
|
+
fi
|
|
212
285
|
|
|
213
286
|
echo "$scores"
|
|
214
287
|
}
|
|
@@ -221,10 +294,23 @@ call_openrouter() {
|
|
|
221
294
|
|
|
222
295
|
# Escape prompt for JSON
|
|
223
296
|
local escaped_prompt
|
|
224
|
-
|
|
297
|
+
if command -v node >/dev/null 2>&1; then
|
|
298
|
+
escaped_prompt=$(printf '%s' "$prompt" | node -e "
|
|
299
|
+
let input = '';
|
|
300
|
+
process.stdin.setEncoding('utf8');
|
|
301
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
302
|
+
process.stdin.on('end', () => {
|
|
303
|
+
process.stdout.write(JSON.stringify(input));
|
|
304
|
+
});
|
|
305
|
+
" 2>/dev/null) || escaped_prompt='""'
|
|
306
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
307
|
+
escaped_prompt=$(printf '%s' "$prompt" | python3 -c "
|
|
225
308
|
import sys, json
|
|
226
309
|
print(json.dumps(sys.stdin.read()))
|
|
227
310
|
" 2>/dev/null) || escaped_prompt='""'
|
|
311
|
+
else
|
|
312
|
+
escaped_prompt='""'
|
|
313
|
+
fi
|
|
228
314
|
|
|
229
315
|
local body
|
|
230
316
|
body=$(cat <<JSON
|
|
@@ -295,7 +381,22 @@ if [ "${BEHAVIORAL_JUDGE_TEST_MODE}" = "1" ]; then
|
|
|
295
381
|
fi
|
|
296
382
|
|
|
297
383
|
# Parse mock scores
|
|
298
|
-
|
|
384
|
+
if command -v node >/dev/null 2>&1; then
|
|
385
|
+
scores=$(printf '%s' "$mock_scores" | node -e "
|
|
386
|
+
let input = '';
|
|
387
|
+
process.stdin.setEncoding('utf8');
|
|
388
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
389
|
+
process.stdin.on('end', () => {
|
|
390
|
+
const scores = JSON.parse(input);
|
|
391
|
+
const security = Number.parseInt(scores.security ?? 3, 10);
|
|
392
|
+
const tdd = Number.parseInt(scores.tdd ?? 3, 10);
|
|
393
|
+
const design = Number.parseInt(scores.design ?? 3, 10);
|
|
394
|
+
const structural = Number.parseInt(scores.structural ?? 3, 10);
|
|
395
|
+
console.log([security, tdd, design, structural].join(' '));
|
|
396
|
+
});
|
|
397
|
+
" 2>/dev/null) || scores="3 3 3 3"
|
|
398
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
399
|
+
scores=$(printf '%s' "$mock_scores" | python3 -c "
|
|
299
400
|
import sys, json
|
|
300
401
|
d = json.loads(sys.stdin.read())
|
|
301
402
|
s = int(d.get('security', 3))
|
|
@@ -304,6 +405,9 @@ de = int(d.get('design', 3))
|
|
|
304
405
|
st = int(d.get('structural', 3))
|
|
305
406
|
print(f'{s} {t} {de} {st}')
|
|
306
407
|
" 2>/dev/null) || scores="3 3 3 3"
|
|
408
|
+
else
|
|
409
|
+
scores="3 3 3 3"
|
|
410
|
+
fi
|
|
307
411
|
|
|
308
412
|
read -r sec tdd_s des str <<< "$scores"
|
|
309
413
|
compute_result "$sec" "$tdd_s" "$des" "$str" "$judge_model" "$judge_calls"
|