@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,347 @@
1
+ #!/usr/bin/env bash
2
+ # forge-solo.sh — the single-task forge run: one dwarf implements in the working
3
+ # tree, one QA reviews the diff it actually produced.
4
+ #
5
+ # This exists for the same reason forge-dispatch.sh does. The pipeline is a dozen
6
+ # steps — memory injection, prompt assembly, dispatch, diff capture, review prompt,
7
+ # a second dispatch, two ledger writes — and it used to live in SKILL.md as shell
8
+ # for the orchestrating model to retype on every invocation, from whichever of five
9
+ # harnesses was driving. Several of those steps are silently wrong rather than
10
+ # loudly broken when mistyped: drop ':(exclude).forge' from the capture and forge's
11
+ # own memory goes in front of the reviewer as if the dwarf had written it.
12
+ # Decomposed runs never had this problem because forge-parallel.sh owns the
13
+ # plumbing. Solo runs should not either.
14
+ #
15
+ # What stays with the orchestrator is the judgment: it writes prompt.md, it reads
16
+ # dwarf.last and qa.last, and it reports. Only the plumbing moved.
17
+ #
18
+ # Usage:
19
+ # forge-solo.sh <run-dir> --repo <dir> --dwarf <spec> [--qa <spec>]
20
+ # [--approach <file>] [--yolo-dwarf] [--yolo-qa]
21
+ # [--native-review] [--no-memory] [--timeout <s>] [--dry-run] [--ui]
22
+ # [--fail-first]
23
+ #
24
+ # Reads <run-dir>/prompt.md the implementation instruction (required)
25
+ # <run-dir>/goal.txt one line, what the user asked for (optional)
26
+ # Writes <run-dir>/dwarf.{input,last,log,resolved}
27
+ # <run-dir>/changes.diff
28
+ # <run-dir>/qa.{input,last,log,resolved}
29
+ # <run-dir>/verdict PASS | FAIL | UNKNOWN, when qa emitted one
30
+ # <run-dir>/failfirst.json with --fail-first: do the change's tests fail without it?
31
+ # <run-dir>/status RUNNING, then this invocation's outcome
32
+ # <run-dir>/dwarf.spec, qa.spec and attempts.tsv
33
+ #
34
+ # Exit codes: 0 ok | 2 usage | 3 precondition | 4 a dispatch failed
35
+ # 5 the dwarf produced no changes | 7 a dispatch timed out
36
+ # 8 a dispatch hit an infrastructure failure (quota, auth, rate limit, network,
37
+ # empty output): nothing about the work was judged, fix the cause and re-run
38
+ set -uo pipefail
39
+
40
+ SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
41
+ DISPATCH="$SKILL_DIR/scripts/forge-dispatch.sh"
42
+ MEMORY="$SKILL_DIR/scripts/forge-memory.sh"
43
+ source "$SKILL_DIR/scripts/forge-artifact.sh"
44
+ source "$SKILL_DIR/scripts/forge-metrics.sh"
45
+ source "$SKILL_DIR/scripts/forge-fractal-options.sh"
46
+ source "$SKILL_DIR/scripts/forge-ui-options.sh"
47
+ source "$SKILL_DIR/scripts/forge-lib-task.sh"
48
+ JEV_OPTS=0
49
+ [ -f "$SKILL_DIR/scripts/forge-jev-options.sh" ] \
50
+ && source "$SKILL_DIR/scripts/forge-jev-options.sh" 2>/dev/null && JEV_OPTS=1
51
+ ORIGINAL_ARGS=("$@")
52
+
53
+ die() { printf 'forge: %s\n' "$1" >&2; exit "${2:-2}"; }
54
+ note() { printf 'forge: %s\n' "$*" >&2; }
55
+
56
+ # forge_metric_begin's EXIT trap calls this, before runtime finish and Fractal's
57
+ # _outcome read status. Only this script defines it. It must not change the exit code.
58
+ forge_metric_on_exit() {
59
+ local rc="${1:-}" status="" word="" cls="" n=0 pick=""
60
+ [ -n "${RUN:-}" ] || return 0
61
+ if [ -f "$RUN/verdict" ]; then
62
+ word=$(awk 'NF { print $1; exit }' "$RUN/verdict" 2>/dev/null || true)
63
+ case "$word" in
64
+ PASS|FAIL|UNKNOWN|NOCHANGES|INVALIDATED) status=$word ;;
65
+ esac
66
+ fi
67
+ if [ -z "$status" ]; then
68
+ case "$rc" in
69
+ 7) status=TIMEOUT ;;
70
+ 8) status=INFRA ;;
71
+ 130|143) status=INTERRUPTED ;;
72
+ *) status=ERROR ;;
73
+ esac
74
+ fi
75
+ printf '%s\n' "$status" > "$RUN/status" 2>/dev/null || true
76
+ # The run start cleared both infra files, so any file here is this invocation's.
77
+ # QA only runs once the dwarf has passed, which makes qa.infra the later stage; no
78
+ # timestamps are compared (macOS mtimes can tie at whole-second resolution).
79
+ if [ "$status" = INFRA ]; then
80
+ for pick in "$RUN/qa.infra" "$RUN/dwarf.infra"; do
81
+ [ -f "$pick" ] || continue
82
+ cls=$(awk '/^class=/ { sub(/^class=/, ""); print; exit }' "$pick" 2>/dev/null || true)
83
+ break
84
+ done
85
+ fi
86
+ n=0
87
+ if [ -f "$RUN/attempts.tsv" ]; then
88
+ n=$(grep -c . "$RUN/attempts.tsv" 2>/dev/null) || n=0
89
+ fi
90
+ n=$(( ${n:-0} + 1 ))
91
+ printf '%s\t%s\t%s\t%s\t%s\n' "$(date +%s)" "$n" "$status" "$cls" "${DWARF:-}" >> "$RUN/attempts.tsv" 2>/dev/null || true
92
+ return 0
93
+ }
94
+
95
+ if [ "${1:-}" = --help ] || [ "${1:-}" = -h ]; then
96
+ sed -n '18,33s/^# *//p' "$0"
97
+ forge_fractal_help
98
+ echo 'Explicit Fractal retry: repeat the original command with --retry.'
99
+ exit 0
100
+ fi
101
+ RUN="${1:-}"; [ -n "$RUN" ] || die "usage: forge-solo.sh <run-dir> --repo <dir> --dwarf <spec> [--qa <spec>]"
102
+ case "$RUN" in -*) die "first argument must be the run directory, got '$RUN'" ;; esac
103
+ shift
104
+
105
+ OUTPUT=summary
106
+ REPO="$PWD"; DWARF=""; QA="opus"; APPROACH=""; YD=""; YQ=""; NATIVE=""; TIMEOUT=""; DRY=0; FAIL_FIRST=0
107
+ while [ $# -gt 0 ]; do
108
+ case "$1" in
109
+ --fractal) forge_fractal_flag on || exit $?; shift ;;
110
+ --fractal-auto-decompose) forge_fractal_flag on || exit $?; FRACTAL_ARGS+=("$1"); shift ;;
111
+ --no-fractal) forge_fractal_flag off || exit $?; shift ;;
112
+ --retry) export FORGE_FRACTAL_RETRY=1; shift ;;
113
+ --fractal-*|--dwarf-low|--dwarf-medium|--dwarf-high) FRACTAL_ARGS+=("$1" "${2:?}"); shift 2 ;;
114
+ --output) OUTPUT="${2:?}"; shift 2 ;;
115
+ --repo) REPO="${2:?--repo needs a value}"; shift 2 ;;
116
+ --dwarf) DWARF="${2:?--dwarf needs a spec}"; shift 2 ;;
117
+ --qa) QA="${2:?--qa needs a spec}"; shift 2 ;;
118
+ --approach) APPROACH="${2:?--approach needs a file}"; shift 2 ;;
119
+ --yolo-dwarf) YD="--yolo"; shift ;;
120
+ --yolo-qa) YQ="--yolo"; shift ;;
121
+ --native-review) NATIVE="--native-review"; shift ;;
122
+ --no-ripwire) export FORGE_RIPWIRE=off; shift ;;
123
+ --no-memory) export FORGE_MEMORY=off; shift ;;
124
+ --timeout) TIMEOUT="${2:?--timeout needs seconds}"; shift 2 ;;
125
+ --dry-run) DRY=1; shift ;;
126
+ --fail-first) FAIL_FIRST=1; shift ;;
127
+ --ui|--no-ui) forge_ui_flag "$1"; shift ;;
128
+ *)
129
+ # `case` is not a loop, so `continue` here belongs to the enclosing while.
130
+ if [ "$JEV_OPTS" = 1 ]; then
131
+ forge_jev_flag "$1"; jev_rc=$?
132
+ case "$jev_rc" in
133
+ 0) shift "$JEV_SHIFT"; continue ;;
134
+ 2) exit 2 ;;
135
+ esac
136
+ fi
137
+ die "unknown option '$1'" ;;
138
+ esac
139
+ done
140
+ # A solo run has one run dir and no plan, so the selection is frozen there: an
141
+ # explicit retry of the same run dir must not change its mind about Jev.
142
+ [ "$JEV_OPTS" = 1 ] && forge_jev_export
143
+
144
+ case "$OUTPUT" in summary|full) ;; *) die "--output must be summary or full" ;; esac
145
+
146
+ # Forge never picks the model: every dispatch spends the user's quota, and choosing
147
+ # for them is not the tool's call.
148
+ [ -n "$DWARF" ] || die "--dwarf is required"
149
+ mkdir -p "$RUN" || die "cannot create run dir '$RUN'"
150
+ [ -s "$RUN/prompt.md" ] || die "no prompt.md in '$RUN' — write the implementation instruction there first"
151
+ [ -d "$REPO" ] || die "--repo '$REPO' is not a directory"
152
+ (cd "$REPO" && git rev-parse --git-dir >/dev/null 2>&1) \
153
+ || die "'$REPO' is not a git repository — there is no way to capture a diff to review" 3
154
+
155
+ # The run dir must live outside the working tree: anything forge writes inside the
156
+ # repo turns up in the dwarf's own diff and reaches QA as if the dwarf wrote it.
157
+ case "$(cd "$RUN" && pwd)/" in
158
+ "$(cd "$REPO" && pwd)"/*) die "run dir '$RUN' is inside the repo; put it somewhere else or it lands in the diff" ;;
159
+ esac
160
+
161
+ RUN="$(cd "$RUN" && pwd)"
162
+ # Freeze the selection now that the run dir is real and absolute. A retry of this
163
+ # same run dir restores it rather than re-reading whatever flags are passed then.
164
+ [ "$JEV_OPTS" = 1 ] && { forge_jev_restore "$RUN" || true; forge_jev_record "$RUN"; }
165
+ REPO="$(cd "$REPO" && pwd)"
166
+ if [ "$DRY" != 1 ] && [ "${FORGE_SOLO_LOCK:-}" != "$RUN" ]; then
167
+ exec python3 "$SKILL_DIR/scripts/forge-fractal.py" _lock "$RUN" "$0" "${ORIGINAL_ARGS[@]}"
168
+ fi
169
+ RUN_ID="$(basename "$RUN")"
170
+ export FORGE_CAPABILITY_CACHE="$RUN/capabilities"
171
+ TFLAG=""; [ -n "$TIMEOUT" ] && TFLAG="--timeout $TIMEOUT"
172
+ GOAL="$(head -1 "$RUN/goal.txt" 2>/dev/null || true)"
173
+
174
+ FFLAGS=()
175
+ [ -z "$YD" ] || FFLAGS+=(--yolo-dwarf)
176
+ [ -z "$YQ" ] || FFLAGS+=(--yolo-qa)
177
+ forge_fractal_select "$RUN" "$REPO" solo "$DRY" --dwarf "$DWARF" --qa "$QA" ${FFLAGS[@]+"${FFLAGS[@]}"} || exit $?
178
+ if [ -n "${FORGE_FRACTAL_RUN:-}" ]; then
179
+ # The UI choice is per invocation: a tmux resume replays these args and must not open it again.
180
+ REMEMBER_ARGS=()
181
+ for arg in "${ORIGINAL_ARGS[@]}"; do
182
+ case "$arg" in --ui|--no-ui) ;; *) REMEMBER_ARGS+=("$arg") ;; esac
183
+ done
184
+ python3 "$SKILL_DIR/scripts/forge-fractal.py" _remember "$0" ${REMEMBER_ARGS[@]+"${REMEMBER_ARGS[@]}"} || exit $?
185
+ fi
186
+ if [ -n "${FORGE_FRACTAL_RUN:-}" ] && [ "$DRY" != 1 ] && [ "${FORGE_FRACTAL_RETRY:-}" != 1 ]; then
187
+ python3 "$SKILL_DIR/scripts/forge-fractal.py" _checkpoint "$REPO"
188
+ checkpoint_rc=$?
189
+ [ "$checkpoint_rc" != 0 ] || exit 0
190
+ [ "$checkpoint_rc" = 1 ] || exit "$checkpoint_rc"
191
+ fi
192
+
193
+ # The dispatches see a review snapshot, not the repo, so the run dir names it for the dataset.
194
+ [ "$DRY" = 1 ] || printf '%s\n' "$REPO" > "$RUN/repo"
195
+ # Every real run, --ui or not: forge-ui's view of this repo's runs reads the registry.
196
+ [ "$DRY" = 1 ] || forge_ui_register "$RUN"
197
+ # Recorded before any dispatch: forge-ui opens this dir immediately, and a re-run
198
+ # that dies early must not keep showing the previous invocation's verdict.
199
+ if [ "$DRY" != 1 ]; then
200
+ forge_metric_begin "$RUN" solo
201
+ printf '%s\n' RUNNING > "$RUN/status"
202
+ printf '%s\n' "$DWARF" > "$RUN/dwarf.spec"
203
+ printf '%s\n' "$QA" > "$RUN/qa.spec"
204
+ rm -f "$RUN/verdict" "$RUN/dwarf.infra" "$RUN/qa.infra"
205
+ fi
206
+
207
+ # --- dwarf --------------------------------------------------------------------
208
+ /bin/bash "$MEMORY" inject "$REPO" dwarf --run-dir "$RUN" > "$RUN/dwarf.memory"
209
+ python3 "$SKILL_DIR/scripts/forge-prompt.py" --requirements "$RUN/prompt.md" --approach "$APPROACH" --memory "$RUN/dwarf.memory" > "$RUN/context.md" || exit 3
210
+ {
211
+ cat "$RUN/context.md"
212
+ cat <<'EOF'
213
+
214
+ Edit the files in this repository directly. When you are done, run whatever tests
215
+ or checks the project already has and report the result.
216
+
217
+ You are running headless: nobody is at the other end and no one can answer a
218
+ question. If something is ambiguous, pick the most reasonable interpretation,
219
+ proceed, and state the assumption in your final message.
220
+ EOF
221
+ echo; python3 "$SKILL_DIR/scripts/forge-contract.py" dwarf
222
+ echo; /bin/bash "$MEMORY" note dwarf
223
+ } > "$RUN/dwarf.input"
224
+
225
+ if [ "$DRY" = 1 ]; then
226
+ echo "--- dry run ---"
227
+ [ "$FORGE_UI_WANTED" != 1 ] || note "--ui ignored: a dry run starts no UI"
228
+ /bin/bash "$DISPATCH" dwarf "$DWARF" --repo "$REPO" --run-dir "$RUN" \
229
+ --ripwire-query-file "$RUN/prompt.md" --prompt-file "$RUN/dwarf.input" $YD $TFLAG --dry-run
230
+ echo
231
+ echo "qa would be: $QA $NATIVE $YQ"
232
+ echo "dwarf prompt: $RUN/dwarf.input ($(wc -l < "$RUN/dwarf.input" | tr -d ' ') lines)"
233
+ exit 0
234
+ fi
235
+
236
+ forge_metric_phase preflight
237
+ /bin/bash "$DISPATCH" doctor --spec "$DWARF" --role dwarf $YD $TFLAG || exit $?
238
+ /bin/bash "$DISPATCH" doctor --spec "$QA" --role qa $YQ $NATIVE $TFLAG || exit $?
239
+ /bin/bash "$SKILL_DIR/scripts/forge-install-ripwire.sh" || true
240
+ # After the _lock re-exec and the Fractal checkpoint exit, so it runs once per run.
241
+ forge_ui_launch "$RUN"
242
+ forge_metric_phase snapshot
243
+ if [ -n "${FORGE_FRACTAL_RUN:-}" ] && [ -s "$RUN/start.tree" ]; then
244
+ START="$(cat "$RUN/start.tree")"
245
+ else
246
+ START="$(forge_tree "$REPO")" || die "cannot record starting snapshot" 3
247
+ fi
248
+ echo "$START" > "$RUN/start.tree"
249
+ git -C "$REPO" diff --binary HEAD "$START" -- . ':(exclude).forge' > "$RUN/existing.diff"
250
+ forge_metric_phase dispatch
251
+ note "dwarf $DWARF in $REPO"
252
+ guard_before_dwarf "$REPO" "$RUN"
253
+ /bin/bash "$DISPATCH" dwarf "$DWARF" --repo "$REPO" --run-dir "$RUN" \
254
+ --ripwire-query-file "$RUN/prompt.md" --prompt-file "$RUN/dwarf.input" $YD $TFLAG --output "$OUTPUT"
255
+ rc=$?
256
+ dur_dwarf="$(sed -n 's/^duration_s=//p' "$RUN/dwarf.resolved" 2>/dev/null | tail -1)"
257
+ /bin/bash "$MEMORY" record "$REPO" --last "$RUN/dwarf.last" --role dwarf \
258
+ --run-id "$RUN_ID" --model "$DWARF" --run-dir "$RUN" \
259
+ --duration "${dur_dwarf:--}" >/dev/null 2>&1
260
+ [ "$rc" = 7 ] && die "the dwarf exceeded its timeout — see $RUN/dwarf.log" 7
261
+ [ "$rc" = 8 ] && die "the dwarf hit an infrastructure failure ($(sed -n 's/^class=//p' "$RUN/dwarf.infra" 2>/dev/null | head -1)) — nothing was reviewed; fix the cause and run again (see $RUN/dwarf.infra)" 8
262
+ [ "$rc" -ne 0 ] && die "the dwarf dispatch failed — see $RUN/dwarf.log" 4
263
+ guard_after_dwarf "$REPO" "$RUN" solo
264
+
265
+ forge_metric_phase snapshot
266
+ # --- the real diff ------------------------------------------------------------
267
+ # Diff complete filesystem snapshots; the private index preserves user staging.
268
+ END="$(forge_tree "$REPO")" || die "cannot capture implementation" 3
269
+ echo "$END" > "$RUN/reviewed.tree"
270
+ git -C "$REPO" diff --binary "$START" "$END" -- . ':(exclude).forge' > "$RUN/changes.diff" || die "cannot create diff" 3
271
+
272
+ if [ ! -s "$RUN/changes.diff" ]; then
273
+ # An exit-0 harness with an empty final message and no changes never really ran.
274
+ grep -qx 'class=empty' "$RUN/dwarf.infra" 2>/dev/null && die "the dwarf returned nothing (empty output, no changes) — an infrastructure failure, not a refusal; fix the cause and run again (see $RUN/dwarf.infra)" 8
275
+ note "the dwarf produced no changes at all — see $RUN/dwarf.last for what it said"
276
+ echo NOCHANGES > "$RUN/verdict"
277
+ exit 5
278
+ fi
279
+
280
+ REVIEW="$RUN/review-$(date +%s)-$$"
281
+ forge_review_snapshot "$REPO" "$START" "$END" "$REVIEW" || die "cannot create review snapshot" 3
282
+ SOURCE_FP="$(forge_fingerprint "$REPO")" || exit 3
283
+ [ "$(forge_tree "$REPO")" = "$END" ] || { echo INVALIDATED > "$RUN/verdict"; die "source changed while preparing QA" 5; }
284
+ REVIEW_FP="$(forge_fingerprint "$REVIEW")" || exit 3
285
+ printf '%s\n' "$SOURCE_FP" > "$RUN/source.fingerprint"
286
+ printf '%s\n' "$REVIEW_FP" > "$RUN/review.fingerprint"
287
+ forge_metric_phase preparation
288
+ # --- qa -----------------------------------------------------------------------
289
+ rm -f "$RUN/failfirst.json" "$RUN/failfirst.txt"
290
+ if [ "$FAIL_FIRST" = 1 ]; then
291
+ # Opt-in and advisory: the reviewer reads the evidence; nothing here changes a verdict.
292
+ forge_fail_first "$REVIEW" "$RUN" "$REPO"
293
+ note "fail-first evidence for the reviewer: $RUN/failfirst.json"
294
+ fi
295
+ /bin/bash "$MEMORY" inject "$REPO" qa --run-dir "$RUN" > "$RUN/qa.memory"
296
+ {
297
+ python3 "$SKILL_DIR/scripts/forge-prompt.py" --requirements "$RUN/prompt.md" --approach "$APPROACH" --goal "$RUN/goal.txt" --memory "$RUN/dwarf.memory" --memory "$RUN/qa.memory"
298
+ python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part head --kind solo
299
+ echo
300
+ if [ -s "$RUN/failfirst.txt" ]; then cat "$RUN/failfirst.txt"; echo; fi
301
+ python3 "$SKILL_DIR/scripts/forge-contract.py" qa --part tail --kind solo
302
+ echo
303
+ echo '```diff'; cat "$RUN/changes.diff"; echo '```'
304
+ echo; /bin/bash "$MEMORY" note qa
305
+ echo "Place any learning notes before the final FORGE_VERDICT: PASS or FORGE_VERDICT: FAIL line."
306
+ } > "$RUN/qa.input"
307
+
308
+ forge_metric_phase dispatch
309
+ note "qa $QA on $(wc -l < "$RUN/changes.diff" | tr -d ' ') diff lines"
310
+ /bin/bash "$DISPATCH" qa "$QA" --repo "$REVIEW" --review-base "$(cat "$RUN/review.base")" --run-dir "$RUN" \
311
+ --ripwire-query-file "$RUN/prompt.md" --prompt-file "$RUN/qa.input" $YQ $NATIVE $TFLAG --output "$OUTPUT"
312
+ rc=$?
313
+ forge_metric_phase verification
314
+ verdict="$(forge_verdict "$RUN/qa.last")"
315
+ [ -n "$NATIVE" ] && verdict=UNKNOWN
316
+ if [ "$(forge_fingerprint "$REPO")" != "$SOURCE_FP" ] || [ "$(forge_fingerprint "$REVIEW")" != "$REVIEW_FP" ]; then
317
+ echo INVALIDATED > "$RUN/verdict"
318
+ die "source or review artifact changed during QA; review invalidated" 5
319
+ fi
320
+ dur_qa="$(sed -n 's/^duration_s=//p' "$RUN/qa.resolved" 2>/dev/null | tail -1)"
321
+ /bin/bash "$MEMORY" record "$REPO" --last "$RUN/qa.last" --role qa \
322
+ --run-id "$RUN_ID" --model "$QA" --verdict "${verdict:-UNKNOWN}" --run-dir "$RUN" \
323
+ --duration "${dur_qa:--}" >/dev/null 2>&1
324
+ [ "$rc" = 7 ] && die "qa exceeded its timeout — the dwarf's changes are still in the tree; see $RUN/qa.log" 7
325
+ [ "$rc" = 8 ] && die "qa hit an infrastructure failure ($(sed -n 's/^class=//p' "$RUN/qa.infra" 2>/dev/null | head -1)) — the dwarf's changes are still in the tree and unreviewed; fix the cause and review again (see $RUN/qa.infra)" 8
326
+ [ "$rc" -ne 0 ] && die "the qa dispatch failed — the dwarf's changes are still in the tree; see $RUN/qa.log" 4
327
+
328
+ # --native-review is codex's own reviewer, which refuses a custom prompt alongside
329
+ # its scope flags — so it never sees the verdict instruction and cannot emit one.
330
+ echo "${verdict:-UNKNOWN}" > "$RUN/verdict"
331
+
332
+ echo
333
+ echo "dwarf: $DWARF ${dur_dwarf:-?}s"
334
+ echo "qa: $QA ${dur_qa:-?}s verdict: ${verdict:-UNKNOWN}"
335
+ echo "diff: $RUN/changes.diff"
336
+ echo "read: $RUN/dwarf.last and $RUN/qa.last"
337
+ echo
338
+ if grep -q '^self-commit' "$RUN/guard.txt" 2>/dev/null; then
339
+ echo "The dwarf COMMITTED in $REPO although it was told not to. The review above covers its"
340
+ echo "changes either way. To turn its commit(s) back into uncommitted changes:"
341
+ echo " git -C $REPO reset --soft $(cat "$RUN/dwarf.head.before" 2>/dev/null)"
342
+ echo "Forge itself committed, pushed and reset nothing."
343
+ else
344
+ echo "The changes are UNCOMMITTED in $REPO. Nothing was committed, pushed or reset."
345
+ fi
346
+ [ ! -s "$RUN/guard.txt" ] || { echo; echo "implementer guard notes:"; sed 's/^/ /' "$RUN/guard.txt"; }
347
+ forge_update_notice
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # Source in runners (forge-solo.sh, forge-parallel.sh plan and run). Compatible with macOS Bash 3.2.
3
+ #
4
+ # forge-ui is opt-in per run: the orchestrator asks the user and passes the answer as
5
+ # --ui. The flag is never persisted, and launching never decides whether the run goes on.
6
+ # Registering a run (forge_ui_register) is not part of that choice: it happens on every
7
+ # run, so forge-ui can later show all of a repo's runs, including those never opened in it.
8
+ FORGE_UI_WANTED=0
9
+
10
+ # forge_ui_flag ARG — returns 0 and records the choice for --ui / --no-ui, 1 for anything else.
11
+ forge_ui_flag() {
12
+ case "${1:-}" in
13
+ --ui) FORGE_UI_WANTED=1 ;;
14
+ --no-ui) FORGE_UI_WANTED=0 ;;
15
+ *) return 1 ;;
16
+ esac
17
+ }
18
+
19
+ # forge_ui_launch DIR — open DIR in the shared forge-ui server when --ui was given.
20
+ # Always returns 0: a UI that cannot start must not change the run's exit code. The
21
+ # launcher's stdin is /dev/null so it can never wait for a terminal.
22
+ forge_ui_launch() {
23
+ [ "${FORGE_UI_WANTED:-0}" = 1 ] || return 0
24
+ if [ "${FORGE_UI:-}" = off ]; then
25
+ printf 'forge: FORGE_UI=off; not starting forge-ui\n' >&2
26
+ return 0
27
+ fi
28
+ python3 "$SKILL_DIR/scripts/forge-ui.py" open "$1" </dev/null \
29
+ || printf 'forge: forge-ui did not start; the run goes on (later: %s/forge ui open %s)\n' "$SKILL_DIR" "$1" >&2
30
+ return 0
31
+ }
32
+
33
+ # forge_ui_register DIR — note DIR in forge's run registry ($XDG_CONFIG_HOME/forge/runs),
34
+ # under the repo named in DIR/repo. Called on every run, whatever --ui and FORGE_UI say,
35
+ # because forge-ui's view of a repo's runs reads that registry. Always returns 0 and prints
36
+ # nothing: the registry can be unwritable or the repo file odd, and neither may change a
37
+ # run's output or exit code. -I keeps a PYTHON* variable or a stray module in the current
38
+ # directory from changing what runs. FORGE_RUN_REGISTRY=off turns it off.
39
+ forge_ui_register() {
40
+ python3 -I "$SKILL_DIR/scripts/forge_registry.py" register "$1" </dev/null >/dev/null 2>&1 || true
41
+ return 0
42
+ }