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.
Files changed (135) hide show
  1. package/.claude/commands/dev.md +2 -2
  2. package/.claude/commands/plan.md +2 -2
  3. package/.claude/commands/premerge.md +2 -2
  4. package/.claude/commands/review.md +5 -2
  5. package/.claude/commands/ship.md +6 -5
  6. package/.claude/commands/status.md +4 -4
  7. package/.claude/rules/greptile-review-process.md +4 -4
  8. package/.cline/workflows/dev.md +2 -2
  9. package/.cline/workflows/plan.md +2 -2
  10. package/.cline/workflows/premerge.md +2 -2
  11. package/.cline/workflows/review.md +5 -2
  12. package/.cline/workflows/ship.md +6 -5
  13. package/.cline/workflows/status.md +4 -4
  14. package/.codex/skills/dev/SKILL.md +2 -2
  15. package/.codex/skills/plan/SKILL.md +2 -2
  16. package/.codex/skills/premerge/SKILL.md +2 -2
  17. package/.codex/skills/review/SKILL.md +5 -2
  18. package/.codex/skills/ship/SKILL.md +6 -5
  19. package/.codex/skills/status/SKILL.md +4 -4
  20. package/.cursor/commands/dev.md +2 -2
  21. package/.cursor/commands/plan.md +2 -2
  22. package/.cursor/commands/premerge.md +2 -2
  23. package/.cursor/commands/review.md +5 -2
  24. package/.cursor/commands/ship.md +6 -5
  25. package/.cursor/commands/status.md +4 -4
  26. package/.github/prompts/dev.prompt.md +2 -2
  27. package/.github/prompts/plan.prompt.md +2 -2
  28. package/.github/prompts/premerge.prompt.md +2 -2
  29. package/.github/prompts/review.prompt.md +5 -2
  30. package/.github/prompts/ship.prompt.md +6 -5
  31. package/.github/prompts/status.prompt.md +4 -4
  32. package/.github/workflows/beads-to-github.yml +44 -11
  33. package/.github/workflows/github-to-beads.yml +11 -8
  34. package/.kilocode/workflows/dev.md +2 -2
  35. package/.kilocode/workflows/plan.md +2 -2
  36. package/.kilocode/workflows/premerge.md +2 -2
  37. package/.kilocode/workflows/review.md +5 -2
  38. package/.kilocode/workflows/ship.md +6 -5
  39. package/.kilocode/workflows/status.md +4 -4
  40. package/.opencode/commands/dev.md +2 -2
  41. package/.opencode/commands/plan.md +2 -2
  42. package/.opencode/commands/premerge.md +2 -2
  43. package/.opencode/commands/review.md +5 -2
  44. package/.opencode/commands/ship.md +6 -5
  45. package/.opencode/commands/status.md +4 -4
  46. package/.roo/commands/dev.md +2 -2
  47. package/.roo/commands/plan.md +2 -2
  48. package/.roo/commands/premerge.md +2 -2
  49. package/.roo/commands/review.md +5 -2
  50. package/.roo/commands/ship.md +6 -5
  51. package/.roo/commands/status.md +4 -4
  52. package/AGENTS.md +10 -9
  53. package/CLAUDE.md +12 -0
  54. package/README.md +12 -6
  55. package/bin/forge.js +24 -8
  56. package/docs/BEADS_GITHUB_SYNC.md +32 -2
  57. package/docs/EXAMPLES.md +22 -22
  58. package/docs/ROADMAP.md +3 -3
  59. package/docs/TOOLCHAIN.md +150 -160
  60. package/lib/agents/codex.plugin.json +3 -0
  61. package/lib/agents-config.js +18 -12
  62. package/lib/beads-bootstrap.js +225 -0
  63. package/lib/beads-health-check.js +55 -10
  64. package/lib/beads-setup.js +104 -28
  65. package/lib/beads-sync-scaffold.js +11 -6
  66. package/lib/codex-skills.js +54 -1
  67. package/lib/commands/_issue.js +11 -1
  68. package/lib/commands/issues.js +49 -0
  69. package/lib/commands/plan.js +5 -2
  70. package/lib/commands/recommend.js +22 -1
  71. package/lib/commands/setup.js +247 -27
  72. package/lib/commands/ship.js +188 -5
  73. package/lib/commands/status.js +187 -19
  74. package/lib/commands/team.js +11 -1
  75. package/lib/commands/test.js +125 -25
  76. package/lib/commands/validate.js +232 -9
  77. package/lib/commands/worktree.js +27 -54
  78. package/lib/dep-guard/keyword-ripple.js +184 -0
  79. package/lib/detect-worktree.js +9 -10
  80. package/lib/forge-issues.js +326 -0
  81. package/lib/issue-sync/authority.js +100 -0
  82. package/lib/issue-sync/github-pull.js +184 -0
  83. package/lib/issue-sync/import-primitives.js +98 -0
  84. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  85. package/lib/issue-sync/link-store.js +292 -0
  86. package/lib/issue-sync/project-github.js +123 -0
  87. package/lib/issue-sync/reconcile.js +195 -0
  88. package/lib/issue-sync/schema.js +126 -0
  89. package/lib/lefthook-check.js +5 -2
  90. package/lib/project-memory.js +564 -0
  91. package/lib/runtime-health.js +100 -12
  92. package/lib/setup-action-log.js +2 -0
  93. package/lib/setup-summary-renderer.js +15 -11
  94. package/lib/smart-status/conflicts.js +205 -0
  95. package/lib/smart-status/scoring.js +177 -0
  96. package/lib/status/beads-snapshot.js +102 -0
  97. package/lib/status/presenter.js +65 -0
  98. package/lib/workflow/enforce-stage.js +14 -8
  99. package/lib/workflow/state-manager.js +349 -0
  100. package/package.json +12 -4
  101. package/scripts/beads-context.sh +124 -5
  102. package/scripts/beads-context.test.js +21 -4
  103. package/scripts/beads-migrate-to-dolt.sh +7 -0
  104. package/scripts/beads-upgrade-smoke.sh +263 -0
  105. package/scripts/behavioral-judge.sh +115 -11
  106. package/scripts/benchmark.js +349 -63
  107. package/scripts/dep-guard-analyze.js +52 -17
  108. package/scripts/dep-guard-keyword-ripple.js +29 -0
  109. package/scripts/dep-guard-render-review.js +86 -0
  110. package/scripts/dep-guard.sh +45 -222
  111. package/scripts/forge-team/lib/hooks.sh +1 -1
  112. package/scripts/forge-team/lib/sync-github.sh +160 -28
  113. package/scripts/forge-team/lib/verify.sh +1 -1
  114. package/scripts/forge-team/lib/workload.sh +56 -27
  115. package/scripts/forge-team/tests/sync-github.test.sh +195 -58
  116. package/scripts/forge-team/tests/workload.test.sh +35 -4
  117. package/scripts/github-beads-sync/index.mjs +122 -98
  118. package/scripts/github-beads-sync/mapping.mjs +54 -0
  119. package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
  120. package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
  121. package/scripts/github-beads-sync/run-bd.mjs +4 -2
  122. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  123. package/scripts/preflight.sh +181 -0
  124. package/scripts/smart-status-score.js +31 -0
  125. package/scripts/smart-status-sessions.js +51 -0
  126. package/scripts/smart-status.sh +84 -330
  127. package/scripts/sync-agentic-workflow.js +48 -0
  128. package/scripts/sync-utils.sh +39 -0
  129. package/scripts/test-ci-shard.js +244 -0
  130. package/scripts/test-dashboard.js +188 -52
  131. package/scripts/test-full-suite.js +186 -0
  132. package/scripts/test-profile.js +278 -0
  133. package/scripts/test.js +335 -38
  134. package/scripts/validate.js +143 -0
  135. package/scripts/validate.sh +18 -1
@@ -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="$(bd update "$@" 2>&1)"
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="$(bd comments add "$@" 2>&1)"
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="$(bd show "$issue_id" --json 2>&1)" || die "Failed to show issue ${issue_id}"
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="$(bd show "$issue_id" --json 2>&1)" || {
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="$(bd comments list "$issue_id" 2>/dev/null || true)"
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.skipIf(!isBdAvailable())('scripts/beads-context.sh', () => {
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.sh', () => {
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,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ NODE_CMD="${NODE_CMD:-node}"
6
+
7
+ exec "$NODE_CMD" "$SCRIPT_DIR/lib/beads-migrate-to-dolt.mjs" "$@"
@@ -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
- # Extract plan_output text if input is JSON, otherwise use raw input
70
- PLAN_TEXT="$INPUT"
71
- if command -v python3 >/dev/null 2>&1; then
72
- EXTRACTED=$(echo "$INPUT" | python3 -c "
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) || true
82
- if [ -n "$EXTRACTED" ]; then
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
- content=$(echo "$response" | python3 -c "
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
- scores=$(echo "$content" | python3 -c "
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
- escaped_prompt=$(echo "$prompt" | python3 -c "
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
- scores=$(echo "$mock_scores" | python3 -c "
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"