@giamme/forge 0.0.0-stage → 0.8.0

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 (127) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1854 -2
  4. package/SKILL.md +122 -0
  5. package/VERSION +1 -0
  6. package/forge +74 -0
  7. package/package.json +26 -4
  8. package/references/dataset.md +329 -0
  9. package/references/decompose.md +667 -0
  10. package/references/fractal-dashboard.md +21 -0
  11. package/references/fractal.md +307 -0
  12. package/references/harnesses.md +353 -0
  13. package/references/jev.md +865 -0
  14. package/references/memory.md +228 -0
  15. package/references/planning.md +42 -0
  16. package/references/ripwire.md +47 -0
  17. package/references/runtime.md +64 -0
  18. package/references/solo.md +113 -0
  19. package/references/ui.md +112 -0
  20. package/registry.tsv +81 -0
  21. package/scripts/forge-artifact.sh +78 -0
  22. package/scripts/forge-contract.py +430 -0
  23. package/scripts/forge-dataset.py +6 -0
  24. package/scripts/forge-dispatch.sh +665 -0
  25. package/scripts/forge-fail-first.py +195 -0
  26. package/scripts/forge-fractal-dispatch.sh +4 -0
  27. package/scripts/forge-fractal-options.sh +54 -0
  28. package/scripts/forge-fractal.py +6 -0
  29. package/scripts/forge-guard.py +266 -0
  30. package/scripts/forge-install-fractal.sh +4 -0
  31. package/scripts/forge-install-ripwire.sh +52 -0
  32. package/scripts/forge-install.sh +135 -0
  33. package/scripts/forge-jev-options.sh +259 -0
  34. package/scripts/forge-jev.py +6 -0
  35. package/scripts/forge-lib-plans.sh +785 -0
  36. package/scripts/forge-lib-review.sh +494 -0
  37. package/scripts/forge-lib-task.sh +302 -0
  38. package/scripts/forge-lib-verify.sh +30 -0
  39. package/scripts/forge-memory.sh +575 -0
  40. package/scripts/forge-metrics.sh +39 -0
  41. package/scripts/forge-parallel-options.sh +76 -0
  42. package/scripts/forge-parallel.sh +1717 -0
  43. package/scripts/forge-plan-check.py +118 -0
  44. package/scripts/forge-prompt.py +45 -0
  45. package/scripts/forge-ripwire.py +136 -0
  46. package/scripts/forge-route.py +344 -0
  47. package/scripts/forge-runtime.py +533 -0
  48. package/scripts/forge-schedule.py +312 -0
  49. package/scripts/forge-shadow.py +305 -0
  50. package/scripts/forge-solo.sh +347 -0
  51. package/scripts/forge-ui-options.sh +42 -0
  52. package/scripts/forge-ui.py +435 -0
  53. package/scripts/forge-update.sh +71 -0
  54. package/scripts/forge-verify-coverage.py +956 -0
  55. package/scripts/forge-version.py +198 -0
  56. package/scripts/forge_dataset/__init__.py +439 -0
  57. package/scripts/forge_dataset/capture.py +330 -0
  58. package/scripts/forge_dataset/checks.py +109 -0
  59. package/scripts/forge_dataset/cli.py +233 -0
  60. package/scripts/forge_dataset/export.py +1009 -0
  61. package/scripts/forge_dataset/harvest.py +716 -0
  62. package/scripts/forge_dataset/importer.py +412 -0
  63. package/scripts/forge_dataset/outcomes.py +275 -0
  64. package/scripts/forge_dataset/routing.py +278 -0
  65. package/scripts/forge_fractal/__init__.py +83 -0
  66. package/scripts/forge_fractal/bridge.py +46 -0
  67. package/scripts/forge_fractal/cli.py +202 -0
  68. package/scripts/forge_fractal/dashboard.css +138 -0
  69. package/scripts/forge_fractal/dashboard.html +41 -0
  70. package/scripts/forge_fractal/dashboard.js +687 -0
  71. package/scripts/forge_fractal/decomposition.py +262 -0
  72. package/scripts/forge_fractal/execution.py +659 -0
  73. package/scripts/forge_fractal/inspection.py +261 -0
  74. package/scripts/forge_fractal/install.py +158 -0
  75. package/scripts/forge_fractal/invocation.py +94 -0
  76. package/scripts/forge_fractal/pipeline.py +124 -0
  77. package/scripts/forge_fractal/selection.py +179 -0
  78. package/scripts/forge_jev/__init__.py +376 -0
  79. package/scripts/forge_jev/backtest.py +322 -0
  80. package/scripts/forge_jev/cli.py +980 -0
  81. package/scripts/forge_jev/client.py +418 -0
  82. package/scripts/forge_jev/corpus.py +156 -0
  83. package/scripts/forge_jev/coupling.py +244 -0
  84. package/scripts/forge_jev/features.py +24 -0
  85. package/scripts/forge_jev/gates.py +100 -0
  86. package/scripts/forge_jev/memory.py +163 -0
  87. package/scripts/forge_jev/outcome.py +49 -0
  88. package/scripts/forge_jev/predict.py +204 -0
  89. package/scripts/forge_jev/questions.py +661 -0
  90. package/scripts/forge_jev/review.py +73 -0
  91. package/scripts/forge_jev/routing.py +271 -0
  92. package/scripts/forge_jev/subset.py +123 -0
  93. package/scripts/forge_jev/verify.py +453 -0
  94. package/scripts/forge_registry.py +140 -0
  95. package/ui/README.md +287 -0
  96. package/ui/THIRD_PARTY_NOTICES.md +69 -0
  97. package/ui/docs/api.md +888 -0
  98. package/ui/forge_ui/__init__.py +20 -0
  99. package/ui/forge_ui/__main__.py +135 -0
  100. package/ui/forge_ui/diffs.py +454 -0
  101. package/ui/forge_ui/executions.py +436 -0
  102. package/ui/forge_ui/export.py +204 -0
  103. package/ui/forge_ui/findings.py +132 -0
  104. package/ui/forge_ui/fractal.py +583 -0
  105. package/ui/forge_ui/models.py +216 -0
  106. package/ui/forge_ui/reader.py +1336 -0
  107. package/ui/forge_ui/server.py +303 -0
  108. package/ui/forge_ui/static/app.js +924 -0
  109. package/ui/forge_ui/static/cloud3d.js +646 -0
  110. package/ui/forge_ui/static/cloudmodel.js +375 -0
  111. package/ui/forge_ui/static/cloudui.js +394 -0
  112. package/ui/forge_ui/static/diffview.js +216 -0
  113. package/ui/forge_ui/static/favicon.svg +1 -0
  114. package/ui/forge_ui/static/files.js +776 -0
  115. package/ui/forge_ui/static/gantt.js +97 -0
  116. package/ui/forge_ui/static/graph.js +266 -0
  117. package/ui/forge_ui/static/index.html +55 -0
  118. package/ui/forge_ui/static/iso.js +765 -0
  119. package/ui/forge_ui/static/leaderboard.js +315 -0
  120. package/ui/forge_ui/static/map3d.js +45 -0
  121. package/ui/forge_ui/static/modelstats.js +249 -0
  122. package/ui/forge_ui/static/notify.js +179 -0
  123. package/ui/forge_ui/static/panel.js +526 -0
  124. package/ui/forge_ui/static/runmodel.js +651 -0
  125. package/ui/forge_ui/static/scene3d.js +996 -0
  126. package/ui/forge_ui/static/search.js +119 -0
  127. package/ui/forge_ui/static/style.css +618 -0
@@ -0,0 +1,1717 @@
1
+ #!/usr/bin/env bash
2
+ # forge-parallel.sh — decomposed forge runs: many dwarves working concurrently in
3
+ # isolated git worktrees, each task reviewed by its own QA pass, only passing work
4
+ # merged. Bash 3.2 entrypoints keep the task and merge contracts; a Python 3
5
+ # standard-library coordinator schedules ready tasks without wave barriers.
6
+ #
7
+ # Usage:
8
+ # forge-parallel.sh plan <plan-dir> --repo <dir> [routing flags] [--decompose-level low|medium|high] [--ui]
9
+ # forge-parallel.sh run <plan-dir> [--max-parallel N] [--yolo-dwarf] [--yolo-qa]
10
+ # [--verify <command>] [--setup <command>] [--dry-run]
11
+ # [--timeout S] [--retry-failed N] [--infra-retries M]
12
+ # [--qa-threshold P0|P1|P2|P3] [--verify-retries N]
13
+ # forge-parallel.sh retry <plan-dir> <task-id> [--dwarf <spec>] [--setup <command>]
14
+ # [--timeout S] [--qa-threshold Pn] [--verify-retries N]
15
+ # forge-parallel.sh integrate <plan-dir> --approved [--verify-retries N] [--final-review <qa-spec>]
16
+ # forge-parallel.sh review <plan-dir> [--qa <spec>] [--yolo-qa] [--qa-threshold Pn]
17
+ # forge-parallel.sh accept <plan-dir> <task-id> --reason "<why>" --approved
18
+ # forge-parallel.sh split <plan-dir> <new-plan> --tasks <id,id> [--dry-run]
19
+ # forge-parallel.sh combine <new-plan> <plan-a> <plan-b> [<plan>...] [--verify <command>] [--dry-run]
20
+ # forge-parallel.sh _task <plan-dir> <task-id> (internal; coordinator target)
21
+ #
22
+ # Routing flags (plan):
23
+ # --dwarf <spec> fallback for any difficulty tier not named below
24
+ # --dwarf-high <spec> spec (or comma-list, pooled) for high-difficulty tasks
25
+ # --dwarf-medium <spec>
26
+ # --dwarf-low <spec>
27
+ # --qa, --qa-high, --qa-medium, --qa-low identical grammar
28
+ # --decompose-level L the --decompose-level the goal was split at, kept as a record
29
+ # --shadow-qa <spec[,..]> opt-in: a second review of a sampled share of reviewed attempts,
30
+ # by a listed reviewer of another model family, recorded and
31
+ # never read (measures reviewer agreement). `none` turns it off
32
+ # --shadow-rate R that share, 0..1 (default 0.1)
33
+ # --explore E on by default at 0.2 (0 turns it off, and is kept): a row left
34
+ # to forge gets, with probability E/2, its neighbouring model
35
+ # (one tier away in the given pools) instead of its pool's turn;
36
+ # each decision and its odds go to tasks/<id>/route.jsonl
37
+ # (precedence: flag, this plan's earlier choice, FORGE_ROUTE_EXPLORE)
38
+ #
39
+ # Opt-in run behaviour (all off unless asked):
40
+ # --retry-failed N re-queue a task whose reviewer said FAIL up to N more times,
41
+ # inside the same run, while the other tasks keep going
42
+ # --infra-retries M wait out quota/auth/network stops up to M times instead of pausing
43
+ # --qa-threshold Pn a reviewer FAIL made only of findings less severe than Pn is
44
+ # accepted as PASS-with-known-issues (P0 worst ... P3 mildest)
45
+ #
46
+ # Exit codes: 0 ok | 2 usage/validation | 3 precondition (not a git repo, etc.)
47
+ # 5 some task failed QA or verification failed | 6 integration conflict
48
+ # 8 paused on an infrastructure failure (quota, auth, network): no attempt was
49
+ # spent, fix the cause and re-run `run`
50
+ set -uo pipefail
51
+
52
+ SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
53
+ SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
54
+ DISPATCH="$SKILL_DIR/scripts/forge-dispatch.sh"
55
+ MEMORY="$SKILL_DIR/scripts/forge-memory.sh"
56
+ source "$SKILL_DIR/scripts/forge-artifact.sh"
57
+ source "$SKILL_DIR/scripts/forge-metrics.sh"
58
+ source "$SKILL_DIR/scripts/forge-fractal-options.sh"
59
+ source "$SKILL_DIR/scripts/forge-ui-options.sh"
60
+ # Beside fractal's, and for the same reason: an option parser sourced from inside
61
+ # a function, after that function has already walked its arguments, cannot parse
62
+ # anything. Every --jev flag was rejected as "unknown option" until this line.
63
+ JEV_OPTS=0
64
+ [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ] \
65
+ && source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && JEV_OPTS=1
66
+ # Flags shared by several subcommands, then the helper libraries. Functions only, resolved
67
+ # at call time, so they may use die/note/field and friends defined below.
68
+ source "$SKILL_DIR/scripts/forge-parallel-options.sh"
69
+ for forge_lib in task verify plans review; do
70
+ [ -f "$SKILL_DIR/scripts/forge-lib-$forge_lib.sh" ] && source "$SKILL_DIR/scripts/forge-lib-$forge_lib.sh"
71
+ done
72
+ OUTPUT=summary
73
+
74
+ die() { printf 'forge: %s\n' "$1" >&2; exit "${2:-2}"; }
75
+ note() { printf 'forge: %s\n' "$*" >&2; }
76
+
77
+ # --- tsv helpers --------------------------------------------------------------
78
+ # awk rather than `read`, because tab is IFS-whitespace: `IFS=$'\t' read` collapses
79
+ # consecutive tabs and would silently shift every column after an empty field.
80
+ TASKS_COLS="id deps difficulty files dwarf qa title"
81
+ col_no() { # col_no <name> -> 1-based index
82
+ local want="$1" i=1 c
83
+ for c in $TASKS_COLS; do [ "$c" = "$want" ] && { echo "$i"; return; }; i=$((i+1)); done
84
+ echo 0
85
+ }
86
+ field() { # field <tasks.tsv> <id> <colname>
87
+ awk -F'\t' -v id="$2" -v n="$(col_no "$3")" '!/^#/ && NF && $1==id {print $n; exit}' "$1"
88
+ }
89
+ all_ids() { awk -F'\t' '!/^#/ && NF {print $1}' "$1"; }
90
+
91
+ list_has() { # list_has <needle> <comma-list>
92
+ local n="$1" l="$2" w
93
+ [ "$l" = "-" ] && return 1
94
+ for w in $(printf '%s' "$l" | tr ',' ' '); do [ "$w" = "$n" ] && return 0; done
95
+ return 1
96
+ }
97
+ lists_overlap() { # lists_overlap <comma-list-a> <space-list-b>
98
+ local a="$1" b="$2" w v
99
+ [ "$a" = "-" ] && return 1
100
+ for w in $(printf '%s' "$a" | tr ',' ' '); do
101
+ w="${w#./}"; w="${w%/}"
102
+ for v in $b; do
103
+ v="${v#./}"; v="${v%/}"
104
+ case "$w/" in "$v/"*) return 0 ;; esac
105
+ case "$v/" in "$w/"*) return 0 ;; esac
106
+ [ "$w" = . ] || [ "$v" = . ] && return 0
107
+ done
108
+ done
109
+ return 1
110
+ }
111
+
112
+ # --- plan ---------------------------------------------------------------------
113
+ # Resolves each task's difficulty into a concrete dwarf/qa spec and writes it back
114
+ # into tasks.tsv, so `run` never needs to know the routing rules. An explicit value
115
+ # already in the column always wins — that is how an approval-gate override for one
116
+ # row survives a re-plan.
117
+ pool_pick() { # pool_pick <comma-list> <index>
118
+ local list="$1" idx="$2" n=0 w last=""
119
+ for w in $(printf '%s' "$list" | tr ',' ' '); do n=$((n+1)); done
120
+ [ "$n" -eq 0 ] && { echo ""; return; }
121
+ idx=$((idx % n)); n=0
122
+ for w in $(printf '%s' "$list" | tr ',' ' '); do
123
+ [ "$n" -eq "$idx" ] && { echo "$w"; return; }
124
+ n=$((n+1)); last="$w"
125
+ done
126
+ echo "$last"
127
+ }
128
+
129
+ do_plan() {
130
+ local PLAN="${1:?plan needs a plan dir}"; shift
131
+ local REPO="" D_ANY="" D_HI="" D_MD="" D_LO="" Q_ANY="" Q_HI="" Q_MD="" Q_LO=""
132
+ local PLANNER="" NOMEM=0 jev_rc=0 DLEVEL="" SHADOW_QA="" SHADOW_RATE="" EXPLORE="" FAIL_FIRST=""
133
+ while [ $# -gt 0 ]; do
134
+ case "$1" in
135
+ --repo) REPO="${2:?}"; shift 2 ;;
136
+ --planner) PLANNER="${2:?}"; shift 2 ;;
137
+ --decompose-level)
138
+ case "${2:-}" in low|medium|high) DLEVEL="$2"; shift 2 ;;
139
+ *) die "--decompose-level must be low, medium or high" ;; esac ;;
140
+ --no-ripwire) export FORGE_RIPWIRE=off; shift ;;
141
+ --no-memory) NOMEM=1; shift ;;
142
+ --shadow-qa)
143
+ [ -n "${2:-}" ] || die "--shadow-qa needs a QA spec, a comma list of specs, or none"
144
+ SHADOW_QA="$2"; shift 2 ;;
145
+ --shadow-rate)
146
+ printf '%s' "${2:-}" | grep -Eq '^(0?\.[0-9]+|0|1|1\.0+)$' || die "--shadow-rate needs a share between 0 and 1"
147
+ SHADOW_RATE="$2"; shift 2 ;;
148
+ --explore) EXPLORE="${2:?--explore needs a share between 0 and 0.5}"; shift 2 ;;
149
+ --dwarf) D_ANY="${2:?}"; shift 2 ;;
150
+ --dwarf-high) D_HI="${2:?}"; shift 2 ;;
151
+ --dwarf-medium) D_MD="${2:?}"; shift 2 ;;
152
+ --dwarf-low) D_LO="${2:?}"; shift 2 ;;
153
+ --qa) Q_ANY="${2:?}"; shift 2 ;;
154
+ --qa-high) Q_HI="${2:?}"; shift 2 ;;
155
+ --qa-medium) Q_MD="${2:?}"; shift 2 ;;
156
+ --qa-low) Q_LO="${2:?}"; shift 2 ;;
157
+ --ui|--no-ui) forge_ui_flag "$1"; shift ;;
158
+ --fail-first) FAIL_FIRST=on; shift ;;
159
+ --no-fail-first) FAIL_FIRST=off; shift ;;
160
+ *)
161
+ # `case` is not a loop, so `continue` here belongs to the enclosing while.
162
+ if [ "$JEV_OPTS" = 1 ]; then
163
+ forge_jev_flag "$1"; jev_rc=$?
164
+ case "$jev_rc" in
165
+ 0) shift "$JEV_SHIFT"; continue ;;
166
+ 2) exit 2 ;;
167
+ esac
168
+ fi
169
+ die "plan: unknown option '$1'"
170
+ ;;
171
+ esac
172
+ done
173
+ [ "$JEV_OPTS" = 1 ] && forge_jev_export
174
+ [ -d "$PLAN" ] || die "plan dir '$PLAN' does not exist"
175
+ # Freeze the selection next to the task table, so run and retry restore it rather
176
+ # than re-deciding. Written on every plan: a re-plan is a new decision.
177
+ [ "$JEV_OPTS" = 1 ] && forge_jev_record "$PLAN"
178
+ local T="$PLAN/tasks.tsv"
179
+ [ -r "$T" ] || die "no tasks.tsv in '$PLAN' — decompose the goal first"
180
+ # The table as the planner or orchestrator wrote it, before Jev or route resolution
181
+ # rewrites any column: which tier a task was DECLARED is a label of its own.
182
+ cp "$T" "$PLAN/tasks.declared.tsv"
183
+ [ -z "$DLEVEL" ] || printf '%s\n' "$DLEVEL" > "$PLAN/decompose_level"
184
+ # Shadow reviews are part of the plan, like its routing: frozen here, shown in the table
185
+ # and read by every task process. `none` turns them off again.
186
+ case "$SHADOW_QA" in
187
+ '') ;;
188
+ none|off) rm -f "$PLAN/shadow_qa" "$PLAN/shadow_rate" "$PLAN/shadow_qa.ok" ;;
189
+ *) printf '%s\n' "$SHADOW_QA" > "$PLAN/shadow_qa" ;;
190
+ esac
191
+ [ -z "$SHADOW_RATE" ] || [ "$SHADOW_QA" = none ] || [ "$SHADOW_QA" = off ] \
192
+ || printf '%s\n' "$SHADOW_RATE" > "$PLAN/shadow_rate"
193
+ # Fail-first evidence (opt-in, advisory) is part of the plan too: every task's review gets it.
194
+ case "$FAIL_FIRST" in on) : > "$PLAN/fail_first" ;; off) rm -f "$PLAN/fail_first" ;; esac
195
+ # Exploration is decided here and only here, so the table and preflight see what will run.
196
+ # On by default: choosing models from measured pass rates needs some randomized picks, and
197
+ # history had almost none (9 tasks ever ran on more than one spec). The flag wins, then
198
+ # this plan's earlier choice, then FORGE_ROUTE_EXPLORE, then 0.2. A 0 is written down too,
199
+ # so a later re-plan never quietly turns exploration back on.
200
+ if [ -z "$EXPLORE" ]; then
201
+ if [ -f "$PLAN/explore" ]; then EXPLORE="$(cat "$PLAN/explore")"
202
+ elif [ -n "${FORGE_ROUTE_EXPLORE:-}" ]; then EXPLORE="$FORGE_ROUTE_EXPLORE"
203
+ else EXPLORE=0.2; fi
204
+ fi
205
+ awk -v v="$EXPLORE" 'BEGIN { exit !(v ~ /^(0|0?\.[0-9]+)$/ && v + 0 <= 0.5) }' \
206
+ || die "--explore (or FORGE_ROUTE_EXPLORE) needs a share between 0 and 0.5"
207
+ printf '%s\n' "$EXPLORE" > "$PLAN/explore"
208
+ if awk -v v="$EXPLORE" 'BEGIN { exit !(v + 0 == 0) }'; then EXPLORE=""; fi
209
+
210
+ # repo: from flag on first plan, remembered afterwards
211
+ if [ -n "$REPO" ]; then
212
+ [ -d "$REPO" ] || die "--repo '$REPO' is not a directory"
213
+ (cd "$REPO" && git rev-parse --git-dir >/dev/null 2>&1) \
214
+ || die "'$REPO' is not a git repository. Decomposed runs need worktrees, and there is no honest fallback without them." 3
215
+ (cd "$REPO" && pwd) > "$PLAN/repo"
216
+ fi
217
+ [ -s "$PLAN/repo" ] || die "plan needs --repo the first time"
218
+ REPO="$(cat "$PLAN/repo")"
219
+ [ -s "$PLAN/run_id" ] || basename "$PLAN" | sed 's/^forge-//' > "$PLAN/run_id"
220
+ # Every plan, --ui or not: forge-ui's view of this repo's runs reads the registry.
221
+ forge_ui_register "$PLAN"
222
+ [ "${FORGE_RIPWIRE:-}" != off ] || touch "$PLAN/no_ripwire"
223
+ # Persist plan choices for later run and retry processes.
224
+ [ "$NOMEM" = 1 ] && : > "$PLAN/no_memory"
225
+ [ -n "$PLANNER" ] && printf '%s\n' "$PLANNER" > "$PLAN/planner"
226
+ python3 - "$PLAN/fractal-routing.json" "$D_ANY" "$D_LO" "$D_MD" "$D_HI" <<'PY'
227
+ import json,sys
228
+ from pathlib import Path
229
+ p=Path(sys.argv[1]); data=json.loads(p.read_text()) if p.exists() else {}
230
+ for key,value in zip(('any','low','medium','high'),sys.argv[2:]):
231
+ if value: data[key]=[s.strip() for s in value.split(',') if s.strip()]
232
+ p.write_text(json.dumps(data))
233
+ PY
234
+
235
+ # Uncommitted work is invisible to every dwarf, because worktrees branch from a
236
+ # commit. That is the likeliest way to get a confusingly wrong result, so it is
237
+ # stated loudly rather than left for the user to deduce from an odd diff.
238
+ if [ -n "$(cd "$REPO" && git status --porcelain 2>/dev/null)" ]; then
239
+ note "WARNING: '$REPO' has uncommitted changes. Task worktrees branch from HEAD,"
240
+ note " so those edits will NOT be visible to any dwarf. Commit or stash first"
241
+ note " if the tasks depend on them."
242
+ fi
243
+
244
+ # --- jev: advisory difficulty scoring ------------------------------------------
245
+ # Advisory only: a suggested tier, and gate warnings about the task text. Writing a Jev
246
+ # tier into the difficulty column (`--jev-act`, gated on a per-repo calibration) was
247
+ # removed on 2026-10-17 without ever having acted; models are chosen from measured pass
248
+ # rates instead (plan --explore, `forge dataset routing`).
249
+ local jev_loaded=0
250
+ if [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ]; then
251
+ source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && jev_loaded=1
252
+ fi
253
+ if [ "$jev_loaded" = 1 ] && forge_jev_active routing; then
254
+ local jev_scores jev_rc
255
+ jev_scores="$(python3 "$SKILL_DIR/scripts/forge-jev.py" score-plan \
256
+ --plan "$PLAN" --repo "$REPO" 2>/dev/null)"
257
+ jev_rc=$?
258
+ if [ "$jev_rc" -eq 0 ] && [ -n "$jev_scores" ]; then
259
+ # Shadow mode records the judgment and prints nothing per task.
260
+ if [ "${FORGE_JEV_SHADOW:-}" = on ]; then
261
+ note "jev: scored $(printf '%s\n' "$jev_scores" | grep -c . ) task(s) in shadow mode (no change)"
262
+ else
263
+ local jid jtier jconf jcomp jesc jdecl jwarn jdrift
264
+ # The columns score-plan prints: id, tier, confidence, composite, escalated,
265
+ # declared, warnings, drift. Read in lockstep with cmd_score_plan.
266
+ while IFS="$(printf '\t')" read -r jid jtier jconf jcomp jesc jdecl jwarn jdrift; do
267
+ [ -n "$jid" ] || continue
268
+ # score-plan writes "-" for "not escalated"; only a real reason is worth showing.
269
+ [ "$jesc" = "-" ] && jesc=""
270
+ note "jev: $jid suggests $jtier (confidence $jconf${jesc:+, escalated: $jesc}) — declared $jdecl"
271
+ # Gates are warnings: they are about the task text and its declared files, not
272
+ # about which model runs it.
273
+ if [ -n "$jwarn" ] && [ "$jwarn" != "-" ]; then
274
+ case "$jwarn" in
275
+ *prompt_adequacy*) note "jev: $jid — the prompt may be too vague for QA to judge correctness against" ;;
276
+ esac
277
+ case "$jwarn" in
278
+ *independently_verifiable*) note "jev: $jid — may not be verifiable on its own (decompose.md forbids splitting this finely)" ;;
279
+ esac
280
+ fi
281
+ if [ -n "$jdrift" ] && [ "$jdrift" != "-" ]; then
282
+ note "jev: $jid may also edit undeclared files: $jdrift"
283
+ note " undeclared files break wave disjointness — add them to the files column or split the task"
284
+ fi
285
+ done <<EOF
286
+ $jev_scores
287
+ EOF
288
+ fi
289
+ fi
290
+ fi
291
+
292
+ # --- validate + resolve routing ---
293
+ local tmp="$PLAN/.tasks.tsv.new"; : > "$tmp"
294
+ local hi_i=0 md_i=0 lo_i=0 an_i=0 qhi_i=0 qmd_i=0 qlo_i=0 qan_i=0 unassigned=0
295
+ printf '# id\tdeps\tdifficulty\tfiles\tdwarf\tqa\ttitle\n' > "$tmp"
296
+ local id deps diff files dw qa title line
297
+ while IFS= read -r line; do
298
+ case "$line" in ''|'#'*) continue ;; esac
299
+ id="$(printf '%s' "$line" | awk -F'\t' '{print $1}')"
300
+ deps="$(printf '%s' "$line" | awk -F'\t' '{print $2}')"
301
+ diff="$(printf '%s' "$line" | awk -F'\t' '{print $3}')"
302
+ files="$(printf '%s' "$line" | awk -F'\t' '{print $4}')"
303
+ dw="$(printf '%s' "$line" | awk -F'\t' '{print $5}')"
304
+ qa="$(printf '%s' "$line" | awk -F'\t' '{print $6}')"
305
+ title="$(printf '%s' "$line" | awk -F'\t' '{print $7}')"
306
+ [ -n "$id" ] || die "tasks.tsv has a row with no id"
307
+ case "$diff" in low|medium|high) ;; *) die "task '$id' has difficulty '$diff' (expected low, medium or high)" ;; esac
308
+ # deps must exist
309
+ if [ "$deps" != "-" ]; then
310
+ local d
311
+ for d in $(printf '%s' "$deps" | tr ',' ' '); do
312
+ all_ids "$T" | grep -qx "$d" || die "task '$id' depends on unknown task '$d'"
313
+ done
314
+ fi
315
+ # resolve dwarf. UNASSIGNED is a marker from a previous plan, not a user
316
+ # choice, so it must be re-resolved rather than preserved like a real override.
317
+ [ "$dw" = "UNASSIGNED" ] && dw="-"
318
+ if [ "$dw" = "-" ] || [ -z "$dw" ]; then
319
+ case "$diff" in
320
+ high) if [ -n "$D_HI" ]; then dw="$(pool_pick "$D_HI" "$hi_i")"; hi_i=$((hi_i+1)); fi ;;
321
+ medium) if [ -n "$D_MD" ]; then dw="$(pool_pick "$D_MD" "$md_i")"; md_i=$((md_i+1)); fi ;;
322
+ low) if [ -n "$D_LO" ]; then dw="$(pool_pick "$D_LO" "$lo_i")"; lo_i=$((lo_i+1)); fi ;;
323
+ esac
324
+ if [ "$dw" = "-" ] || [ -z "$dw" ]; then
325
+ if [ -n "$D_ANY" ]; then dw="$(pool_pick "$D_ANY" "$an_i")"; an_i=$((an_i+1));
326
+ else dw="UNASSIGNED"; unassigned=$((unassigned+1)); fi
327
+ fi
328
+ # Exploration (--explore): now and then the neighbouring tier's model instead, with the
329
+ # odds recorded, so `forge dataset routing` can compare two models on tasks neither was
330
+ # routed to by design. The pool's turn above was still taken, so other rows are unchanged.
331
+ if [ -n "$EXPLORE" ] && [ "$dw" != UNASSIGNED ]; then
332
+ local explored
333
+ explored="$(python3 "$SKILL_DIR/scripts/forge-route.py" explore --plan "$PLAN" --task "$id" \
334
+ --tier "$diff" --epsilon "$EXPLORE" --default "$dw" --pools "$PLAN/fractal-routing.json" 2>/dev/null)"
335
+ [ -z "$explored" ] || dw="$explored"
336
+ fi
337
+ fi
338
+ # resolve qa: the same grammar as the dwarf, a comma-list round-robins within its tier.
339
+ # Written verbatim, a list reached dispatch as one spec ('opus,sol' is not a known
340
+ # alias) and every review of the run failed before a model was called.
341
+ if [ "$qa" = "-" ] || [ -z "$qa" ]; then
342
+ case "$diff" in
343
+ high) if [ -n "$Q_HI" ]; then qa="$(pool_pick "$Q_HI" "$qhi_i")"; qhi_i=$((qhi_i+1)); fi ;;
344
+ medium) if [ -n "$Q_MD" ]; then qa="$(pool_pick "$Q_MD" "$qmd_i")"; qmd_i=$((qmd_i+1)); fi ;;
345
+ low) if [ -n "$Q_LO" ]; then qa="$(pool_pick "$Q_LO" "$qlo_i")"; qlo_i=$((qlo_i+1)); fi ;;
346
+ esac
347
+ if [ "$qa" = "-" ] || [ -z "$qa" ]; then
348
+ if [ -n "$Q_ANY" ]; then qa="$(pool_pick "$Q_ANY" "$qan_i")"; qan_i=$((qan_i+1)); else qa="opus"; fi
349
+ fi
350
+ fi
351
+ # A column runs one spec; a list there would only fail later, at its first dispatch.
352
+ case "$dw" in *,*) die "task '$id': the dwarf column names one spec, not a list ('$dw'); pool models with a --dwarf flag" ;; esac
353
+ case "$qa" in *,*) die "task '$id': the qa column names one spec, not a list ('$qa'); pool reviewers with a --qa flag" ;; esac
354
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$id" "$deps" "$diff" "$files" "$dw" "$qa" "$title" >> "$tmp"
355
+ done < "$T"
356
+ mv "$tmp" "$T"
357
+
358
+ compute_waves "$PLAN"
359
+
360
+ # --- clef: silent first-attempt predictions -------------------------------------
361
+ # Recorded only (tasks/<id>/clef.first_pass and the run dataset), so a later export can
362
+ # measure whether Clef tells which tasks a first attempt will fail. Nothing in a run
363
+ # reads them. Without a Cloudflare provider the command returns at once; with one it
364
+ # bounds itself (one deadline for the plan, an in-process watchdog).
365
+ if [ "${FORGE_JEV:-}" != off ]; then
366
+ python3 "$SKILL_DIR/scripts/forge-jev.py" first-pass --plan "$PLAN" --repo "$REPO" >/dev/null 2>&1 || true
367
+ fi
368
+
369
+ # --- jev: advisory wave coupling ------------------------------------------------
370
+ # Disjoint files bound parallelism structurally, but they cannot see the conflict
371
+ # where one task in a wave changes an interface another task in the SAME wave
372
+ # consumes -- both diffs are clean against their own baseline, and the merge is
373
+ # broken. This is advisory only: it never edits tasks.tsv, never adds a dep, and
374
+ # never changes waves.tsv, so it must run after compute_waves and before the table
375
+ # that reports the plan is printed.
376
+ if [ "$jev_loaded" = 1 ] && forge_jev_active gates; then
377
+ local jev_coupling jev_coupling_rc
378
+ jev_coupling="$(python3 "$SKILL_DIR/scripts/forge-jev.py" coupling \
379
+ --plan "$PLAN" --repo "$REPO" 2>/dev/null)"
380
+ jev_coupling_rc=$?
381
+ if [ "$jev_coupling_rc" -eq 0 ] && [ -n "$jev_coupling" ]; then
382
+ if [ "${FORGE_JEV_SHADOW:-}" = on ]; then
383
+ note "jev: found $(printf '%s\n' "$jev_coupling" | grep -c .) coupled pair(s) in shadow mode (no change)"
384
+ else
385
+ local ca cb cp ckind
386
+ while IFS="$(printf '\t')" read -r ca cb cp ckind; do
387
+ [ -n "$ca" ] || continue
388
+ # Two different failures, so two different sentences. same-wave is a broken
389
+ # merge between concurrent tasks; forward is an earlier task written against
390
+ # work that has not been built yet, which its own review cannot catch.
391
+ if [ "$ckind" = forward ]; then
392
+ note "jev: task '$ca' runs before '$cb' and may depend on what it builds ($cp) — consider a dep"
393
+ else
394
+ note "jev: tasks '$ca' and '$cb' may conflict despite disjoint files ($cp) — consider a dep"
395
+ fi
396
+ done <<EOF
397
+ $jev_coupling
398
+ EOF
399
+ fi
400
+ fi
401
+ fi
402
+
403
+ render_table "$PLAN"
404
+ # Before the UNASSIGNED return, so the URL prints under the table either way.
405
+ forge_ui_launch "$PLAN"
406
+
407
+ if [ "$unassigned" -gt 0 ]; then
408
+ note ""
409
+ note "$unassigned task(s) are UNASSIGNED: their difficulty tier has no --dwarf-<tier>"
410
+ note "rule and there is no plain --dwarf fallback. Assign them before running —"
411
+ note "forge will not pick a model that spends your quota on your behalf."
412
+ return 2
413
+ fi
414
+ return 0
415
+ }
416
+
417
+ # --- waves --------------------------------------------------------------------
418
+ # A wave is the largest set of tasks whose deps are already satisfied AND whose
419
+ # file sets are pairwise disjoint. Disjointness is what actually bounds
420
+ # parallelism: two dwarves editing one file produce a conflict no reviewer can
421
+ # untangle, so they get separate waves no matter how the goal was decomposed.
422
+ compute_waves() {
423
+ local PLAN="$1" T="$1/tasks.tsv" W="$1/waves.tsv"
424
+ : > "$W"; : > "$PLAN/deferrals.txt"
425
+ local remaining done_ids wave n id deps files wave_files ok d progressed
426
+ remaining="$(all_ids "$T" | tr '\n' ' ')"
427
+ done_ids=" "; wave=0
428
+ while [ -n "${remaining// /}" ]; do
429
+ wave=$((wave+1)); wave_files=""; progressed=0
430
+ local next_remaining=""
431
+ for id in $remaining; do
432
+ deps="$(field "$T" "$id" deps)"
433
+ files="$(field "$T" "$id" files)"
434
+ ok=1
435
+ if [ "$deps" != "-" ]; then
436
+ for d in $(printf '%s' "$deps" | tr ',' ' '); do
437
+ case "$done_ids" in *" $d "*) ;; *) ok=0 ;; esac
438
+ done
439
+ fi
440
+ if [ "$ok" = 1 ] && lists_overlap "$files" "$wave_files"; then
441
+ ok=0
442
+ printf 'wave %s: %s deferred (file overlap with a task already in this wave)\n' \
443
+ "$wave" "$id" >> "$PLAN/deferrals.txt"
444
+ fi
445
+ if [ "$ok" = 1 ]; then
446
+ printf '%s\t%s\n' "$wave" "$id" >> "$W"
447
+ wave_files="$wave_files $(printf '%s' "$files" | tr ',' ' ')"
448
+ progressed=1
449
+ else
450
+ next_remaining="$next_remaining $id"
451
+ fi
452
+ done
453
+ [ "$progressed" = 1 ] || die "dependency cycle or unsatisfiable deps among:$next_remaining"
454
+ for id in $(awk -F'\t' -v w="$wave" '$1==w {print $2}' "$W"); do done_ids="$done_ids$id "; done
455
+ remaining="$next_remaining"
456
+ done
457
+ }
458
+
459
+ render_table() {
460
+ local PLAN="$1" T="$1/tasks.tsv" W="$1/waves.tsv"
461
+ local run_id; run_id="$(cat "$PLAN/run_id")"
462
+ local n; n="$(all_ids "$T" | wc -l | tr -d ' ')"
463
+ local waves; waves="$(awk -F'\t' '{print $1}' "$W" | sort -u | wc -l | tr -d ' ')"
464
+ echo "run forge/$run_id repo $(cat "$PLAN/repo")"
465
+ [ -s "$PLAN/goal.txt" ] && echo "goal: $(head -1 "$PLAN/goal.txt")"
466
+ local est=$((n*2)) extra=""
467
+ if [ -s "$PLAN/planner" ]; then
468
+ est=$((est+1)); extra=" + 1 planner ($(cat "$PLAN/planner"))"
469
+ fi
470
+ echo "tasks: $n waves: $waves estimated dispatches: $est ($n dwarf + $n qa$extra)"
471
+ if [ -s "$PLAN/shadow_qa" ]; then
472
+ local rate; rate="$(cat "$PLAN/shadow_rate" 2>/dev/null || echo 0.1)"
473
+ echo "shadow review: $(cat "$PLAN/shadow_qa") on a sampled $rate of reviewed attempts" \
474
+ "(about $(awk -v n="$n" -v r="$rate" 'BEGIN { x = n * r; printf "%d", (x == int(x)) ? x : int(x) + 1 }') extra reviews; recorded, never changes a result)"
475
+ fi
476
+ if [ -f "$PLAN/fail_first" ]; then
477
+ if [ -s "$(cat "$PLAN/repo")/.forge/verify-subset" ]; then
478
+ echo "fail-first: each task's new tests also run without its change (advice for the reviewer; never changes a result)"
479
+ else
480
+ echo "fail-first: on, but $(cat "$PLAN/repo")/.forge/verify-subset (a test command with {files}) is missing, so nothing will run"
481
+ fi
482
+ fi
483
+ echo
484
+ local w id dws starred=0
485
+ for w in $(awk -F'\t' '{print $1}' "$W" | sort -un); do
486
+ local cnt; cnt="$(awk -F'\t' -v w="$w" '$1==w' "$W" | wc -l | tr -d ' ')"
487
+ echo "wave $w ($cnt parallel)"
488
+ printf ' %-14s %-7s %-24s %-14s %s\n' id diff dwarf qa files
489
+ for id in $(awk -F'\t' -v w="$w" '$1==w {print $2}' "$W"); do
490
+ dws="$(field "$T" "$id" dwarf)"
491
+ if [ -s "$PLAN/tasks/$id/route.explored" ] && [ "$(cat "$PLAN/tasks/$id/route.explored")" = "$dws" ]; then
492
+ dws="$dws*"; starred=1
493
+ fi
494
+ printf ' %-14s %-7s %-24s %-14s %s\n' \
495
+ "$id" "$(field "$T" "$id" difficulty)" "$dws" \
496
+ "$(field "$T" "$id" qa)" "$(field "$T" "$id" files)"
497
+ # The approach is shown here because this gate is the only moment a human can
498
+ # change the plan for free. After dispatch, changing it costs a whole run.
499
+ if [ -s "$PLAN/tasks/$id/approach.md" ]; then
500
+ sed 's/^/ | /' "$PLAN/tasks/$id/approach.md"
501
+ fi
502
+ done
503
+ echo
504
+ done
505
+ if [ "$starred" = 1 ]; then
506
+ echo "* picked by exploratory routing (--explore $(cat "$PLAN/explore" 2>/dev/null)) instead of the pool's turn;"
507
+ echo " to pin another model, put its spec in the dwarf column of tasks.tsv and re-plan"
508
+ echo
509
+ fi
510
+ local explore drawn=0 decisions; explore="$(cat "$PLAN/explore" 2>/dev/null)"
511
+ # Said only when a row was actually drawn (route.jsonl): with every row named, or no tier
512
+ # with a neighbour, there is no spend to show. LC_ALL=C: a comma-decimal locale prints "0,1".
513
+ for decisions in "$PLAN"/tasks/*/route.jsonl; do [ -e "$decisions" ] && { drawn=1; break; }; done
514
+ if [ -n "$explore" ] && [ "$drawn" = 1 ] && LC_ALL=C awk -v v="$explore" 'BEGIN { exit !(v + 0 > 0) }'; then
515
+ echo "exploration on (--explore $explore): a row left to forge gets its neighbouring tier's model with"
516
+ echo " probability $(LC_ALL=C awk -v v="$explore" 'BEGIN { print v / 2 }'), odds logged for \`forge dataset routing\`; --explore 0 turns it off for this plan"
517
+ echo
518
+ fi
519
+ report_size "$PLAN"
520
+ if [ -s "$PLAN/deferrals.txt" ]; then
521
+ echo "deferrals (serialized to protect the merge):"
522
+ sed 's/^/ /' "$PLAN/deferrals.txt"
523
+ echo
524
+ fi
525
+ }
526
+
527
+ # --- capsule ------------------------------------------------------------------
528
+ # Every dispatch is a clean slate, which keeps one bad turn from poisoning another
529
+ # task but leaves each dwarf blind to the wider run. The capsule is the fix, and it
530
+ # is regenerated per dispatch because status changes as waves land. Two failures it
531
+ # exists to prevent: reimplementing something an earlier task already merged, and
532
+ # editing a file another dwarf currently owns.
533
+ task_status() { # task_status <plan> <id>
534
+ local PLAN="$1" id="$2"
535
+ [ -f "$PLAN/tasks/$id/merged" ] && { echo MERGED; return; }
536
+ [ -f "$PLAN/tasks/$id/status" ] && { cat "$PLAN/tasks/$id/status"; return; }
537
+ echo PENDING
538
+ }
539
+
540
+ dependencies_ready() {
541
+ local plan="$1" id="$2" dep
542
+ for dep in $(field "$plan/tasks.tsv" "$id" deps | tr ',' ' '); do
543
+ [ "$dep" = - ] && continue
544
+ if [ "$(task_status "$plan" "$dep")" != MERGED ]; then
545
+ echo BLOCKED > "$plan/tasks/$id/status"
546
+ note "$id: BLOCKED by $dep ($(task_status "$plan" "$dep"))"
547
+ return 1
548
+ fi
549
+ done
550
+ }
551
+
552
+ preflight_plan() {
553
+ local plan="$1" id role flags
554
+ export FORGE_CAPABILITY_CACHE="$plan/capabilities"
555
+ for id in $(all_ids "$plan/tasks.tsv"); do
556
+ for role in dwarf qa; do
557
+ flags=""; [ -f "$plan/yolo_$role" ] && flags=--yolo
558
+ /bin/bash "$DISPATCH" doctor --spec "$(field "$plan/tasks.tsv" "$id" "$role")" --role "$role" $flags || return $?
559
+ done
560
+ done
561
+ if [ -s "$plan/planner" ]; then
562
+ /bin/bash "$DISPATCH" doctor --spec "$(cat "$plan/planner")" --role planner || return $?
563
+ fi
564
+ # A shadow reviewer that cannot run here is dropped with a note, never a failed run.
565
+ if [ -s "$plan/shadow_qa" ]; then
566
+ python3 "$SKILL_DIR/scripts/forge-shadow.py" preflight --plan "$plan" || true
567
+ fi
568
+ }
569
+
570
+ write_capsule() { # write_capsule <plan> <id> <role>
571
+ local PLAN="$1" me="$2" role="$3" T="$1/tasks.tsv"
572
+ local run_id; run_id="$(cat "$PLAN/run_id")"
573
+ local n; n="$(all_ids "$T" | wc -l | tr -d ' ')"
574
+ # Per task, NOT one shared file. Tasks in a wave run concurrently, so a single
575
+ # $PLAN/capsule.md is a race: each task overwrites it and then reads back
576
+ # whichever sibling wrote last. That silently hands a dwarf someone else's task
577
+ # description — observed in testing before this was split per task.
578
+ local out="$PLAN/tasks/$me/capsule.md" id st
579
+ {
580
+ echo "# forge run $run_id"
581
+ [ -s "$PLAN/goal.txt" ] && { echo; echo "Overall goal: $(cat "$PLAN/goal.txt")"; }
582
+ echo
583
+ echo "This run is split into $n tasks worked by different models. You are handling ONE of"
584
+ echo "them. Do not do the other tasks' work."
585
+ echo
586
+ if [ "$role" = qa ]; then
587
+ echo "## Under review"
588
+ else
589
+ echo "## Your task"
590
+ fi
591
+ echo "$me — $(field "$T" "$me" title) (difficulty: $(field "$T" "$me" difficulty))"
592
+ echo "Files this task owns: $(field "$T" "$me" files)"
593
+ echo
594
+ echo "## Run status"
595
+ printf '%-14s %-7s %-9s %s\n' id diff status files
596
+ for id in $(all_ids "$T"); do
597
+ if [ "$id" = "$me" ]; then st="THIS ONE"
598
+ elif [ -s "$PLAN/tasks/$id/reviewed.commit" ] && git -C "$(cat "$PLAN/repo")" merge-base --is-ancestor "$(cat "$PLAN/tasks/$id/reviewed.commit")" "$(cat "$PLAN/tasks/$me/base_ref")"; then st=MERGED
599
+ else st="NOT IN BASE"; fi
600
+ printf '%-14s %-7s %-9s %s\n' "$id" "$(field "$T" "$id" difficulty)" "$st" "$(field "$T" "$id" files)"
601
+ done
602
+ echo
603
+ echo "## Ground rules"
604
+ echo "- Your branch already contains every MERGED task's work. Do not reimplement it."
605
+ echo "- MERGED means present in this task's pinned baseline; later merges are not included."
606
+ echo "- The review diff is only this task's work against that baseline."
607
+ echo "- Implementation must respect other tasks' ownership; report necessary scope changes."
608
+
609
+ } > "$out"
610
+ echo "$out"
611
+ }
612
+
613
+ # --- one task -----------------------------------------------------------------
614
+ # Idempotent by construction: it may be entered on a fresh task, on a resumed run
615
+ # whose worktree already exists, or on a retry of a task that already failed once.
616
+ # The original version created the worktree unconditionally, which made a
617
+ # decomposed run a one-shot — a failed task's preserved branch had nothing that
618
+ # could act on it, and an interrupted run could not be continued.
619
+ # A fresh worktree is a bare checkout: no node_modules, no venv, no build output,
620
+ # nothing a package manager put there. Every dwarf in the run then rediscovers that
621
+ # the same way — by running the test command, watching it fail, and working out the
622
+ # install step — and pays it again. In one observed 6-task run that tax showed up as
623
+ # four separate "this worktree has no node_modules" entries in project memory and
624
+ # minutes per dispatch.
625
+ #
626
+ # Forge deliberately does NOT guess the fix. The obvious guess, symlinking the source
627
+ # repo's node_modules into the worktree, is actively wrong in a workspace monorepo:
628
+ # the workspace links inside it point back at the source checkout, so the worktree
629
+ # silently builds against the source repo's packages instead of its own, and the
630
+ # resulting type errors name files the task never touched. Copying is correct but can
631
+ # be gigabytes per task.
632
+ #
633
+ # So the repo says what its worktrees need, and forge runs it verbatim: `.forge/setup`
634
+ # (executable, or any file whose contents are a shell command), or `--setup <command>`
635
+ # to override. It runs once per worktree, from the worktree root, only on creation —
636
+ # a resumed run or a retry reuses the existing worktree and does not pay it again.
637
+ # Always succeeds: "no setup configured" is the normal case, not a failure. Returning
638
+ # the status of the last test instead would make the common path report an error, and
639
+ # under a caller running `set -e` that aborts the task before the worktree is usable.
640
+ worktree_setup_command() { # worktree_setup_command <plan> <repo>
641
+ local PLAN="$1" REPO="$2"
642
+ if [ -s "$PLAN/setup_cmd" ]; then cat "$PLAN/setup_cmd"
643
+ elif [ -f "$REPO/.forge/setup" ]; then cat "$REPO/.forge/setup"
644
+ fi
645
+ return 0
646
+ }
647
+
648
+ # Non-fatal by design. A setup step that fails leaves a worktree the dwarf can still
649
+ # work in — it just has to install things itself, which is the status quo this exists
650
+ # to improve on. Failing the task instead would turn a slow run into a dead one.
651
+ run_worktree_setup() { # run_worktree_setup <plan> <repo> <wt> <tdir> <id>
652
+ local PLAN="$1" REPO="$2" wt="$3" tdir="$4" id="$5" cmd
653
+ cmd="$(worktree_setup_command "$PLAN" "$REPO")"
654
+ [ -n "$cmd" ] || return 0
655
+ if (cd "$wt" && eval "$cmd") >"$tdir/setup.out" 2>&1; then
656
+ note "$id: worktree setup ok"
657
+ else
658
+ note "$id: worktree setup failed (continuing) — see $tdir/setup.out"
659
+ fi
660
+ }
661
+
662
+ ensure_worktree() { # ensure_worktree <repo> <wt> <br> <base> <tdir> <plan> <id>
663
+ local REPO="$1" wt="$2" br="$3" base="$4" tdir="$5" PLAN="$6" id="$7"
664
+ # A worktree directory deleted by hand leaves its registration behind, and that
665
+ # registration then blocks `worktree add` with "already exists". Prune first.
666
+ (cd "$REPO" && git worktree prune >/dev/null 2>&1)
667
+ if [ -e "$wt/.git" ]; then
668
+ return 0
669
+ elif (cd "$REPO" && git rev-parse --verify --quiet "$br" >/dev/null 2>&1); then
670
+ (cd "$REPO" && git worktree add "$wt" "$br") >"$tdir/worktree.out" 2>&1
671
+ else
672
+ (cd "$REPO" && git worktree add -b "$br" "$wt" "$base") >"$tdir/worktree.out" 2>&1
673
+ fi || return 1
674
+ run_worktree_setup "$PLAN" "$REPO" "$wt" "$tdir" "$id"
675
+ }
676
+
677
+ dispatch_duration() { # dispatch_duration <tdir> <role>
678
+ sed -n 's/^duration_s=//p' "$1/$2.resolved" 2>/dev/null | tail -1
679
+ }
680
+
681
+ # `files` is documented as a promise, not a prediction: it decides which tasks may
682
+ # run concurrently and what the capsule declares off limits. Nothing used to check
683
+ # it, so an understated set surfaced much later as a merge CONFLICT with no stated
684
+ # cause. The answer is free right after the diff capture, so take it there.
685
+ #
686
+ # A warning, never a failure: a dwarf that genuinely had to touch one more file did
687
+ # the right thing, and failing the task for it would be worse than reporting it.
688
+ check_drift() { # check_drift <declared-comma-list> <tdir> <wt> <base>
689
+ local declared="$1" tdir="$2" wt="$3" base="$4" touched f d covered
690
+ : > "$tdir/drift.txt"
691
+ touched="$(cd "$wt" && git diff --cached --name-only "$base" -- . ":(exclude).forge" 2>/dev/null)"
692
+ [ -n "$touched" ] || return 0
693
+ for f in $touched; do
694
+ covered=0
695
+ if [ "$declared" != "-" ] && [ -n "$declared" ]; then
696
+ for d in $(printf '%s' "$declared" | tr ',' ' '); do
697
+ # Exact match, or a declared directory prefix: declaring src/routes covers
698
+ # src/routes/api.py, which is how the column is normally written.
699
+ case "$f" in
700
+ "$d"|"$d"/*) covered=1 ;;
701
+ esac
702
+ done
703
+ fi
704
+ [ "$covered" = 0 ] && printf '%s\n' "$f" >> "$tdir/drift.txt"
705
+ done
706
+ [ -s "$tdir/drift.txt" ] || rm -f "$tdir/drift.txt"
707
+ return 0
708
+ }
709
+
710
+ do_task_body() (
711
+ local PLAN="${1:?}" id="${2:?}"
712
+ local T="$PLAN/tasks.tsv" REPO run_id base wt br tdir attempt resume=""
713
+ local reviewed review source_fp review_fp
714
+ REPO="$(cat "$PLAN/repo")"; run_id="$(cat "$PLAN/run_id")"
715
+ tdir="$PLAN/tasks/$id"; mkdir -p "$tdir"
716
+ if [ -s "$PLAN/fractal-selection.json" ]; then
717
+ forge_fractal_select "$PLAN" "$REPO" parallel 0 || return $?
718
+ fi
719
+ forge_metric_begin "$tdir" task
720
+ dependencies_ready "$PLAN" "$id" || return 1
721
+ echo RUNNING > "$tdir/status"
722
+ rm -f "$tdir/noretry"
723
+ [ -f "$PLAN/no_memory" ] && export FORGE_MEMORY=off
724
+ # --timeout given to run/retry is kept in the plan and wins over the environment.
725
+ [ -s "$PLAN/timeout" ] && export FORGE_TIMEOUT="$(cat "$PLAN/timeout")"
726
+ wt="$(cat "$PLAN/wt_root")/$id"
727
+ br="forge/$run_id/$id"
728
+ # Every attempt gets a distinct number and commit message. A QA-only resume (the
729
+ # reviewer hit an infrastructure failure, or never reached a verdict) re-reviews work
730
+ # that is already committed, so it neither runs the dwarf nor spends an attempt.
731
+ attempt="$(cat "$tdir/attempt" 2>/dev/null || echo 0)"
732
+ if [ -f "$tdir/resume" ]; then
733
+ if resume_valid "$tdir" "$wt"; then
734
+ resume=qa
735
+ else
736
+ rm -f "$tdir/resume"
737
+ note "$id: the saved review state no longer matches the worktree — running the full task"
738
+ fi
739
+ fi
740
+ if [ -z "$resume" ]; then
741
+ attempt=$(( attempt + 1 ))
742
+ echo "$attempt" > "$tdir/attempt"
743
+ # Silent: Clef's guess at whether this retry passes with the findings it was handed.
744
+ # Only a real new dwarf attempt (not a QA-only resume); recorded in clef.retry_pass.<n>
745
+ # and the run dataset, never read by the run, and it can never fail the task.
746
+ if [ "$attempt" -gt 1 ] && [ -s "$tdir/retry_findings.md" ] && [ "${FORGE_JEV:-}" != off ]; then
747
+ python3 "$SKILL_DIR/scripts/forge-jev.py" retry-pass --plan "$PLAN" --repo "$REPO" --task "$id" \
748
+ --attempt "$attempt" >/dev/null 2>&1 || true
749
+ fi
750
+ fi
751
+
752
+ # The base is pinned per task on first creation and reused by every retry. The
753
+ # integration branch moves as later waves land, so re-reading the run-level
754
+ # base_ref on a retry would pull other tasks' merged code into this task's diff
755
+ # and put it in front of its reviewer as if this dwarf had written it.
756
+ if [ -s "$tdir/base_ref" ]; then
757
+ base="$(cat "$tdir/base_ref")"
758
+ else
759
+ base="$(git -C "$(cat "$PLAN/wt_root")/_integration" rev-parse HEAD)"; printf '%s\n' "$base" > "$tdir/base_ref"
760
+ fi
761
+
762
+ local dw qa yd yq rc
763
+ dw="$(field "$T" "$id" dwarf)"; qa="$(field "$T" "$id" qa)"
764
+ yd=""; yq=""
765
+ [ -f "$PLAN/yolo_dwarf" ] && yd="--yolo"
766
+ [ -f "$PLAN/yolo_qa" ] && yq="--yolo"
767
+
768
+ if ! ensure_worktree "$REPO" "$wt" "$br" "$base" "$tdir" "$PLAN" "$id"; then
769
+ echo ERROR > "$tdir/status"
770
+ note "$id: could not create worktree — $(tail -1 "$tdir/worktree.out")"
771
+ return 1
772
+ fi
773
+
774
+ if [ -z "$resume" ]; then
775
+ # Persist each source separately; QA receives complete context once.
776
+ write_capsule "$PLAN" "$id" dwarf >/dev/null
777
+ cp "$tdir/capsule.md" "$tdir/baseline.capsule"
778
+ # --task lets Jev narrow the injected slice to facts that bear on THIS task. Without
779
+ # it, or with Jev off, the full role slice is injected exactly as before.
780
+ /bin/bash "$MEMORY" inject "$REPO" dwarf --task "$tdir/prompt.md" \
781
+ --run-dir "$tdir" > "$tdir/dwarf.memory"
782
+ {
783
+ python3 "$SKILL_DIR/scripts/forge-prompt.py" --requirements "$tdir/prompt.md" --capsule "$tdir/baseline.capsule" --approach "$tdir/approach.md" --retry "$tdir/retry_findings.md" --memory "$tdir/dwarf.memory"
784
+ echo "Implement this task in the repository. Follow the intended approach unless it is wrong; explain deviations. Run the task's requested verification and report results."
785
+ echo "On retry, fix the findings in the existing work; preserve parts not criticised. Explain any finding you reject."
786
+ echo; python3 "$SKILL_DIR/scripts/forge-contract.py" dwarf
787
+ echo; /bin/bash "$MEMORY" note dwarf
788
+ } > "$tdir/dwarf.input"
789
+ guard_before_dwarf "$wt" "$tdir"
790
+ forge_metric_phase dispatch
791
+ /bin/bash "$DISPATCH" dwarf "$dw" --repo "$wt" --run-dir "$tdir" \
792
+ --ripwire-query-file "$tdir/prompt.md" --prompt-file "$tdir/dwarf.input" $yd --output "${FORGE_OUTPUT:-summary}" >"$tdir/dwarf.out" 2>&1
793
+ rc=$?
794
+ if [ "${FORGE_OUTPUT:-summary}" = full ]; then cat "$tdir/dwarf.out"; fi
795
+ if [ "$rc" -ne 0 ]; then
796
+ case "$rc" in
797
+ 7) echo TIMEOUT > "$tdir/status"; note "$id: dwarf hit the dispatch timeout — see $tdir/dwarf.out" ;;
798
+ 8) infra_stop "$tdir" "$id" dwarf "$attempt"; return 8 ;;
799
+ *) echo ERROR > "$tdir/status"; note "$id: dwarf dispatch failed — see $tdir/dwarf.out" ;;
800
+ esac
801
+ return 1
802
+ fi
803
+
804
+ /bin/bash "$MEMORY" record "$REPO" --last "$tdir/dwarf.last" --role dwarf \
805
+ --run-id "$run_id" --task "$id" --model "$dw" --run-dir "$tdir" \
806
+ --duration "$(dispatch_duration "$tdir" dwarf)" >/dev/null 2>&1
807
+
808
+ guard_after_dwarf "$wt" "$tdir" "$id"
809
+ forge_metric_phase snapshot
810
+ # Capture the real diff, including new files, then commit on the task branch.
811
+ # Diffed against the task's pinned base rather than the branch head, so a retry
812
+ # hands QA the task's CUMULATIVE work. Reviewing only the fix would let the
813
+ # first attempt's code through unread.
814
+ # .forge/ is forge's own memory, rewritten in the user's tree after every run.
815
+ # Left in, it would reach QA as if a dwarf had written it, and any task whose
816
+ # files touched it would collide with every other task in the wave planner.
817
+ if [ -z "${FORGE_FRACTAL_RUN:-}" ] || [ "${FORGE_FRACTAL_RETRY:-}" = 1 ]; then
818
+ [ -s "$tdir/changes.diff" ] && mv "$tdir/changes.diff" "$tdir/changes.prev.diff"
819
+ fi
820
+ (cd "$wt" && git add -A -- . ":(exclude).forge" \
821
+ && git diff --cached --binary "$base" -- . ":(exclude).forge") > "$tdir/changes.diff" 2>/dev/null || { echo ERROR > "$tdir/status"; return 1; }
822
+ if [ ! -s "$tdir/changes.diff" ]; then
823
+ # A harness that exited 0 with an empty final message and changed nothing did not
824
+ # decline the task; it never really ran (login, quota, a dropped connection).
825
+ if grep -qx 'class=empty' "$tdir/dwarf.infra" 2>/dev/null; then
826
+ infra_stop "$tdir" "$id" dwarf "$attempt"; return 8
827
+ fi
828
+ echo FAIL > "$tdir/status"
829
+ : > "$tdir/noretry"
830
+ echo "The dwarf produced no changes at all." > "$tdir/qa.last"
831
+ note "$id: dwarf produced an empty diff"; return 1
832
+ fi
833
+ if [ -s "$tdir/changes.prev.diff" ] && cmp -s "$tdir/changes.diff" "$tdir/changes.prev.diff"; then
834
+ echo FAIL > "$tdir/status"
835
+ : > "$tdir/noretry"
836
+ echo "The retry changed nothing: the diff is byte-identical to the previous attempt." \
837
+ > "$tdir/qa.last"
838
+ note "$id: retry produced no new changes — not re-reviewing identical code"; return 1
839
+ fi
840
+
841
+ if ! git -C "$wt" diff --cached --quiet "$base" -- .forge; then
842
+ echo FAIL > "$tdir/status"; note "$id: changed excluded Forge memory; cannot accept an unreviewed change"; return 1
843
+ fi
844
+ check_drift "$(field "$T" "$id" files)" "$tdir" "$wt" "$base"
845
+ if [ -s "$tdir/drift.txt" ]; then
846
+ note "$id: touched undeclared file(s): $(tr '\n' ' ' < "$tdir/drift.txt")"
847
+ fi
848
+
849
+ (cd "$wt" && git -c user.name=forge -c user.email=forge@local \
850
+ commit --allow-empty -q -m "forge($id) attempt $attempt: $(field "$T" "$id" title)") >/dev/null 2>&1 || {
851
+ echo ERROR > "$tdir/status"; return 1;
852
+ }
853
+ reviewed="$(git -C "$wt" rev-parse HEAD)" || return 1
854
+ git -C "$wt" diff --binary "$base" "$reviewed" -- . ':(exclude).forge' > "$tdir/committed.diff" || return 1
855
+ if ! cmp -s "$tdir/changes.diff" "$tdir/committed.diff" ||
856
+ [ "$(forge_tree "$wt")" != "$(git -C "$wt" rev-parse "$reviewed^{tree}")" ]; then
857
+ echo INVALIDATED > "$tdir/status"; return 1
858
+ fi
859
+ echo "$reviewed" > "$tdir/reviewed.commit"
860
+ else
861
+ reviewed="$(cat "$tdir/reviewed.commit")"
862
+ fi
863
+ review="$tdir/review-$attempt-$$"
864
+ forge_review_snapshot "$wt" "$base" "$reviewed" "$review" || { echo ERROR > "$tdir/status"; return 1; }
865
+ source_fp="$(forge_fingerprint "$wt")" || return 1
866
+ review_fp="$(forge_fingerprint "$review")" || return 1
867
+ echo "$source_fp" > "$tdir/source.fingerprint"
868
+ echo "$review_fp" > "$tdir/review.fingerprint"
869
+
870
+ forge_metric_phase preparation
871
+ local jev_loaded_qa=0 subset_cmd="" qa_thr
872
+ qa_thr="$(qa_threshold_for "$PLAN" "$id")"
873
+
874
+ # A SEPARATE file from .forge/verify, and opt-in. It must contain {files}, because
875
+ # only the project knows how its runner takes a file list -- `pytest -q {files}` works,
876
+ # `npm test -- {files}` needs the dashes, and `go test ./...` cannot do it at all.
877
+ # Absent, this whole feature stays off and nothing extra runs.
878
+ [ -s "$REPO/.forge/verify-subset" ] && subset_cmd="$(cat "$REPO/.forge/verify-subset")"
879
+ if [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ]; then
880
+ source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && jev_loaded_qa=1
881
+ fi
882
+ # Early feedback for the reviewer: run the tests this change most likely broke, in a
883
+ # throwaway export. Requires a configured verify command -- without one there is no
884
+ # runner to point at a file list, and inventing one is not this feature's job.
885
+ if [ -n "$subset_cmd" ] && [ "$jev_loaded_qa" = 1 ] && forge_jev_active tests; then
886
+ git -C "$wt" diff --name-only "$(cat "$tdir/review.base")" "$reviewed" \
887
+ > "$tdir/changed.txt" 2>/dev/null || : > "$tdir/changed.txt"
888
+ python3 "$SKILL_DIR/scripts/forge-jev.py" test-subset --repo "$wt" \
889
+ --commit "$reviewed" --task "$tdir/prompt.md" --command "$subset_cmd" \
890
+ --changed "$tdir/changed.txt" --base "$(cat "$tdir/review.base")" \
891
+ --run-dir "$tdir" > "$tdir/subset.jev.txt" 2>/dev/null \
892
+ || : > "$tdir/subset.jev.txt"
893
+ fi
894
+ # Opt-in (plan --fail-first): do this attempt's new tests fail without its change? Read-only
895
+ # on the review snapshot, like the subset run above; the reviewer gets the evidence.
896
+ rm -f "$tdir/failfirst.json" "$tdir/failfirst.txt"
897
+ [ ! -f "$PLAN/fail_first" ] || forge_fail_first "$review" "$tdir" "$REPO"
898
+ # QA uses the dispatch-time ownership and baseline, never later merge state.
899
+ /bin/bash "$MEMORY" inject "$REPO" qa --task "$tdir/prompt.md" \
900
+ --run-dir "$tdir" > "$tdir/qa.memory"
901
+ {
902
+ python3 "$SKILL_DIR/scripts/forge-prompt.py" --requirements "$tdir/prompt.md" --capsule "$tdir/baseline.capsule" --approach "$tdir/approach.md" --retry "$tdir/retry_findings.md" --memory "$tdir/dwarf.memory" --memory "$tdir/qa.memory"
903
+ python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part head --kind task
904
+ echo
905
+ if [ -s "$tdir/failfirst.txt" ]; then cat "$tdir/failfirst.txt"; echo; fi
906
+ # A subset failure is the one thing a reviewer most wants to know and cannot see
907
+ # from the diff. It runs in a throwaway export of the reviewed commit, so the task
908
+ # worktree is untouched and no fingerprint can change. It never replaces
909
+ # verification -- the full suite still runs at integration exactly as before.
910
+ if [ -s "$tdir/subset.jev.txt" ]; then
911
+ cat "$tdir/subset.jev.txt"
912
+ echo
913
+ echo "That is a subset chosen by relevance, not the full suite, so a pass would"
914
+ echo "prove nothing and is not reported. A failure here is real."
915
+ echo
916
+ fi
917
+ if [ -s "$tdir/guard.txt" ]; then
918
+ # Facts forge observed about how the implementer worked. The reviewer never sees the
919
+ # implementer's own report, so this is the only way an unfinished measurement or a
920
+ # stray process reaches the review.
921
+ echo "Implementer guard notes (observed by forge, not claimed by the implementer):"
922
+ sed 's/^/ - /' "$tdir/guard.txt"
923
+ echo "A promise of later work means a check may never have run; judge the diff on its own merits."
924
+ echo
925
+ fi
926
+ if [ -s "$tdir/drift.txt" ]; then
927
+ # The reviewer is already asked about scope and had no way to know the answer.
928
+ echo "Scope note: this task declared it would touch $(field "$T" "$id" files), and the"
929
+ echo "diff also changes files it did not declare:"
930
+ sed 's/^/ - /' "$tdir/drift.txt"
931
+ echo "That is not automatically wrong — judge whether each was actually necessary. It"
932
+ echo "matters because another task may own those files."
933
+ echo
934
+ fi
935
+ python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part tail --kind task ${qa_thr:+--threshold "$qa_thr"}
936
+ echo
937
+ echo '```diff'; cat "$tdir/changes.diff"; echo '```'
938
+ echo; /bin/bash "$MEMORY" note qa
939
+ echo "Place any learning notes before the final FORGE_VERDICT: PASS or FORGE_VERDICT: FAIL line."
940
+ } > "$tdir/qa.input"
941
+ # Opt-in (plan --shadow-qa): a second, independent review of this same diff, to measure
942
+ # how often two reviewers agree. It reviews its own snapshot from its own dir under
943
+ # $PLAN/shadow/, and nothing in this script ever reads its result.
944
+ local shadow_pid=""
945
+ if [ -s "$PLAN/shadow_qa" ] && [ -z "${FORGE_FRACTAL_RUN:-}" ]; then
946
+ python3 "$SKILL_DIR/scripts/forge-shadow.py" run --plan "$PLAN" --task "$id" --worktree "$wt" \
947
+ --base "$base" --reviewed "$reviewed" --attempt "$attempt" --qa "$qa" --tdir "$tdir" ${yq:+--yolo} \
948
+ </dev/null >/dev/null 2>&1 &
949
+ shadow_pid=$!
950
+ fi
951
+ forge_metric_phase dispatch
952
+ /bin/bash "$DISPATCH" qa "$qa" --repo "$review" --run-dir "$tdir" \
953
+ --review-base "$(cat "$tdir/review.base")" --ripwire-query-file "$tdir/prompt.md" --prompt-file "$tdir/qa.input" $yq --output "${FORGE_OUTPUT:-summary}" >"$tdir/qa.out" 2>&1
954
+ rc=$?
955
+ if [ -n "$shadow_pid" ]; then
956
+ # On every path below, before any status is written: the shadow learns the primary is
957
+ # done (which bounds how much longer it may take), and no shadow outlives its task, so
958
+ # a retry can never start while one is still reviewing. It can delay a task, never change it.
959
+ kill -USR1 "$shadow_pid" 2>/dev/null || true
960
+ wait "$shadow_pid" 2>/dev/null || true
961
+ fi
962
+ if [ "${FORGE_OUTPUT:-summary}" = full ]; then cat "$tdir/qa.out"; fi
963
+ if [ "$rc" -ne 0 ]; then
964
+ case "$rc" in
965
+ 7) echo TIMEOUT > "$tdir/status"; note "$id: qa hit the dispatch timeout — see $tdir/qa.out" ;;
966
+ 8) infra_stop "$tdir" "$id" qa "$attempt"; return 8 ;;
967
+ *) echo ERROR > "$tdir/status"; note "$id: qa dispatch failed — see $tdir/qa.out" ;;
968
+ esac
969
+ return 1
970
+ fi
971
+ rm -f "$tdir/resume"
972
+
973
+ forge_metric_phase verification
974
+ if [ "$(forge_fingerprint "$wt")" != "$source_fp" ] || [ "$(forge_fingerprint "$review")" != "$review_fp" ]; then
975
+ echo INVALIDATED > "$tdir/status"; note "$id: artifact changed during review"; return 1
976
+ fi
977
+ local verdict
978
+ verdict="$(forge_verdict "$tdir/qa.last")"
979
+ /bin/bash "$MEMORY" record "$REPO" --last "$tdir/qa.last" --role qa \
980
+ --run-id "$run_id" --task "$id" --model "$qa" --verdict "${verdict:-UNKNOWN}" \
981
+ --run-dir "$tdir" \
982
+ --duration "$(dispatch_duration "$tdir" qa)" >/dev/null 2>&1
983
+
984
+ case "$verdict" in
985
+ PASS) echo PASS > "$tdir/status" ;;
986
+ FAIL) echo FAIL > "$tdir/status" ;;
987
+ # No verdict means the reviewer never reached a conclusion. Treating that as a
988
+ # pass would merge unreviewed code, so it is fail-safe by construction.
989
+ *) echo UNKNOWN > "$tdir/status" ;;
990
+ esac
991
+ # With an explicit --qa-threshold a FAIL made only of tolerated findings becomes an
992
+ # accepted PASS-with-known-issues. No threshold, no change.
993
+ qa_gate "$PLAN" "$id" "$tdir" "$verdict"
994
+
995
+ # Annotate a FAIL that looks unsound. Note what this does NOT do: the status file is
996
+ # already written above and nothing below rewrites it. A model that could talk a
997
+ # reviewer out of a FAIL would produce confident PASSes on code nobody checked, which
998
+ # is worth less than having no reviewer at all. Only a human acts on this line.
999
+ if [ "$verdict" = FAIL ] && [ "$(cat "$tdir/status")" = FAIL ]; then
1000
+ # do_task is a subshell and does not inherit do_plan's copy, so source it here.
1001
+ # A missing or broken options file leaves jev_loaded=0 and this block does nothing.
1002
+ local jev_loaded=0 jev_rc
1003
+ if [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ]; then
1004
+ source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && jev_loaded=1
1005
+ fi
1006
+ if [ "$jev_loaded" = 1 ] && forge_jev_active gates; then
1007
+ # One request, not two: asking the same question twice costs twice and can come
1008
+ # back with two different answers, and the record must match what was printed.
1009
+ python3 "$SKILL_DIR/scripts/forge-jev.py" review-triage --review "$tdir/qa.last" \
1010
+ --diff "$tdir/changes.diff" --run-dir "$tdir" --json > "$tdir/qa.jev.json" 2>/dev/null
1011
+ jev_rc=$?
1012
+ if [ "$jev_rc" -eq 0 ]; then
1013
+ note "$id: FAIL stands, but jev flags the review — $(python3 -c '
1014
+ import json, sys
1015
+ try:
1016
+ data = json.load(open(sys.argv[1]))
1017
+ except Exception:
1018
+ sys.exit(0)
1019
+ print("; ".join(data.get("reasons") or []))
1020
+ ' "$tdir/qa.jev.json" 2>/dev/null)"
1021
+ else
1022
+ # Exit 1 means the review looked sound; there is nothing to say and no file to keep.
1023
+ rm -f "$tdir/qa.jev.json" 2>/dev/null || true
1024
+ fi
1025
+ fi
1026
+ fi
1027
+ return 0
1028
+ )
1029
+
1030
+ # Entry point for both the scheduler's `_task` and `retry`: runs the pipeline, then logs
1031
+ # the attempt (including infrastructure stops, which spend nothing) for later audit.
1032
+ do_task() {
1033
+ local rc
1034
+ do_task_body "$@"; rc=$?
1035
+ record_attempt "$1" "$2"
1036
+ # The retry policy's state (escalation.next or escalation.stop), decided now rather than
1037
+ # at the next retry so a spent retry budget still reports it and the scheduler sees it.
1038
+ python3 "$SKILL_DIR/scripts/forge-route.py" escalation --plan "$1" --task "$2" --check >/dev/null 2>&1 || true
1039
+ return $rc
1040
+ }
1041
+
1042
+
1043
+ # Merge one passing task onto the integration branch. Shared by `run` and `retry`
1044
+ # so a retried task lands exactly the way a first-attempt task does.
1045
+ merge_task() { # merge_task <plan> <id> -> 0 merged, 1 conflicted
1046
+ local PLAN="$1" id="$2" REPO run_id int_wt
1047
+ REPO="$(cat "$PLAN/repo")"; run_id="$(cat "$PLAN/run_id")"
1048
+ int_wt="$(cat "$PLAN/wt_root")/_integration"
1049
+ if [ -s "$PLAN/accepted.integration" ] && [ "$(git -C "$int_wt" rev-parse HEAD)" != "$(cat "$PLAN/accepted.integration")" ]; then
1050
+ echo INVALIDATED > "$PLAN/tasks/$id/status"
1051
+ forge_dataset_event merge --dir "$PLAN" --task "$id" --set status=INVALIDATED --set reason=integration-moved
1052
+ return 1
1053
+ fi
1054
+ [ -z "$(git -C "$int_wt" status --porcelain -- . ':(exclude).forge')" ] || {
1055
+ echo INVALIDATED > "$PLAN/tasks/$id/status"
1056
+ forge_dataset_event merge --dir "$PLAN" --task "$id" --set status=INVALIDATED --set reason=integration-dirty
1057
+ return 1;
1058
+ }
1059
+ local tdir="$PLAN/tasks/$id" reviewed wt
1060
+ dependencies_ready "$PLAN" "$id" || return 1
1061
+ reviewed="$(cat "$tdir/reviewed.commit")" || return 1
1062
+ wt="$(cat "$PLAN/wt_root")/$id"
1063
+ if [ "$(forge_fingerprint "$wt")" != "$(cat "$tdir/source.fingerprint")" ] ||
1064
+ [ "$(git -C "$REPO" rev-parse "forge/$run_id/$id")" != "$reviewed" ]; then
1065
+ echo INVALIDATED > "$tdir/status"
1066
+ forge_dataset_event merge --dir "$PLAN" --task "$id" --set status=INVALIDATED --set reason=task-moved
1067
+ return 1
1068
+ fi
1069
+ if (cd "$int_wt" && git merge --no-ff -m "forge: merge $id" "$reviewed" >/dev/null 2>&1); then
1070
+ git -C "$int_wt" rev-parse HEAD > "$PLAN/accepted.integration"
1071
+ touch "$PLAN/tasks/$id/merged"
1072
+ rm -f "$tdir/conflict.files"
1073
+ forge_dataset_event merge --dir "$PLAN" --task "$id" --set status=MERGED --set commit="$reviewed" \
1074
+ --set integration="$(cat "$PLAN/accepted.integration")"
1075
+ return 0
1076
+ fi
1077
+ # Which files collided is the label for whether two tasks were coupled, and the abort
1078
+ # below erases it, so it is kept first.
1079
+ (cd "$int_wt" && git diff --name-only --diff-filter=U) > "$tdir/conflict.files" 2>/dev/null
1080
+ (cd "$int_wt" && git merge --abort >/dev/null 2>&1)
1081
+ echo CONFLICT > "$PLAN/tasks/$id/status"
1082
+ forge_dataset_event merge --dir "$PLAN" --task "$id" --set status=CONFLICT --set commit="$reviewed" \
1083
+ --set conflicts="$(tr '\n' ' ' < "$tdir/conflict.files" 2>/dev/null)"
1084
+ return 1
1085
+ }
1086
+
1087
+ # --- retry --------------------------------------------------------------------
1088
+ # A failed task keeps its branch and worktree "so there is something to inspect and
1089
+ # retry from" — this is the retry. It is a command a human types, not an automatic
1090
+ # repair loop, which is what keeps forge's one-dwarf-then-QA rule intact.
1091
+ do_retry() {
1092
+ local PLAN="${1:?retry needs a plan dir}" id="${2:?retry needs a task id}"; shift 2
1093
+ local NEWDWARF="" jev_rc=0
1094
+ while [ $# -gt 0 ]; do
1095
+ case "$1" in
1096
+ --no-ripwire) export FORGE_RIPWIRE=off; shift ;;
1097
+ --output) OUTPUT="${2:?}"; shift 2 ;;
1098
+ --dwarf) NEWDWARF="${2:?--dwarf needs a spec}"; shift 2 ;;
1099
+ --yolo-dwarf) touch "$PLAN/yolo_dwarf"; shift ;;
1100
+ --yolo-qa) touch "$PLAN/yolo_qa"; shift ;;
1101
+ --verify) printf '%s' "${2:?--verify needs a command}" > "$PLAN/verify_cmd"; shift 2 ;;
1102
+ --setup) printf '%s' "${2:?--setup needs a command}" > "$PLAN/setup_cmd"; shift 2 ;;
1103
+ *)
1104
+ # `case` is not a loop, so `continue` here belongs to the enclosing while.
1105
+ if forge_pp_flag retry "$PLAN" "$@"; then shift "$PP_SHIFT"; continue; fi
1106
+ if [ "$JEV_OPTS" = 1 ]; then
1107
+ forge_jev_flag "$1"; jev_rc=$?
1108
+ case "$jev_rc" in
1109
+ 0) shift "$JEV_SHIFT"; continue ;;
1110
+ 2) exit 2 ;;
1111
+ esac
1112
+ fi
1113
+ die "retry: unknown option '$1'"
1114
+ ;;
1115
+ esac
1116
+ done
1117
+ [ "$JEV_OPTS" = 1 ] && forge_jev_settle "$PLAN"
1118
+ case "$OUTPUT" in summary|full) ;; *) die "--output must be summary or full" ;; esac
1119
+ export FORGE_OUTPUT="$OUTPUT"
1120
+ local T="$PLAN/tasks.tsv" tdir="$PLAN/tasks/$id"
1121
+ [ -s "$T" ] || die "no tasks.tsv in '$PLAN'"
1122
+ all_ids "$T" | grep -qx "$id" || die "no task '$id' in this plan"
1123
+ [ -s "$PLAN/wt_root" ] || die "this plan has never been run — use 'run', not 'retry'"
1124
+ local st; st="$(task_status "$PLAN" "$id")"
1125
+ [ "$st" = "MERGED" ] && die "task '$id' already merged — nothing to retry"
1126
+
1127
+ # Findings carry-forward, the optional --dwarf escalation and the QA-only resume
1128
+ # decision are shared with the scheduler's automatic retries.
1129
+ # A refusal (the retry policy found no stronger model, or the escalation could not be
1130
+ # written) must stop here: past this point the status is cleared and a dwarf dispatched.
1131
+ local prc=0
1132
+ prepare_retry "$PLAN" "$id" "$NEWDWARF" || prc=$?
1133
+ if [ "$prc" != 0 ]; then
1134
+ write_results "$PLAN"
1135
+ [ "$prc" = 3 ] && return 2
1136
+ return "$prc"
1137
+ fi
1138
+ [ "${FORGE_RIPWIRE:-}" != off ] || touch "$PLAN/no_ripwire"
1139
+ dependencies_ready "$PLAN" "$id" || { write_results "$PLAN"; return 5; }
1140
+ [ ! -f "$PLAN/no_ripwire" ] || export FORGE_RIPWIRE=off
1141
+ preflight_plan "$PLAN" || return $?
1142
+ /bin/bash "$SKILL_DIR/scripts/forge-install-ripwire.sh" || true
1143
+ rm -f "$tdir/status"
1144
+
1145
+ note "retrying $id with dwarf $(field "$T" "$id" dwarf)"
1146
+ export FORGE_FRACTAL_RETRY=1
1147
+ export FORGE_DATASET_TRIGGER=human
1148
+ do_task "$PLAN" "$id"
1149
+ st="$(task_status "$PLAN" "$id")"
1150
+ if [ "$st" = "INFRA" ]; then
1151
+ # Nothing was judged and no attempt was spent: skip verification, say how to resume.
1152
+ write_results "$PLAN"
1153
+ echo
1154
+ report_infra "$PLAN"
1155
+ return 8
1156
+ fi
1157
+ if [ "$st" = "PASS" ]; then
1158
+ if merge_task "$PLAN" "$id"; then
1159
+ note "$id PASS -> merged"
1160
+ else
1161
+ note "$id PASS but conflicted on merge — branch preserved"
1162
+ fi
1163
+ fi
1164
+ verify_result "$PLAN" "$(cat "$PLAN/wt_root")/_integration" "$PLAN" || { write_results "$PLAN"; run_report_extras "$PLAN"; return 5; }
1165
+ write_results "$PLAN"
1166
+ echo
1167
+ printf ' %-14s %-9s %s\n' id status branch
1168
+ awk -F'\t' '{printf " %-14s %-9s %s\n", $1, $2, $3}' "$PLAN/results.tsv"
1169
+ forge_jev_review_flags "$PLAN"
1170
+ run_report_extras "$PLAN"
1171
+ [ "$(task_status "$PLAN" "$id")" = "MERGED" ] || return 5
1172
+ return 0
1173
+ }
1174
+
1175
+ # Print any Jev review-triage flags under the results table.
1176
+ #
1177
+ # do_task's notes go to tasks/<id>/task.out, which the scheduler only echoes when
1178
+ # FORGE_OUTPUT=full -- the right call for parallel dispatch, where interleaved output is
1179
+ # unreadable. But it meant this particular note was written where nobody would read it,
1180
+ # and it is the only signal that a FAIL may not be sound. A human has to see it to act
1181
+ # on it, and the default output mode is `summary`.
1182
+ #
1183
+ # qa.jev.json exists only when the review was flagged: review-triage deletes it when the
1184
+ # review looks sound, so its presence is the signal and no new file is needed.
1185
+ forge_jev_review_flags() {
1186
+ local plan="$1" id notice printed=0
1187
+ for id in $(all_ids "$plan/tasks.tsv"); do
1188
+ [ -s "$plan/tasks/$id/qa.jev.json" ] || continue
1189
+ notice="$(python3 -c '
1190
+ import json, sys
1191
+ try:
1192
+ data = json.load(open(sys.argv[1]))
1193
+ except Exception:
1194
+ sys.exit(0)
1195
+ print("; ".join(data.get("reasons") or []))
1196
+ ' "$plan/tasks/$id/qa.jev.json" 2>/dev/null)"
1197
+ [ -n "$notice" ] || continue
1198
+ [ "$printed" = 0 ] && { echo; printed=1; }
1199
+ printf ' jev: %s — the FAIL stands; %s\n' "$id" "$notice"
1200
+ done
1201
+ [ "$printed" = 1 ] && printf ' A flagged review is not an overturned one. Read it before retrying.\n'
1202
+ return 0
1203
+ }
1204
+
1205
+ # One row per execution of the verify command, with its own copy of the log: the
1206
+ # deterministic rerun and Jev's retry both write verification.retry.log, so without this
1207
+ # only the last of them would survive.
1208
+ verify_run_note() { # verify_run_note <out> <kind> <rc> <before> <after> <log> <t0>
1209
+ local out="$1" n=1
1210
+ [ -f "$out/verification.runs.tsv" ] && n=$(( $(wc -l < "$out/verification.runs.tsv") + 1 ))
1211
+ cp "$out/$6" "$out/verification.run-$n.log" 2>/dev/null || :
1212
+ printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$n" "$2" "${3:-}" "$([ "$4" = "$5" ] && echo 0 || echo 1)" \
1213
+ "$(( $(date +%s) - $7 ))" "verification.run-$n.log" >> "$out/verification.runs.tsv"
1214
+ }
1215
+
1216
+ verify_result() ( # plan, checkout, artifact directory
1217
+ local plan="$1" wt="$2" out="$3" cmd="" before after rc=0 repo jev_loaded=0 vretries
1218
+ mkdir -p "$out"
1219
+ forge_metric_begin "$out" verification
1220
+ forge_metric_phase verification
1221
+ repo="$(cat "$plan/repo")"
1222
+ rm -f "$out/verification.flaky" "$out/verification.retry.log" "$out/verification.coverage.txt" \
1223
+ "$out/verification.runs.tsv" "$out"/verification.run-*.log "$out/verification.commit"
1224
+ # The commit being verified, named before any early return, so every outcome (FAIL and
1225
+ # UNVERIFIED included) can be joined to the code it judged.
1226
+ git -C "$wt" rev-parse HEAD > "$out/verification.commit" 2>/dev/null || rm -f "$out/verification.commit"
1227
+ # A failed command is rerun this many times before it counts, so one flaky lane does not
1228
+ # cost a whole run. The source fingerprint still has to be unchanged, and a rerun that
1229
+ # passes is recorded loudly (verification.flaky) rather than passed off as clean.
1230
+ vretries="$(cat "$plan/verify_retries" 2>/dev/null)"
1231
+ case "$vretries" in ''|*[!0-9]*) vretries=1 ;; esac
1232
+ # Sourced unconditionally (cheap: local function/var definitions, no network) so
1233
+ # forge_jev_active is available below. verify_result is itself a subshell, so this
1234
+ # never leaks state into the caller. A missing or broken options file just leaves
1235
+ # jev_loaded=0 and every Jev branch below falls through to today's behaviour.
1236
+ if [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ]; then
1237
+ source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && jev_loaded=1
1238
+ fi
1239
+ if [ -s "$plan/verify_cmd" ]; then cmd="$(cat "$plan/verify_cmd")"
1240
+ elif [ -s "$repo/.forge/verify" ]; then cmd="$(cat "$repo/.forge/verify")"; fi
1241
+ if [ "$(forge_tree "$wt")" != "$(git -C "$wt" rev-parse HEAD^{tree})" ]; then
1242
+ echo FAIL > "$out/verification.status"
1243
+ note "verification checkout has source changes outside its commit"; return 1
1244
+ fi
1245
+ # Only when nothing is configured: ask Jev to name a command that actually exists
1246
+ # in the repo. Its exit code, not its stdout text, decides whether a judgment was
1247
+ # produced — any non-zero exit (no key, disabled, nothing suitable, a traceback)
1248
+ # falls straight through to the exact UNVERIFIED behaviour below. A Jev failure
1249
+ # must never change the outcome of a run.
1250
+ if [ -z "$cmd" ] && [ "$jev_loaded" = 1 ] && forge_jev_active tests; then
1251
+ local jev_json jev_rc jev_cmd
1252
+ # --json rather than the bare form: the record exists so a human can audit why a
1253
+ # command ran, and "confidence: null" answers nothing. The exit code still decides.
1254
+ jev_json="$(python3 "$SKILL_DIR/scripts/forge-jev.py" verify-discover --repo "$repo" --json --run-dir "$out" 2>/dev/null)"
1255
+ jev_rc=$?
1256
+ jev_cmd=""
1257
+ if [ "$jev_rc" -eq 0 ] && [ -n "$jev_json" ]; then
1258
+ jev_cmd="$(printf '%s' "$jev_json" | python3 -c '
1259
+ import json, sys
1260
+ try:
1261
+ print((json.load(sys.stdin) or {}).get("command") or "")
1262
+ except ValueError:
1263
+ print("")
1264
+ ' 2>/dev/null)"
1265
+ fi
1266
+ if [ -n "$jev_cmd" ]; then
1267
+ cmd="$jev_cmd"
1268
+ # Loud on purpose: an unreviewed command is about to run on the user's behalf,
1269
+ # and they must never discover that later from a log instead of right here.
1270
+ note "jev chose a verification command (no --verify or .forge/verify configured): $cmd"
1271
+ printf '%s' "$jev_json" | python3 -c '
1272
+ import json, sys
1273
+ out = sys.argv[1]
1274
+ try:
1275
+ found = json.load(sys.stdin) or {}
1276
+ except ValueError:
1277
+ found = {}
1278
+ with open(out + "/verification.jev.json", "w") as fh:
1279
+ json.dump({"discovered": found}, fh, indent=2, sort_keys=True)
1280
+ fh.write("\n")
1281
+ ' "$out" 2>/dev/null || true
1282
+ fi
1283
+ fi
1284
+ verify_coverage_report "$wt" "$cmd" "$out/verification.coverage.txt"
1285
+ if [ -z "$cmd" ]; then
1286
+ echo UNVERIFIED > "$out/verification.status"
1287
+ note "combined result UNVERIFIED: no --verify command or .forge/verify configured"
1288
+ return 0
1289
+ fi
1290
+ printf '%s\n' "$cmd" > "$out/verification.command"
1291
+ before="$(forge_fingerprint "$wt")" || return 1
1292
+ local vt0; vt0="$(date +%s)"
1293
+ (cd "$wt" && /bin/bash -c "$cmd") > "$out/verification.log" 2>&1 || rc=$?
1294
+ echo "$rc" > "$out/verification.exit"
1295
+ after="$(forge_fingerprint "$wt")" || return 1
1296
+ verify_run_note "$out" initial "$rc" "$before" "$after" verification.log "$vt0"
1297
+ echo "$before" > "$out/verification.fingerprint"
1298
+ if [ "$rc" != 0 ] || [ "$before" != "$after" ]; then
1299
+ local flaked=0 det_retried=0 n_retry=0 rc3 b3 a3
1300
+ # Deterministic rerun first: the rerun itself is the evidence, so unlike Jev's triage
1301
+ # below it needs no model and no recorded trap. A tree change is never retried.
1302
+ if [ "$rc" != 0 ] && [ "$before" = "$after" ] && [ "$vretries" -gt 0 ]; then
1303
+ det_retried=1
1304
+ while [ "$n_retry" -lt "$vretries" ] && [ "$flaked" = 0 ]; do
1305
+ n_retry=$((n_retry+1)); rc3=0
1306
+ note "verification failed (exit $rc); rerunning to tell a flaky lane from a regression ($n_retry/$vretries)"
1307
+ b3="$(forge_fingerprint "$wt")" || return 1
1308
+ vt0="$(date +%s)"
1309
+ (cd "$wt" && /bin/bash -c "$cmd") > "$out/verification.retry.log" 2>&1 || rc3=$?
1310
+ a3="$(forge_fingerprint "$wt")" || return 1
1311
+ verify_run_note "$out" rerun "$rc3" "$b3" "$a3" verification.retry.log "$vt0"
1312
+ if [ "$rc3" = 0 ] && [ "$b3" = "$a3" ]; then
1313
+ flaked=1
1314
+ {
1315
+ echo "first_exit=$rc"; echo "retry_exit=0"; echo "retries_used=$n_retry"
1316
+ echo "first_log=$out/verification.log"; echo "retry_log=$out/verification.retry.log"
1317
+ } > "$out/verification.flaky"
1318
+ echo 0 > "$out/verification.exit"
1319
+ note "FLAKY: verification failed once (exit $rc) and passed on rerun — the first failure is in $out/verification.log; this is not a clean pass"
1320
+ fi
1321
+ done
1322
+ fi
1323
+ # A tree change is never a flake — it stays FAIL immediately, no triage. Beyond
1324
+ # that, act on a "known_flake" verdict ONLY when Forge's own memory already
1325
+ # records a `trap` for this repo: a model alone deciding a failure was a flake
1326
+ # is exactly how a real regression ships, so the verdict needs corroboration
1327
+ # from Forge's own records, not just the model's word.
1328
+ if [ "$rc" != 0 ] && [ "$before" = "$after" ] && [ "$jev_loaded" = 1 ] \
1329
+ && [ "$det_retried" = 0 ] \
1330
+ && forge_jev_active tests \
1331
+ && [ -s "$repo/.forge/memory.md" ] \
1332
+ && awk '/^## Known traps$/{f=1;next} /^## /{f=0} f && NF{c++} END{exit !(c>0)}' "$repo/.forge/memory.md"
1333
+ then
1334
+ local triage_json triage_rc verdict="" confidence="" flake_act="0.90" parsed corroborated=0 retried=0 rc2="" before2 after2
1335
+ triage_json="$(python3 "$SKILL_DIR/scripts/forge-jev.py" verify-triage --repo "$repo" \
1336
+ --command "$cmd" --log "$out/verification.log" --json --run-dir "$out" 2>/dev/null)"
1337
+ triage_rc=$?
1338
+ if [ "$triage_rc" -eq 0 ] && [ -n "$triage_json" ]; then
1339
+ parsed="$(printf '%s' "$triage_json" | python3 -c '
1340
+ import json, sys
1341
+ try:
1342
+ data = json.load(sys.stdin)
1343
+ except ValueError:
1344
+ data = {}
1345
+ print(data.get("verdict") or "")
1346
+ c = data.get("confidence")
1347
+ print(c if c is not None else "")
1348
+ print(bool(data.get("corroborated")))
1349
+ ' 2>/dev/null)"
1350
+ verdict="$(printf '%s\n' "$parsed" | sed -n 1p)"
1351
+ confidence="$(printf '%s\n' "$parsed" | sed -n 2p)"
1352
+ # triage() reports whether a recorded trap shares a distinctive token with this
1353
+ # failure's output. Without that, one flaky test noted months ago would excuse
1354
+ # every future failure in the repo.
1355
+ [ "$(printf '%s\n' "$parsed" | sed -n 3p)" = "True" ] && corroborated=1 || corroborated=0
1356
+ if [ -n "$FORGE_JEV_STATUS_JSON" ]; then
1357
+ local threshold_read
1358
+ threshold_read="$(printf '%s' "$FORGE_JEV_STATUS_JSON" | python3 -c '
1359
+ import json, sys
1360
+ try:
1361
+ data = json.load(sys.stdin)
1362
+ except ValueError:
1363
+ data = {}
1364
+ v = (data.get("thresholds") or {}).get("flake_act")
1365
+ print(v if v is not None else "0.90")
1366
+ ' 2>/dev/null)"
1367
+ [ -n "$threshold_read" ] && flake_act="$threshold_read"
1368
+ fi
1369
+ if [ "$verdict" = known_flake ] && [ -n "$confidence" ] && [ "$corroborated" = 1 ] \
1370
+ && awk -v a="$confidence" -v b="$flake_act" 'BEGIN{exit !(a+0 >= b+0)}'
1371
+ then
1372
+ retried=1
1373
+ note "jev: known_flake (confidence $confidence >= $flake_act), corroborated by a recorded trap — retrying once"
1374
+ before2="$(forge_fingerprint "$wt")" || return 1
1375
+ vt0="$(date +%s)"
1376
+ (cd "$wt" && /bin/bash -c "$cmd") > "$out/verification.retry.log" 2>&1 || rc2=$?
1377
+ rc2="${rc2:-0}"
1378
+ after2="$(forge_fingerprint "$wt")" || return 1
1379
+ verify_run_note "$out" jev_retry "$rc2" "$before2" "$after2" verification.retry.log "$vt0"
1380
+ if [ "$rc2" = 0 ] && [ "$before2" = "$after2" ]; then
1381
+ flaked=1
1382
+ note "jev: retry passed — treating as a corroborated flake, not a regression"
1383
+ else
1384
+ note "jev: retry also failed (exit $rc2) — keeping FAIL"
1385
+ fi
1386
+ fi
1387
+ python3 -c '
1388
+ import json, sys
1389
+ out, verdict, confidence, threshold, corroborated, retried, first_rc, retry_rc = sys.argv[1:9]
1390
+ path = out + "/verification.jev.json"
1391
+ try:
1392
+ with open(path) as fh:
1393
+ data = json.load(fh)
1394
+ except (OSError, ValueError):
1395
+ data = {}
1396
+ data["triage"] = {
1397
+ "verdict": verdict or None,
1398
+ "confidence": float(confidence) if confidence else None,
1399
+ "flake_act_threshold": float(threshold),
1400
+ "trap_corroboration": corroborated == "1",
1401
+ "retried": retried == "1",
1402
+ "first_exit": int(first_rc),
1403
+ "retry_exit": int(retry_rc) if retry_rc != "" else None,
1404
+ }
1405
+ with open(path, "w") as fh:
1406
+ json.dump(data, fh, indent=2, sort_keys=True)
1407
+ fh.write("\n")
1408
+ ' "$out" "$verdict" "$confidence" "$flake_act" "$corroborated" "$retried" "$rc" "$rc2" 2>/dev/null || true
1409
+ fi
1410
+ fi
1411
+ if [ "$flaked" = 1 ]; then
1412
+ echo PASS > "$out/verification.status"
1413
+ return 0
1414
+ fi
1415
+ echo FAIL > "$out/verification.status"
1416
+ note "combined verification failed or changed source; see $out/verification.log"
1417
+ return 1
1418
+ fi
1419
+ echo PASS > "$out/verification.status"
1420
+ )
1421
+
1422
+ write_results() { # write_results <plan>
1423
+ local PLAN="$1" T="$1/tasks.tsv" run_id id
1424
+ run_id="$(cat "$PLAN/run_id")"
1425
+ : > "$PLAN/results.tsv"
1426
+ for id in $(all_ids "$T"); do
1427
+ printf '%s\t%s\tforge/%s/%s\n' "$id" "$(task_status "$PLAN" "$id")" "$run_id" "$id" >> "$PLAN/results.tsv"
1428
+ done
1429
+ }
1430
+
1431
+ # --- run ----------------------------------------------------------------------
1432
+ do_run() (
1433
+ local PLAN="${1:?run needs a plan dir}"; shift
1434
+ local fractal_pipeline_args=("$@")
1435
+ local MAXP=3 DRY=0 jev_rc=0
1436
+ while [ $# -gt 0 ]; do
1437
+ case "$1" in
1438
+ --fractal) forge_fractal_flag on || exit $?; shift ;;
1439
+ --fractal-auto-decompose) forge_fractal_flag on || exit $?; FRACTAL_ARGS+=("$1"); shift ;;
1440
+ --no-fractal) forge_fractal_flag off || exit $?; shift ;;
1441
+ --fractal-*) FRACTAL_ARGS+=("$1" "${2:?}"); shift 2 ;;
1442
+ --no-ripwire) export FORGE_RIPWIRE=off; shift ;;
1443
+ --output) OUTPUT="${2:?}"; shift 2 ;;
1444
+ --max-parallel) MAXP="${2:?}"; shift 2 ;;
1445
+ --yolo-dwarf) touch "$PLAN/yolo_dwarf"; shift ;;
1446
+ --yolo-qa) touch "$PLAN/yolo_qa"; shift ;;
1447
+ --verify) printf '%s' "${2:?--verify needs a command}" > "$PLAN/verify_cmd"; shift 2 ;;
1448
+ --setup) printf '%s' "${2:?--setup needs a command}" > "$PLAN/setup_cmd"; shift 2 ;;
1449
+ --dry-run) DRY=1; shift ;;
1450
+ *)
1451
+ # `case` is not a loop, so `continue` here belongs to the enclosing while.
1452
+ if forge_pp_flag run "$PLAN" "$@"; then shift "$PP_SHIFT"; continue; fi
1453
+ if [ "$JEV_OPTS" = 1 ]; then
1454
+ forge_jev_flag "$1"; jev_rc=$?
1455
+ case "$jev_rc" in
1456
+ 0) shift "$JEV_SHIFT"; continue ;;
1457
+ 2) exit 2 ;;
1458
+ esac
1459
+ fi
1460
+ die "run: unknown option '$1'"
1461
+ ;;
1462
+ esac
1463
+ done
1464
+ [ "$JEV_OPTS" = 1 ] && forge_jev_settle "$PLAN"
1465
+ case "$OUTPUT" in summary|full) ;; *) die "--output must be summary or full" ;; esac
1466
+ export FORGE_OUTPUT="$OUTPUT"
1467
+ case "$MAXP" in ''|*[!0-9]*) die "--max-parallel must be positive" ;; esac
1468
+ [ "$MAXP" -gt 0 ] || die "--max-parallel must be positive"
1469
+ local T="$PLAN/tasks.tsv" W="$PLAN/waves.tsv"
1470
+ [ -s "$W" ] || die "no waves.tsv — run the plan subcommand first"
1471
+ grep -q 'UNASSIGNED' "$T" && die "some tasks are UNASSIGNED — assign a dwarf before running"
1472
+
1473
+ compute_waves "$PLAN"
1474
+ local REPO run_id wt_root int_wt int_br
1475
+ REPO="$(cat "$PLAN/repo")"; run_id="$(cat "$PLAN/run_id")"
1476
+ if [ "$DRY" != 1 ]; then
1477
+ # The concurrency limit this run was given, for forge-ui to draw; a later run rewrites it.
1478
+ printf '%s\n' "$MAXP" > "$PLAN/max_parallel"
1479
+ # Plan registers too; this also picks up plans made before it did.
1480
+ forge_ui_register "$PLAN"
1481
+ fi
1482
+ forge_fractal_parallel_run "$PLAN" "$REPO" "$DRY" ${fractal_pipeline_args[@]+"${fractal_pipeline_args[@]}"} || return $?
1483
+ # Durable, beside the repo — never TMPDIR. A temp worktree root has destroyed
1484
+ # real work here before: isolated worktrees are never pushed, so when the root
1485
+ # goes, the only copy of that work goes with it.
1486
+ wt_root="$(cd "$REPO/.." && pwd)/.forge-worktrees/$run_id"
1487
+ echo "$wt_root" > "$PLAN/wt_root"
1488
+ # Sibling, not parent: git refs are files, so a branch `forge/<run>` makes
1489
+ # `forge/<run>/<task>` impossible ("cannot lock ref ... exists; cannot create").
1490
+ int_br="forge/$run_id-integration"; int_wt="$wt_root/_integration"
1491
+
1492
+ if [ "$DRY" = 1 ]; then
1493
+ echo "worktree root: $wt_root"
1494
+ echo "integration: $int_br"
1495
+ local id
1496
+ for id in $(awk -F'\t' '{print $2}' "$W"); do
1497
+ echo " $id -> $wt_root/$id (branch forge/$run_id/$id)"
1498
+ echo " dwarf: $DISPATCH dwarf $(field "$T" "$id" dwarf) --repo $wt_root/$id"
1499
+ echo " qa : $DISPATCH qa $(field "$T" "$id" qa) --repo $wt_root/$id"
1500
+ done
1501
+ echo "(dry run — nothing created or dispatched)"
1502
+ return 0
1503
+ fi
1504
+
1505
+ forge_metric_begin "$PLAN" parallel
1506
+ forge_metric_phase preflight
1507
+ [ "${FORGE_RIPWIRE:-}" != off ] || touch "$PLAN/no_ripwire"
1508
+ [ ! -f "$PLAN/no_ripwire" ] || export FORGE_RIPWIRE=off
1509
+ preflight_plan "$PLAN" || return $?
1510
+ /bin/bash "$SKILL_DIR/scripts/forge-install-ripwire.sh" || true
1511
+ forge_metric_phase preparation
1512
+ mkdir -p "$wt_root"
1513
+ # A branch named exactly forge/<run-id> would occupy refs/heads/forge/<run-id> as a
1514
+ # file and make every task branch below it impossible to create. Catch it here with
1515
+ # an actionable message rather than letting all N tasks fail identically at dispatch.
1516
+ if (cd "$REPO" && git rev-parse --verify --quiet "forge/$run_id" >/dev/null 2>&1); then
1517
+ die "branch 'forge/$run_id' blocks task branches 'forge/$run_id/<task>' (git refs are files, so a ref cannot also be a directory). Delete or rename it: git branch -D forge/$run_id" 3
1518
+ fi
1519
+ if ! (cd "$REPO" && git rev-parse --verify "$int_br" >/dev/null 2>&1); then
1520
+ (cd "$REPO" && git branch "$int_br" HEAD) || die "could not create $int_br" 3
1521
+ # Where the integration branch started, so a whole-run review can diff exactly what
1522
+ # this run added. Plans created before this file existed fall back to a merge-base.
1523
+ git -C "$REPO" rev-parse HEAD > "$PLAN/integration.base"
1524
+ fi
1525
+ if [ ! -d "$int_wt" ]; then
1526
+ (cd "$REPO" && git worktree add "$int_wt" "$int_br" >/dev/null 2>&1) \
1527
+ || die "could not create integration worktree" 3
1528
+ # The integration worktree is where a merged result gets validated, so it needs
1529
+ # the same setup the task worktrees get. Without it, the one checkout holding
1530
+ # every task's merged work is the one checkout that cannot build or test it.
1531
+ mkdir -p "$PLAN/tasks/_integration"
1532
+ run_worktree_setup "$PLAN" "$REPO" "$int_wt" "$PLAN/tasks/_integration" _integration
1533
+ fi
1534
+
1535
+ if [ -s "$PLAN/accepted.integration" ] && [ "$(git -C "$int_wt" rev-parse HEAD)" != "$(cat "$PLAN/accepted.integration")" ]; then
1536
+ die "integration branch changed outside this run" 3
1537
+ fi
1538
+ git -C "$int_wt" rev-parse HEAD > "$PLAN/accepted.integration"
1539
+ local id failed=0
1540
+ # Say now, before any model is paid for, which suites the verify command will not run.
1541
+ verify_coverage_report "$REPO" "$(verify_command_for "$PLAN" "$REPO")" "$PLAN/verification.coverage.txt"
1542
+ local sched_rc=0 paused=0 sched_args=()
1543
+ [ "$PP_RETRY_FAILED" -gt 0 ] && sched_args+=(--retry-failed "$PP_RETRY_FAILED")
1544
+ [ "$PP_INFRA_RETRIES" -gt 0 ] && sched_args+=(--infra-retries "$PP_INFRA_RETRIES")
1545
+ forge_metric_phase dispatch
1546
+ python3 "$SKILL_DIR/scripts/forge-schedule.py" "$SELF" "$PLAN" "$MAXP" ${sched_args[@]+"${sched_args[@]}"} || sched_rc=$?
1547
+ [ "$sched_rc" != 0 ] && failed=1
1548
+ [ "$sched_rc" = 8 ] && paused=1
1549
+
1550
+ forge_metric_phase verification
1551
+ # An infrastructure pause leaves the run unfinished, so there is nothing yet worth
1552
+ # verifying; the next `run` resumes and verifies at its end.
1553
+ if [ "$paused" = 0 ]; then
1554
+ verify_result "$PLAN" "$int_wt" "$PLAN" || failed=1
1555
+ fi
1556
+ write_results "$PLAN"
1557
+ # Only prune worktrees for work that is safely merged, and only after the record
1558
+ # of it has been written and read back. A failed task keeps its worktree and
1559
+ # branch so there is something to inspect and retry from.
1560
+ if [ -s "$PLAN/results.tsv" ]; then
1561
+ for id in $(awk -F'\t' '$2=="MERGED" {print $1}' "$PLAN/results.tsv"); do
1562
+ (cd "$REPO" && git worktree remove --force "$wt_root/$id" >/dev/null 2>&1)
1563
+ done
1564
+ fi
1565
+
1566
+ echo
1567
+ echo "results (integration branch: $int_br)"
1568
+ echo "artifacts: $PLAN/tasks/<id>/{dwarf,qa}.{last,log}; results: $PLAN/results.tsv"
1569
+ printf ' %-14s %-9s %s\n' id status branch
1570
+ awk -F'\t' '{printf " %-14s %-9s %s\n", $1, $2, $3}' "$PLAN/results.tsv"
1571
+ forge_jev_review_flags "$PLAN"
1572
+ echo
1573
+ run_report_extras "$PLAN"
1574
+
1575
+ # Undeclared files are reported here rather than buried in a task log, because
1576
+ # this is the list that explains any CONFLICT above.
1577
+ local drift=0
1578
+ for id in $(all_ids "$T"); do
1579
+ if [ -s "$PLAN/tasks/$id/drift.txt" ]; then
1580
+ [ "$drift" = 0 ] && echo "scope drift (touched files the task did not declare):"
1581
+ drift=1
1582
+ printf ' %-14s %s\n' "$id" "$(tr '\n' ' ' < "$PLAN/tasks/$id/drift.txt")"
1583
+ fi
1584
+ done
1585
+ [ "$drift" = 1 ] && echo
1586
+
1587
+ # The retry hint is for tasks that actually failed; a run that only failed verification,
1588
+ # or only paused on infrastructure, has nothing to retry task by task.
1589
+ if [ "$(awk -F'\t' '$2!="MERGED" && $2!="INFRA" && $2!="PENDING" && $2!="BLOCKED" {n++} END {print n+0}' "$PLAN/results.tsv")" -gt 0 ]; then
1590
+ echo "retry a failed task with its reviewer's findings:"
1591
+ echo " forge-parallel.sh retry $PLAN <task-id> [--dwarf <spec>]"
1592
+ echo "or let a run retry them itself: forge-parallel.sh run $PLAN --retry-failed 1"
1593
+ echo
1594
+ fi
1595
+ if [ "$failed" = 1 ] && [ "$paused" = 0 ] && [ "$(cat "$PLAN/verification.status" 2>/dev/null)" = FAIL ]; then
1596
+ echo "combined verification failed: see $PLAN/verification.log"
1597
+ echo
1598
+ fi
1599
+
1600
+ if [ ! -f "$PLAN/no_memory" ]; then
1601
+ /bin/bash "$MEMORY" spend "$REPO" --run "$run_id" 2>/dev/null | sed 's/^/ /'
1602
+ echo
1603
+ fi
1604
+ if [ ! -f "$PLAN/no_memory" ] && [ -s "$REPO/.forge/memory.md" ]; then
1605
+ echo "project memory: $(grep -c '^- ' "$REPO/.forge/memory.md") fact(s) in .forge/ —"
1606
+ echo " uncommitted, and the only thing forge wrote outside its own branches."
1607
+ echo
1608
+ echo "Your branch and working tree were not touched, apart from .forge/ above."
1609
+ else
1610
+ echo "Your branch and working tree were not touched."
1611
+ fi
1612
+ [ "$paused" = 1 ] && return 8
1613
+ [ "$failed" = 1 ] && return 5
1614
+ return 0
1615
+ )
1616
+
1617
+ # --- integrate ----------------------------------------------------------------
1618
+ do_integrate() {
1619
+ local PLAN="${1:?}"; shift
1620
+ local APPROVED=0
1621
+ while [ $# -gt 0 ]; do
1622
+ case "$1" in
1623
+ --approved) APPROVED=1; shift ;;
1624
+ *) if forge_pp_flag integrate "$PLAN" "$@"; then shift "$PP_SHIFT"; else die "integrate: unknown option '$1'"; fi ;;
1625
+ esac
1626
+ done
1627
+ [ "$APPROVED" = 1 ] || die "integrate needs --approved: it merges forge's work into YOUR current branch"
1628
+ report_unmerged "$PLAN"
1629
+ local REPO run_id int_br cur
1630
+ REPO="$(cat "$PLAN/repo")"; run_id="$(cat "$PLAN/run_id")"; int_br="forge/$run_id-integration"
1631
+ cur="$(cd "$REPO" && git rev-parse --abbrev-ref HEAD)"
1632
+ [ "$cur" = HEAD ] && die "integration requires a checked-out user branch" 3
1633
+ [ "$cur" = "$int_br" ] && die "you are already on $int_br"
1634
+ [ -n "$(cd "$REPO" && git status --porcelain)" ] && die "working tree is dirty — commit or stash before integrating"
1635
+ # Verify the actual combined candidate, including changes on the user's branch.
1636
+ local parent target candidate candidate_sha out
1637
+ parent="$(git -C "$REPO" rev-parse HEAD)" || return 3
1638
+ target="$(git -C "$REPO" rev-parse "$int_br")" || return 3
1639
+ [ -s "$PLAN/accepted.integration" ] && [ "$target" = "$(cat "$PLAN/accepted.integration")" ] || die "integration revision does not match the accepted run" 3
1640
+ out="$PLAN/integrate-$(date +%s)-$$"; mkdir -p "$out"
1641
+ candidate="$(cat "$PLAN/wt_root")/_candidate-$$"
1642
+ git -C "$REPO" worktree add --detach "$candidate" "$parent" > "$out/setup.log" 2>&1 || return 3
1643
+ if ! git -C "$candidate" merge --no-ff -m "forge: integrate run $run_id" "$target" > "$out/merge.log" 2>&1; then
1644
+ forge_dataset_event integrate_conflict --dir "$PLAN" --set branch="$cur" --set parent="$parent" \
1645
+ --set target="$target" --set conflicts="$(git -C "$candidate" diff --name-only --diff-filter=U 2>/dev/null | tr '\n' ' ')" \
1646
+ --file merge_log="$out/merge.log"
1647
+ note "candidate merge conflicted; inspect $candidate and $out/merge.log"; return 6
1648
+ fi
1649
+ run_worktree_setup "$PLAN" "$REPO" "$candidate" "$out" integration-candidate
1650
+ verify_result "$PLAN" "$candidate" "$out" || { report_verify_notes "$out"; return 5; }
1651
+ candidate_sha="$(git -C "$candidate" rev-parse HEAD)" || return 3
1652
+ report_verify_notes "$out"
1653
+ report_known_issues "$PLAN"
1654
+ # Opt-in whole-run review of the combined candidate, after verification and before the
1655
+ # user's branch moves. Per-task review only ever saw one slice at a time.
1656
+ if [ -n "$PP_FINAL_REVIEW" ]; then
1657
+ declare -F final_review_for_integrate >/dev/null || die "the final review is not available in this build" 3
1658
+ final_review_for_integrate "$PLAN" "$parent" "$candidate_sha" "$candidate" "$out" "$PP_FINAL_REVIEW" || return $?
1659
+ fi
1660
+ [ "$(git -C "$REPO" rev-parse --abbrev-ref HEAD)" = "$cur" ] &&
1661
+ [ "$(git -C "$REPO" rev-parse HEAD)" = "$parent" ] &&
1662
+ [ "$(git -C "$REPO" rev-parse "$int_br")" = "$target" ] &&
1663
+ [ -z "$(git -C "$REPO" status --porcelain)" ] || die "branch or working tree changed during verification" 3
1664
+ git -C "$REPO" merge --ff-only "$candidate_sha" || return 6
1665
+ # The user's branch has moved: the run shipped. Recorded now, because the cleanup below
1666
+ # can still fail and make the command's exit code say otherwise.
1667
+ forge_dataset_event integrate_advanced --dir "$PLAN" --set branch="$cur" --set parent="$parent" \
1668
+ --set candidate="$candidate_sha" --set target="$target" --set verification="$(cat "$out/verification.status" 2>/dev/null)"
1669
+ git -C "$REPO" worktree remove "$candidate" || return 3
1670
+ echo "merged candidate of $int_br into $cur (verification: $(cat "$out/verification.status"))"
1671
+ }
1672
+
1673
+ # --- main ---------------------------------------------------------------------
1674
+ if [ "${1:-}" = --help ] || [ "${1:-}" = -h ]; then
1675
+ awk '/^# Usage:/ {on=1} /^set -uo pipefail/ {on=0} on {sub(/^# ?/, ""); print}' "$SELF"
1676
+ forge_fractal_help
1677
+ exit 0
1678
+ fi
1679
+ [ $# -ge 1 ] || die "usage: forge-parallel.sh <plan|run|retry|accept|review|integrate|split|combine> <plan-dir> [...]"
1680
+ CMD="$1"; shift
1681
+ [ ! -f "${1:-}/no_ripwire" ] || export FORGE_RIPWIRE=off
1682
+ if [ -d "${1:-}" ]; then
1683
+ PLAN_PATH="$(cd "$1" && pwd)"; shift; set -- "$PLAN_PATH" "$@"
1684
+ fi
1685
+ case "$CMD" in
1686
+ # `combine` takes a plan dir that does not exist yet and locks its sources itself.
1687
+ run|retry|integrate|accept|review|split)
1688
+ if [ "${FORGE_PLAN_LOCK:-}" != "${1:-}" ]; then
1689
+ exec python3 "$SKILL_DIR/scripts/forge-schedule.py" lock "$SELF" "$CMD" "$@"
1690
+ fi ;;
1691
+ esac
1692
+ # Every user-facing subcommand leaves one record in the run dataset, whatever its exit
1693
+ # path (die included). Installed after the lock re-exec, so it fires once, in the process
1694
+ # that did the work; `_task` and `_merge` are parts of a `run` and are not recorded alone.
1695
+ case "$CMD" in
1696
+ plan|run|retry|integrate|accept|review|split|combine)
1697
+ FORGE_DATASET_INVOCATION="inv-$(date +%s)-$$-$RANDOM"; export FORGE_DATASET_INVOCATION
1698
+ FORGE_DATASET_STARTED="$(date +%s)"
1699
+ FORGE_DATASET_ARGV=("$@")
1700
+ trap 'forge_dataset_rc=$?; trap - EXIT; forge_dataset_invocation "$CMD" "$forge_dataset_rc" "$FORGE_DATASET_STARTED" ${FORGE_DATASET_ARGV[@]+"${FORGE_DATASET_ARGV[@]}"}; exit "$forge_dataset_rc"' EXIT ;;
1701
+ esac
1702
+ # Subcommands implemented in the optional libraries fail clearly if the library is absent.
1703
+ need_fn() { declare -F "$1" >/dev/null || die "'$CMD' is not available: $1 is missing" 3; }
1704
+ case "$CMD" in
1705
+ plan) do_plan "$@"; rc=$?; forge_update_notice; exit "$rc" ;;
1706
+ run) do_run "$@"; rc=$?; forge_update_notice; exit "$rc" ;;
1707
+ retry) do_retry "$@" ;;
1708
+ integrate) do_integrate "$@" ;;
1709
+ accept) need_fn do_accept; do_accept "$@" ;;
1710
+ review) need_fn do_review; do_review "$@" ;;
1711
+ split) need_fn do_split; do_split "$@" ;;
1712
+ combine) need_fn do_combine; do_combine "$@" ;;
1713
+ _task) do_task "$@" ;;
1714
+ _merge) merge_task "$@" ;;
1715
+ _prepare_retry) retry_for_scheduler "$@" ;;
1716
+ *) die "unknown subcommand '$CMD'" ;;
1717
+ esac