@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.
- package/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +1854 -2
- package/SKILL.md +122 -0
- package/VERSION +1 -0
- package/forge +74 -0
- package/package.json +26 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +353 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +665 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runtime.py +533 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +198 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +83 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +287 -0
- package/ui/THIRD_PARTY_NOTICES.md +69 -0
- package/ui/docs/api.md +888 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +204 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +583 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1336 -0
- package/ui/forge_ui/server.py +303 -0
- package/ui/forge_ui/static/app.js +924 -0
- package/ui/forge_ui/static/cloud3d.js +646 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +394 -0
- package/ui/forge_ui/static/diffview.js +216 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/index.html +55 -0
- package/ui/forge_ui/static/iso.js +765 -0
- package/ui/forge_ui/static/leaderboard.js +315 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +526 -0
- package/ui/forge_ui/static/runmodel.js +651 -0
- package/ui/forge_ui/static/scene3d.js +996 -0
- package/ui/forge_ui/static/search.js +119 -0
- package/ui/forge_ui/static/style.css +618 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plan operations for forge-parallel.sh: `accept`, `combine` and `split`.
|
|
3
|
+
#
|
|
4
|
+
# Sourced by forge-parallel.sh, which provides die/note/field/all_ids/task_status,
|
|
5
|
+
# merge_task, compute_waves, write_results, record_attempt, run_worktree_setup,
|
|
6
|
+
# forge_fingerprint and SELF. Functions only; nothing runs at source time.
|
|
7
|
+
#
|
|
8
|
+
# accept <plan> <task> --reason "<why>" --approved (runs under the plan's lock)
|
|
9
|
+
# split <plan> <new-plan> --tasks a,b [--dry-run] (runs under the plan's lock)
|
|
10
|
+
# combine <new-plan> <plan-a> <plan-b>... [--verify CMD] [--dry-run]
|
|
11
|
+
#
|
|
12
|
+
# Exit codes follow forge-parallel.sh: 0 ok, 2 usage, 3 precondition (nothing was
|
|
13
|
+
# changed), 6 integration conflict (anything created was removed again).
|
|
14
|
+
#
|
|
15
|
+
# `combine` and `split` only ever move git refs and worktrees that forge itself made
|
|
16
|
+
# (forge/<run>-integration, forge/<run>/<task>, <wt_root>/...). Nothing they do touches
|
|
17
|
+
# the user's branches or working tree.
|
|
18
|
+
|
|
19
|
+
# --- shared helpers -------------------------------------------------------------
|
|
20
|
+
plans_abs() { # absolute path of a directory that may not exist yet
|
|
21
|
+
if [ -d "$1" ]; then (cd "$1" && pwd)
|
|
22
|
+
else python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "$1"
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
plans_dir_empty() { # plans_dir_empty <dir>: true when it holds nothing at all
|
|
27
|
+
local f
|
|
28
|
+
for f in "$1"/* "$1"/.[!.]* "$1"/..?*; do
|
|
29
|
+
if [ -e "$f" ] || [ -L "$f" ]; then return 1; fi
|
|
30
|
+
done
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
plans_run_id() { basename "$1" | sed 's/^forge-//'; }
|
|
35
|
+
|
|
36
|
+
plans_short() { printf '%s' "${1:0:7}"; }
|
|
37
|
+
|
|
38
|
+
# What `run` would call this plan's worktree root: beside the repository, never in TMPDIR.
|
|
39
|
+
plans_wt_root_for() { # plans_wt_root_for <repo path as written in a plan> <run id>
|
|
40
|
+
printf '%s/.forge-worktrees/%s' "$(cd "$1/.." && pwd)" "$2"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Refuses (exit 3, nothing touched) unless <new-plan> and its git names are free to use.
|
|
44
|
+
plans_target_checks() { # plans_target_checks <repo> <new-plan> <run id> <wt root>
|
|
45
|
+
local repo="$1" new="$2" rid="$3" wtr="$4"
|
|
46
|
+
[ -n "$rid" ] || die "'$new' has no usable name: its run id is the directory name minus a leading 'forge-'" 3
|
|
47
|
+
git check-ref-format "refs/heads/forge/$rid-integration" >/dev/null 2>&1 &&
|
|
48
|
+
git check-ref-format "refs/heads/forge/$rid/x" >/dev/null 2>&1 ||
|
|
49
|
+
die "'$rid' (from '$new') cannot be part of a branch name; use a plan directory name of plain letters, digits, '-' and '_'" 3
|
|
50
|
+
if [ -e "$new" ] || [ -L "$new" ]; then
|
|
51
|
+
[ -d "$new" ] || die "'$new' exists and is not a directory" 3
|
|
52
|
+
plans_dir_empty "$new" || die "'$new' already exists and is not empty; pick a new plan directory" 3
|
|
53
|
+
fi
|
|
54
|
+
if git -C "$repo" show-ref --verify --quiet "refs/heads/forge/$rid-integration"; then
|
|
55
|
+
die "branch forge/$rid-integration already exists; pick a different plan directory name" 3
|
|
56
|
+
fi
|
|
57
|
+
if git -C "$repo" show-ref --verify --quiet "refs/heads/forge/$rid"; then
|
|
58
|
+
die "branch forge/$rid would block the task branches forge/$rid/<task> (git refs are files); pick a different plan directory name" 3
|
|
59
|
+
fi
|
|
60
|
+
if [ -n "$(git -C "$repo" for-each-ref --count=1 --format=x "refs/heads/forge/$rid/" 2>/dev/null)" ]; then
|
|
61
|
+
die "branches forge/$rid/... already exist; pick a different plan directory name" 3
|
|
62
|
+
fi
|
|
63
|
+
if [ -e "$wtr" ]; then
|
|
64
|
+
plans_dir_empty "$wtr" || die "worktree root '$wtr' already exists; pick a different plan directory name" 3
|
|
65
|
+
fi
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Copies a task's artifact directory, minus review snapshots (review-<attempt>-<pid>/ is a
|
|
70
|
+
# full export of the reviewed tree, rebuilt by every review, and nothing reads it back).
|
|
71
|
+
plans_copy_taskdir() { # plans_copy_taskdir <src dir> <dst dir>
|
|
72
|
+
local src="$1" dst="$2" f
|
|
73
|
+
mkdir -p "$dst" || return 1
|
|
74
|
+
for f in "$src"/* "$src"/.[!.]*; do
|
|
75
|
+
if [ -e "$f" ] || [ -L "$f" ]; then
|
|
76
|
+
case "$(basename "$f")" in review-*) continue ;; esac
|
|
77
|
+
cp -pR "$f" "$dst/" || return 1
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# The state `undo` must reverse. Globals, so an interrupt trap can reach them.
|
|
84
|
+
plans_reset_undo() {
|
|
85
|
+
PLANS_U_REPO=""; PLANS_U_BR=""; PLANS_U_WT=""; PLANS_U_WTR=""; PLANS_U_WTR_NEW=0
|
|
86
|
+
PLANS_U_PLAN=""; PLANS_U_PLAN_NEW=0; PLANS_U_JOURNAL=""
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Reverses everything combine/split created, newest first. Best effort and silent: it runs
|
|
90
|
+
# on the way out of a failure that has already been reported.
|
|
91
|
+
plans_undo() {
|
|
92
|
+
local kind a b line
|
|
93
|
+
trap - INT TERM
|
|
94
|
+
while IFS="$(printf '\t')" read -r kind a b; do
|
|
95
|
+
case "$kind" in
|
|
96
|
+
head) git -C "$a" symbolic-ref HEAD "refs/heads/$b" >/dev/null 2>&1 ;;
|
|
97
|
+
move) git -C "$PLANS_U_REPO" worktree move "$b" "$a" >/dev/null 2>&1 ;;
|
|
98
|
+
branch) git -C "$PLANS_U_REPO" branch -D "$a" >/dev/null 2>&1 ;;
|
|
99
|
+
esac
|
|
100
|
+
done <<EOF
|
|
101
|
+
$PLANS_U_JOURNAL
|
|
102
|
+
EOF
|
|
103
|
+
if [ -n "$PLANS_U_WT" ]; then
|
|
104
|
+
git -C "$PLANS_U_REPO" worktree remove --force "$PLANS_U_WT" >/dev/null 2>&1
|
|
105
|
+
rm -rf "$PLANS_U_WT"
|
|
106
|
+
fi
|
|
107
|
+
[ -z "$PLANS_U_BR" ] || git -C "$PLANS_U_REPO" branch -D "$PLANS_U_BR" >/dev/null 2>&1
|
|
108
|
+
if [ "$PLANS_U_WTR_NEW" = 1 ] && [ -n "$PLANS_U_WTR" ]; then rm -rf "$PLANS_U_WTR"; fi
|
|
109
|
+
if [ -n "$PLANS_U_PLAN" ]; then
|
|
110
|
+
if [ "$PLANS_U_PLAN_NEW" = 1 ]; then rm -rf "$PLANS_U_PLAN"
|
|
111
|
+
else rm -rf "$PLANS_U_PLAN"/* "$PLANS_U_PLAN"/.[!.]* 2>/dev/null
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
[ -z "$PLANS_U_REPO" ] || git -C "$PLANS_U_REPO" worktree prune >/dev/null 2>&1
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
plans_journal() { # plans_journal <kind> <a> <b>: newest entry first
|
|
119
|
+
PLANS_U_JOURNAL="$1$(printf '\t')$2$(printf '\t')$3
|
|
120
|
+
$PLANS_U_JOURNAL"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Creates <branch> at <commit> with a worktree at <wt>, both recorded for undo.
|
|
124
|
+
plans_make_integration() { # plans_make_integration <repo> <branch> <commit> <wt>
|
|
125
|
+
git -C "$1" branch "$2" "$3" >/dev/null 2>&1 || return 1
|
|
126
|
+
PLANS_U_BR="$2"
|
|
127
|
+
git -C "$1" worktree add "$4" "$2" >/dev/null 2>&1 || return 1
|
|
128
|
+
PLANS_U_WT="$4"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Per-plan option files carried to a derived plan. The cost-bearing choices a user made
|
|
132
|
+
# for this plan apply to the plan made from it; `planner` does not (nothing plans again).
|
|
133
|
+
PLANS_OPTION_FILES="no_ripwire no_memory yolo_dwarf yolo_qa qa_threshold timeout verify_retries verify_cmd setup_cmd final_qa shadow_qa shadow_rate explore escalate_after fractal-routing.json fractal-selection.json jev-selection.json"
|
|
134
|
+
|
|
135
|
+
# --- accept ---------------------------------------------------------------------
|
|
136
|
+
# A reviewer's FAIL is the QA gate; overriding it is the user's call and never forge's or
|
|
137
|
+
# the orchestrator's, hence both --reason and --approved. Only work the reviewer actually
|
|
138
|
+
# judged can be accepted: the branch tip and the worktree must still be exactly what was
|
|
139
|
+
# reviewed, or the human would be signing off on code nobody reviewed.
|
|
140
|
+
plans_why_not() { # plans_why_not <plan> <id> <status>: exit 3 with advice for a status accept cannot take
|
|
141
|
+
local plan="$1" id="$2" st="$3"
|
|
142
|
+
case "$st" in
|
|
143
|
+
MERGED) die "task '$id' is already merged — nothing to accept" 3 ;;
|
|
144
|
+
PASS) die "task '$id' already passed review; 'forge-parallel.sh run $plan' merges it — nothing to accept" 3 ;;
|
|
145
|
+
INVALIDATED) die "task '$id' is INVALIDATED: its work or the integration branch changed after the review, so the review no longer describes it. Use 'forge-parallel.sh retry $plan $id' for a fresh review" 3 ;;
|
|
146
|
+
CONFLICT) die "task '$id' is CONFLICT: its reviewed work does not merge cleanly, so accepting it cannot help. Resolve the conflict or 'forge-parallel.sh retry $plan $id'" 3 ;;
|
|
147
|
+
ERROR|TIMEOUT|INFRA|INTERRUPTED) die "task '$id' is $st: no reviewer verdict exists to override. Use 'forge-parallel.sh retry $plan $id' (or 'run')" 3 ;;
|
|
148
|
+
BLOCKED|PENDING) die "task '$id' is $st: it has not been reviewed yet, so there is nothing to override" 3 ;;
|
|
149
|
+
RUNNING) die "task '$id' is RUNNING (or a run was killed): re-run 'forge-parallel.sh run $plan' so it settles first" 3 ;;
|
|
150
|
+
*) die "task '$id' has status '$st'; only a FAIL or UNKNOWN task can be accepted" 3 ;;
|
|
151
|
+
esac
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Prints the reviewer's text for known_issues.md: the reply minus the verdict line, and,
|
|
155
|
+
# when the last attempt was a no-progress retry whose qa.last is just forge's explanation,
|
|
156
|
+
# the real review that retry carried forward.
|
|
157
|
+
plans_reviewer_text() { # plans_reviewer_text <tdir> <status>
|
|
158
|
+
local tdir="$1" st="$2"
|
|
159
|
+
if [ -s "$tdir/qa.last" ]; then tr -d '\r' < "$tdir/qa.last" | grep -v '^FORGE_VERDICT:'; fi
|
|
160
|
+
if [ "$st" = FAIL ] && [ -s "$tdir/retry_findings.md" ] && ! grep -q '^FORGE_VERDICT:' "$tdir/qa.last" 2>/dev/null; then
|
|
161
|
+
echo
|
|
162
|
+
echo "Earlier review of the same work (retry_findings.md):"
|
|
163
|
+
echo
|
|
164
|
+
tr -d '\r' < "$tdir/retry_findings.md" | grep -v '^FORGE_VERDICT:'
|
|
165
|
+
fi
|
|
166
|
+
return 0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
do_accept() {
|
|
170
|
+
local PLAN="${1:?accept needs a plan dir}" id="" reason="" approved=0
|
|
171
|
+
shift
|
|
172
|
+
while [ $# -gt 0 ]; do
|
|
173
|
+
case "$1" in
|
|
174
|
+
--reason) [ $# -ge 2 ] || die "accept: --reason needs a text"; reason="$2"; shift 2 ;;
|
|
175
|
+
--approved) approved=1; shift ;;
|
|
176
|
+
-*) die "accept: unknown option '$1'" ;;
|
|
177
|
+
*) [ -z "$id" ] || die "accept: unexpected argument '$1'"; id="$1"; shift ;;
|
|
178
|
+
esac
|
|
179
|
+
done
|
|
180
|
+
[ -n "$id" ] || die "usage: forge-parallel.sh accept <plan-dir> <task-id> --reason \"<why>\" --approved"
|
|
181
|
+
reason="$(printf '%s' "$reason" | tr '\n\r\t' ' ' | sed 's/^ *//; s/ *$//')"
|
|
182
|
+
[ -n "$reason" ] || die "accept needs --reason \"<why this work is acceptable despite the reviewer>\": it is recorded with the work"
|
|
183
|
+
[ "$approved" = 1 ] || die "accept needs --approved: it overrides the reviewer's verdict and is only run on the user's explicit instruction"
|
|
184
|
+
|
|
185
|
+
local T="$PLAN/tasks.tsv" tdir="$PLAN/tasks/$id" REPO run_id wt br int_wt st dep dst
|
|
186
|
+
[ -s "$T" ] || die "no tasks.tsv in '$PLAN'"
|
|
187
|
+
all_ids "$T" | grep -qx "$id" || die "no task '$id' in this plan"
|
|
188
|
+
[ -s "$PLAN/wt_root" ] && [ -s "$PLAN/run_id" ] && [ -s "$PLAN/repo" ] \
|
|
189
|
+
|| die "this plan has never been run, so there is no reviewed work to accept" 3
|
|
190
|
+
REPO="$(cat "$PLAN/repo")"; run_id="$(cat "$PLAN/run_id")"
|
|
191
|
+
wt="$(cat "$PLAN/wt_root")/$id"; br="forge/$run_id/$id"
|
|
192
|
+
int_wt="$(cat "$PLAN/wt_root")/_integration"
|
|
193
|
+
|
|
194
|
+
st="$(task_status "$PLAN" "$id")"
|
|
195
|
+
case "$st" in FAIL|UNKNOWN) ;; *) plans_why_not "$PLAN" "$id" "$st" ;; esac
|
|
196
|
+
|
|
197
|
+
# The reviewed work must be exactly what is on disk: same commit on the branch, same
|
|
198
|
+
# tree, index and HEAD in the worktree as when QA finished.
|
|
199
|
+
[ -s "$tdir/reviewed.commit" ] && [ -s "$tdir/source.fingerprint" ] \
|
|
200
|
+
|| die "task '$id' has no saved review state (it never reached a reviewed commit); use 'forge-parallel.sh retry $PLAN $id'" 3
|
|
201
|
+
[ -e "$wt/.git" ] || die "task '$id': its worktree $wt is gone, so the reviewed work cannot be verified; use 'forge-parallel.sh retry $PLAN $id'" 3
|
|
202
|
+
[ "$(git -C "$REPO" rev-parse --verify --quiet "refs/heads/$br" 2>/dev/null)" = "$(cat "$tdir/reviewed.commit")" ] \
|
|
203
|
+
|| die "task '$id': branch $br is no longer at the commit that was reviewed — the work changed since review. Use 'forge-parallel.sh retry $PLAN $id' for a fresh review" 3
|
|
204
|
+
[ "$(forge_fingerprint "$wt" 2>/dev/null)" = "$(cat "$tdir/source.fingerprint")" ] \
|
|
205
|
+
|| die "task '$id': the worktree $wt changed since it was reviewed (edited, staged or committed). Accepting would sign off on code no reviewer saw; use 'forge-parallel.sh retry $PLAN $id' for a fresh review" 3
|
|
206
|
+
|
|
207
|
+
# Checked here, not left to merge_task: it records INVALIDATED/BLOCKED on a task it
|
|
208
|
+
# refuses, and a refused accept must leave the task exactly as it was.
|
|
209
|
+
for dep in $(field "$T" "$id" deps | tr ',' ' '); do
|
|
210
|
+
[ "$dep" = - ] && continue
|
|
211
|
+
dst="$(task_status "$PLAN" "$dep")"
|
|
212
|
+
[ "$dst" = MERGED ] || die "task '$id' depends on '$dep', which is not merged (status $dst); merge or accept '$dep' first" 3
|
|
213
|
+
done
|
|
214
|
+
[ -e "$int_wt/.git" ] || die "the integration worktree $int_wt is missing" 3
|
|
215
|
+
[ -s "$PLAN/accepted.integration" ] && [ "$(git -C "$int_wt" rev-parse HEAD 2>/dev/null)" = "$(cat "$PLAN/accepted.integration")" ] \
|
|
216
|
+
|| die "integration branch changed outside this run; refusing to merge onto it" 3
|
|
217
|
+
[ -z "$(git -C "$int_wt" status --porcelain -- . ':(exclude).forge' 2>/dev/null)" ] \
|
|
218
|
+
|| die "the integration worktree $int_wt has uncommitted changes; refusing to merge onto it" 3
|
|
219
|
+
|
|
220
|
+
# Record first, merge second, and put it all back if the merge does not happen.
|
|
221
|
+
local ki="$tdir/known_issues.md" kibak="$tdir/.known_issues.before" tmp="$tdir/.known_issues.new"
|
|
222
|
+
local who="${USER:-${LOGNAME:-unknown}}" when body n sha attempt
|
|
223
|
+
when="$(date '+%Y-%m-%dT%H:%M:%S%z')"
|
|
224
|
+
attempt="$(cat "$tdir/attempt" 2>/dev/null || echo 0)"
|
|
225
|
+
sha="$(cat "$tdir/reviewed.commit")"
|
|
226
|
+
body="$(plans_reviewer_text "$tdir" "$st")"
|
|
227
|
+
n="$(printf '%s\n' "$body" | grep -c '^- ')"
|
|
228
|
+
rm -f "$kibak"; [ -f "$ki" ] && cp -p "$ki" "$kibak"
|
|
229
|
+
{
|
|
230
|
+
if [ -s "$ki" ]; then cat "$ki"; echo; fi
|
|
231
|
+
echo "## Accepted by a human despite the reviewer's $st verdict"
|
|
232
|
+
echo
|
|
233
|
+
echo "Reason: $reason"
|
|
234
|
+
echo "Accepted by $who on $when (task $id, attempt $attempt, reviewed commit ${sha:0:12})."
|
|
235
|
+
echo "The reviewer's reply follows, copied from tasks/$id/qa.last. None of it was fixed."
|
|
236
|
+
echo
|
|
237
|
+
[ -z "$body" ] || printf '%s\n' "$body"
|
|
238
|
+
if [ "$n" = 0 ]; then
|
|
239
|
+
echo
|
|
240
|
+
echo "- [$st] the reviewer left no itemised findings; read tasks/$id/qa.last"
|
|
241
|
+
fi
|
|
242
|
+
} > "$tmp" && mv "$tmp" "$ki" || { rm -f "$tmp"; die "could not write $ki" 3; }
|
|
243
|
+
{
|
|
244
|
+
echo "task=$id"
|
|
245
|
+
echo "status=$st"
|
|
246
|
+
echo "reason=$reason"
|
|
247
|
+
echo "by=$who"
|
|
248
|
+
echo "at=$when"
|
|
249
|
+
echo "epoch=$(date +%s)"
|
|
250
|
+
echo "attempt=$attempt"
|
|
251
|
+
echo "reviewed_commit=$sha"
|
|
252
|
+
} > "$tdir/accepted.by"
|
|
253
|
+
echo PASS > "$tdir/status"
|
|
254
|
+
|
|
255
|
+
if ! merge_task "$PLAN" "$id"; then
|
|
256
|
+
# merge_task already wrote CONFLICT or INVALIDATED. The task was not accepted, so the
|
|
257
|
+
# acceptance record goes too; a stale known_issues.md would be listed as accepted work.
|
|
258
|
+
rm -f "$tdir/accepted.by" "$ki"
|
|
259
|
+
[ -f "$kibak" ] && mv "$kibak" "$ki"
|
|
260
|
+
write_results "$PLAN"
|
|
261
|
+
die "task '$id' could not be merged (status now $(task_status "$PLAN" "$id")); nothing was accepted. See 'git -C $int_wt status' and the task branch $br" 6
|
|
262
|
+
fi
|
|
263
|
+
rm -f "$kibak"
|
|
264
|
+
record_attempt "$PLAN" "$id"
|
|
265
|
+
write_results "$PLAN"
|
|
266
|
+
echo "$id accepted by a human and merged"
|
|
267
|
+
echo "known issues recorded in $ki"
|
|
268
|
+
if [ "$(awk -F'\t' '$2!="MERGED" {n++} END {print n+0}' "$PLAN/results.tsv")" -gt 0 ]; then
|
|
269
|
+
echo "tasks that were waiting on it are unchanged; continue with: forge-parallel.sh run $PLAN"
|
|
270
|
+
fi
|
|
271
|
+
return 0
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# --- combine --------------------------------------------------------------------
|
|
275
|
+
# Every source plan is locked for the whole operation (a concurrent run would move the
|
|
276
|
+
# very integration branch being merged). The lock is taken by re-exec, like the router's
|
|
277
|
+
# own lock for run/retry/integrate, so the file descriptors stay open for bash.
|
|
278
|
+
PLANS_LOCK_PY='
|
|
279
|
+
import fcntl, os, sys
|
|
280
|
+
script, count = sys.argv[1], int(sys.argv[2])
|
|
281
|
+
held = []
|
|
282
|
+
for path in sys.argv[3:3 + count]:
|
|
283
|
+
try:
|
|
284
|
+
handle = open(path, "a")
|
|
285
|
+
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
286
|
+
except OSError as error:
|
|
287
|
+
sys.stderr.write("forge: plan %s is locked by another forge command (%s); wait for it to finish\n"
|
|
288
|
+
% (os.path.dirname(path), error))
|
|
289
|
+
sys.exit(3)
|
|
290
|
+
os.set_inheritable(handle.fileno(), True)
|
|
291
|
+
held.append(handle)
|
|
292
|
+
env = dict(os.environ, FORGE_COMBINE_LOCKED="1")
|
|
293
|
+
os.execve("/bin/bash", ["/bin/bash", script, "combine"] + sys.argv[3 + count:], env)
|
|
294
|
+
'
|
|
295
|
+
|
|
296
|
+
plans_predict_conflict() { # 0 merges cleanly, 1 would conflict, 2 cannot tell (old git)
|
|
297
|
+
git -C "$1" merge-tree --write-tree "$2" "$3" >/dev/null 2>&1
|
|
298
|
+
case "$?" in 0) return 0 ;; 1) return 1 ;; *) return 2 ;; esac
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
do_combine() {
|
|
302
|
+
local orig=("$@") NEW="" VERIFY="" DRY=0 SRC=() p f i n id d
|
|
303
|
+
[ $# -ge 1 ] || die "usage: forge-parallel.sh combine <new-plan> <plan-a> <plan-b> [<plan>...] [--verify <command>] [--dry-run]"
|
|
304
|
+
NEW="$1"; shift
|
|
305
|
+
while [ $# -gt 0 ]; do
|
|
306
|
+
case "$1" in
|
|
307
|
+
--verify) [ -n "${2:-}" ] || die "combine: --verify needs a command"; VERIFY="$2"; shift 2 ;;
|
|
308
|
+
--dry-run) DRY=1; shift ;;
|
|
309
|
+
-*) die "combine: unknown option '$1'" ;;
|
|
310
|
+
*) SRC+=("$1"); shift ;;
|
|
311
|
+
esac
|
|
312
|
+
done
|
|
313
|
+
[ "${#SRC[@]}" -ge 2 ] || die "combine needs at least two source plans to combine (got ${#SRC[@]})" 3
|
|
314
|
+
NEW="$(plans_abs "$NEW")"
|
|
315
|
+
|
|
316
|
+
local norm=() seen=""
|
|
317
|
+
for p in "${SRC[@]}"; do
|
|
318
|
+
[ -d "$p" ] || die "combine: plan dir '$p' does not exist" 3
|
|
319
|
+
p="$(cd "$p" && pwd)"
|
|
320
|
+
[ -f "$p/tasks.tsv" ] || die "combine: '$p' is not a forge plan (no tasks.tsv)" 3
|
|
321
|
+
[ "$p" != "$NEW" ] || die "combine: the new plan cannot be one of its sources" 3
|
|
322
|
+
case "$seen" in *"|$p|"*) die "combine: plan '$p' is listed twice" 3 ;; esac
|
|
323
|
+
seen="$seen|$p|"
|
|
324
|
+
norm+=("$p")
|
|
325
|
+
done
|
|
326
|
+
SRC=("${norm[@]}")
|
|
327
|
+
|
|
328
|
+
if [ "${FORGE_COMBINE_LOCKED:-}" != 1 ]; then
|
|
329
|
+
local locks=()
|
|
330
|
+
for p in "${SRC[@]}"; do locks+=("$p/schedule.lock"); done
|
|
331
|
+
exec python3 -c "$PLANS_LOCK_PY" "$SELF" "${#SRC[@]}" "${locks[@]}" "${orig[@]}"
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# --- every precondition, before anything is created -------------------------------
|
|
335
|
+
local REPO="" repo_i rid wtr acc br wt tip
|
|
336
|
+
local S_RUN=() S_TIP=() S_NAME=()
|
|
337
|
+
for p in "${SRC[@]}"; do
|
|
338
|
+
for f in repo run_id wt_root accepted.integration; do
|
|
339
|
+
[ -s "$p/$f" ] || die "plan '$p' has no $f: it never ran far enough to have integration state to combine" 3
|
|
340
|
+
done
|
|
341
|
+
repo_i="$(cat "$p/repo")"
|
|
342
|
+
[ -d "$repo_i" ] || die "plan '$p': its repository '$repo_i' does not exist" 3
|
|
343
|
+
repo_i="$(cd "$repo_i" && pwd -P)"
|
|
344
|
+
if [ -z "$REPO" ]; then REPO="$repo_i"
|
|
345
|
+
elif [ "$repo_i" != "$REPO" ]; then
|
|
346
|
+
die "plans from different repositories cannot be combined: '${SRC[0]}' is in $REPO but '$p' is in $repo_i" 3
|
|
347
|
+
fi
|
|
348
|
+
rid="$(cat "$p/run_id")"; wtr="$(cat "$p/wt_root")"; acc="$(cat "$p/accepted.integration")"
|
|
349
|
+
br="forge/$rid-integration"; wt="$wtr/_integration"
|
|
350
|
+
tip="$(git -C "$REPO" rev-parse --verify --quiet "refs/heads/$br^{commit}")" \
|
|
351
|
+
|| die "plan '$p': integration branch $br does not exist" 3
|
|
352
|
+
[ "$tip" = "$acc" ] \
|
|
353
|
+
|| die "plan '$p': $br is at $(plans_short "$tip") but the run accepted $(plans_short "$acc"); it changed outside forge, so it is not safe to combine" 3
|
|
354
|
+
[ -e "$wt/.git" ] || die "plan '$p': its integration worktree $wt is missing" 3
|
|
355
|
+
[ "$(git -C "$wt" rev-parse HEAD 2>/dev/null)" = "$tip" ] \
|
|
356
|
+
|| die "plan '$p': its integration worktree $wt is not at the accepted tip" 3
|
|
357
|
+
[ -z "$(git -C "$wt" status --porcelain -- . ':(exclude).forge' 2>/dev/null)" ] \
|
|
358
|
+
|| die "plan '$p': its integration worktree $wt has uncommitted changes; commit or discard them first" 3
|
|
359
|
+
[ -z "$(awk -F'\t' '!/^#/ && NF && NF!=7 {print $1}' "$p/tasks.tsv")" ] \
|
|
360
|
+
|| die "plan '$p': tasks.tsv must have exactly seven columns" 3
|
|
361
|
+
S_RUN+=("$rid"); S_TIP+=("$tip"); S_NAME+=("$(basename "$p")")
|
|
362
|
+
done
|
|
363
|
+
|
|
364
|
+
# The new plan's own names are checked before the sources' contents: they are the
|
|
365
|
+
# cheapest refusal and the one most often caused by a typo.
|
|
366
|
+
local NEWRID NEWWTR NEWBR NEWINT repo_text
|
|
367
|
+
repo_text="$(cat "${SRC[0]}/repo")"
|
|
368
|
+
NEWRID="$(plans_run_id "$NEW")"
|
|
369
|
+
NEWWTR="$(plans_wt_root_for "$repo_text" "$NEWRID")"
|
|
370
|
+
NEWBR="forge/$NEWRID-integration"; NEWINT="$NEWWTR/_integration"
|
|
371
|
+
plans_target_checks "$REPO" "$NEW" "$NEWRID" "$NEWWTR"
|
|
372
|
+
|
|
373
|
+
local owners="" owner
|
|
374
|
+
n="${#SRC[@]}"; i=0
|
|
375
|
+
while [ "$i" -lt "$n" ]; do
|
|
376
|
+
for id in $(all_ids "${SRC[$i]}/tasks.tsv"); do
|
|
377
|
+
owner="$(printf '%s' "$owners" | awk -F'\t' -v id="$id" '$1==id {print $2; exit}')"
|
|
378
|
+
[ -z "$owner" ] || die "duplicate task id '$id': it is in both '$owner' and '${SRC[$i]}'; ids must be unique across the plans being combined" 3
|
|
379
|
+
owners="$owners$id$(printf '\t')${SRC[$i]}
|
|
380
|
+
"
|
|
381
|
+
done
|
|
382
|
+
i=$((i+1))
|
|
383
|
+
done
|
|
384
|
+
i=0
|
|
385
|
+
while [ "$i" -lt "$n" ]; do
|
|
386
|
+
for id in $(all_ids "${SRC[$i]}/tasks.tsv"); do
|
|
387
|
+
for d in $(field "${SRC[$i]}/tasks.tsv" "$id" deps | tr ',' ' '); do
|
|
388
|
+
[ "$d" = - ] && continue
|
|
389
|
+
printf '%s' "$owners" | awk -F'\t' -v d="$d" '$1==d {f=1} END {exit !f}' \
|
|
390
|
+
|| die "plan '${SRC[$i]}': task '$id' depends on '$d', which is in none of the plans being combined" 3
|
|
391
|
+
done
|
|
392
|
+
done
|
|
393
|
+
i=$((i+1))
|
|
394
|
+
done
|
|
395
|
+
|
|
396
|
+
# Only finished work can be combined: a task that never merged has no place in the result
|
|
397
|
+
# and silently dropping it would hide a failure.
|
|
398
|
+
local left="" st
|
|
399
|
+
i=0
|
|
400
|
+
while [ "$i" -lt "$n" ]; do
|
|
401
|
+
for id in $(all_ids "${SRC[$i]}/tasks.tsv"); do
|
|
402
|
+
st="$(task_status "${SRC[$i]}" "$id")"
|
|
403
|
+
[ "$st" = MERGED ] || left="$left
|
|
404
|
+
${S_NAME[$i]}: $id ($st)"
|
|
405
|
+
done
|
|
406
|
+
i=$((i+1))
|
|
407
|
+
done
|
|
408
|
+
[ -z "$left" ] || die "tasks left behind: every task of every source plan must be MERGED before plans are combined.$left
|
|
409
|
+
Retry or accept them (or split them off into their own plan) first." 3
|
|
410
|
+
|
|
411
|
+
# The base the new plan's whole-run review diffs from: the oldest base of any source.
|
|
412
|
+
local bases="" b
|
|
413
|
+
i=0
|
|
414
|
+
while [ "$i" -lt "$n" ]; do
|
|
415
|
+
b="$(cat "${SRC[$i]}/integration.base" 2>/dev/null)"
|
|
416
|
+
[ -n "$b" ] || b="$(git -C "$REPO" merge-base "${S_TIP[$i]}" HEAD 2>/dev/null)"
|
|
417
|
+
[ -z "$b" ] || bases="$bases $b"
|
|
418
|
+
i=$((i+1))
|
|
419
|
+
done
|
|
420
|
+
local newbase=""
|
|
421
|
+
[ -z "$bases" ] || newbase="$(git -C "$REPO" merge-base --octopus $bases 2>/dev/null)"
|
|
422
|
+
|
|
423
|
+
local ntasks=0
|
|
424
|
+
for p in "${SRC[@]}"; do ntasks=$((ntasks + $(all_ids "$p/tasks.tsv" | wc -l | tr -d ' '))); done
|
|
425
|
+
|
|
426
|
+
if [ "$DRY" = 1 ]; then
|
|
427
|
+
echo "combine: would create plan $NEW (run forge/$NEWRID) from $n plans, $ntasks tasks, all MERGED"
|
|
428
|
+
echo " integration branch $NEWBR at $(plans_short "${S_TIP[0]}") (${S_NAME[0]}), worktree $NEWINT"
|
|
429
|
+
i=1
|
|
430
|
+
while [ "$i" -lt "$n" ]; do
|
|
431
|
+
echo " then git merge --no-ff ${S_NAME[$i]} ($(plans_short "${S_TIP[$i]}"))"
|
|
432
|
+
i=$((i+1))
|
|
433
|
+
done
|
|
434
|
+
i=0
|
|
435
|
+
while [ "$i" -lt "$n" ]; do
|
|
436
|
+
local j=$((i+1))
|
|
437
|
+
while [ "$j" -lt "$n" ]; do
|
|
438
|
+
plans_predict_conflict "$REPO" "${S_TIP[$i]}" "${S_TIP[$j]}"
|
|
439
|
+
case "$?" in
|
|
440
|
+
0) echo " merge check: ${S_NAME[$i]} and ${S_NAME[$j]} merge cleanly" ;;
|
|
441
|
+
1) echo " merge check: ${S_NAME[$i]} and ${S_NAME[$j]} WOULD CONFLICT; the real run would stop with exit 6" ;;
|
|
442
|
+
esac
|
|
443
|
+
j=$((j+1))
|
|
444
|
+
done
|
|
445
|
+
i=$((i+1))
|
|
446
|
+
done
|
|
447
|
+
echo " tasks:$(for p in "${SRC[@]}"; do for id in $(all_ids "$p/tasks.tsv"); do printf ' %s' "$id"; done; done)"
|
|
448
|
+
if [ -n "$VERIFY" ]; then echo " verify: $VERIFY"; fi
|
|
449
|
+
echo "(dry run — nothing created or changed)"
|
|
450
|
+
return 0
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
# --- build ----------------------------------------------------------------------
|
|
454
|
+
plans_reset_undo
|
|
455
|
+
PLANS_U_REPO="$REPO"; PLANS_U_PLAN="$NEW"; PLANS_U_WTR="$NEWWTR"
|
|
456
|
+
trap 'plans_undo; exit 130' INT TERM
|
|
457
|
+
if [ -d "$NEW" ]; then PLANS_U_PLAN_NEW=0
|
|
458
|
+
else mkdir -p "$NEW" || { plans_undo; die "could not create '$NEW'" 3; }; PLANS_U_PLAN_NEW=1
|
|
459
|
+
fi
|
|
460
|
+
if [ -d "$NEWWTR" ]; then PLANS_U_WTR_NEW=0
|
|
461
|
+
else mkdir -p "$NEWWTR" || { plans_undo; die "could not create '$NEWWTR'" 3; }; PLANS_U_WTR_NEW=1
|
|
462
|
+
fi
|
|
463
|
+
plans_make_integration "$REPO" "$NEWBR" "${S_TIP[0]}" "$NEWINT" \
|
|
464
|
+
|| { plans_undo; die "could not create $NEWBR and its worktree" 3; }
|
|
465
|
+
|
|
466
|
+
local mlog="$NEW/.merge.log" files merged_names="${S_NAME[0]}"
|
|
467
|
+
i=1
|
|
468
|
+
while [ "$i" -lt "$n" ]; do
|
|
469
|
+
if ! git -C "$NEWINT" -c user.name=forge -c user.email=forge@local \
|
|
470
|
+
merge --no-ff -m "forge: combine ${S_NAME[$i]}" "${S_TIP[$i]}" > "$mlog" 2>&1; then
|
|
471
|
+
files="$(git -C "$NEWINT" diff --name-only --diff-filter=U 2>/dev/null | tr '\n' ' ')"
|
|
472
|
+
git -C "$NEWINT" merge --abort >/dev/null 2>&1
|
|
473
|
+
local why; why="$(tail -3 "$mlog" 2>/dev/null | tr '\n' ' ')"
|
|
474
|
+
# Recorded before the rollback deletes the new plan: two plans whose work collides
|
|
475
|
+
# is a coupling label nothing else keeps.
|
|
476
|
+
forge_dataset_event combine_conflict --dir "${SRC[0]}" --set new_plan="$NEW" \
|
|
477
|
+
--set plans="${SRC[*]}" --set failed="${S_NAME[$i]}" --set into="$merged_names" --set conflicts="$files"
|
|
478
|
+
plans_undo
|
|
479
|
+
trap - INT TERM
|
|
480
|
+
if [ -n "$files" ]; then
|
|
481
|
+
die "plans conflict: '${S_NAME[$i]}' cannot be merged with $merged_names (conflicting files: $files). Nothing was created; split the overlapping work into one plan or run it in sequence." 6
|
|
482
|
+
fi
|
|
483
|
+
die "merging '${S_NAME[$i]}' into $merged_names failed: $why Nothing was created." 6
|
|
484
|
+
fi
|
|
485
|
+
merged_names="$merged_names + ${S_NAME[$i]}"
|
|
486
|
+
i=$((i+1))
|
|
487
|
+
done
|
|
488
|
+
rm -f "$mlog"
|
|
489
|
+
|
|
490
|
+
local ok=1 tsv="$NEW/tasks.tsv"
|
|
491
|
+
{
|
|
492
|
+
printf '# id\tdeps\tdifficulty\tfiles\tdwarf\tqa\ttitle\n'
|
|
493
|
+
for p in "${SRC[@]}"; do awk -F'\t' '!/^#/ && NF' "$p/tasks.tsv"; done
|
|
494
|
+
} > "$tsv" || ok=0
|
|
495
|
+
printf '%s\n' "$repo_text" > "$NEW/repo" || ok=0
|
|
496
|
+
printf '%s\n' "$NEWRID" > "$NEW/run_id" || ok=0
|
|
497
|
+
printf '%s\n' "$NEWWTR" > "$NEW/wt_root" || ok=0
|
|
498
|
+
git -C "$NEWINT" rev-parse HEAD > "$NEW/accepted.integration" || ok=0
|
|
499
|
+
[ -z "$newbase" ] || printf '%s\n' "$newbase" > "$NEW/integration.base"
|
|
500
|
+
for p in "${SRC[@]}"; do
|
|
501
|
+
for id in $(all_ids "$p/tasks.tsv"); do
|
|
502
|
+
plans_copy_taskdir "$p/tasks/$id" "$NEW/tasks/$id" || ok=0
|
|
503
|
+
done
|
|
504
|
+
done
|
|
505
|
+
[ "$ok" = 1 ] || { plans_undo; trap - INT TERM; die "could not write the combined plan" 3; }
|
|
506
|
+
|
|
507
|
+
# Options: a plan-wide choice carries over when every source made it identically; the
|
|
508
|
+
# two permission flags only when every source granted them.
|
|
509
|
+
local goals="" g
|
|
510
|
+
for p in "${SRC[@]}"; do
|
|
511
|
+
if [ -s "$p/goal.txt" ]; then g="$(tr '\n' ' ' < "$p/goal.txt" | sed 's/ *$//')"; else g="$(basename "$p")"; fi
|
|
512
|
+
if [ -z "$goals" ]; then goals="$g"; else goals="$goals + $g"; fi
|
|
513
|
+
done
|
|
514
|
+
printf '%s\n' "$goals" > "$NEW/goal.txt"
|
|
515
|
+
for f in no_ripwire no_memory; do
|
|
516
|
+
for p in "${SRC[@]}"; do [ -f "$p/$f" ] && { : > "$NEW/$f"; break; }; done
|
|
517
|
+
done
|
|
518
|
+
local all same
|
|
519
|
+
for f in yolo_dwarf yolo_qa; do
|
|
520
|
+
all=1; for p in "${SRC[@]}"; do [ -f "$p/$f" ] || all=0; done
|
|
521
|
+
[ "$all" = 1 ] && : > "$NEW/$f"
|
|
522
|
+
done
|
|
523
|
+
for f in qa_threshold timeout verify_retries; do
|
|
524
|
+
same=1
|
|
525
|
+
for p in "${SRC[@]}"; do
|
|
526
|
+
if [ ! -f "$p/$f" ] || ! cmp -s "$p/$f" "${SRC[0]}/$f"; then same=0; fi
|
|
527
|
+
done
|
|
528
|
+
if [ "$same" = 1 ]; then cp -p "${SRC[0]}/$f" "$NEW/$f"
|
|
529
|
+
else
|
|
530
|
+
for p in "${SRC[@]}"; do
|
|
531
|
+
if [ -f "$p/$f" ]; then note "the plans do not agree on $f; the combined plan leaves it unset"; break; fi
|
|
532
|
+
done
|
|
533
|
+
fi
|
|
534
|
+
done
|
|
535
|
+
# The integration worktree needs a setup step to build and test the combined result, and
|
|
536
|
+
# no task is ever dispatched here, so the first source's is as good as any.
|
|
537
|
+
for p in "${SRC[@]}"; do [ -s "$p/setup_cmd" ] && { cp -p "$p/setup_cmd" "$NEW/setup_cmd"; break; }; done
|
|
538
|
+
if [ -n "$VERIFY" ]; then printf '%s' "$VERIFY" > "$NEW/verify_cmd"
|
|
539
|
+
else
|
|
540
|
+
for p in "${SRC[@]}"; do [ -s "$p/verify_cmd" ] && { cp -p "$p/verify_cmd" "$NEW/verify_cmd"; break; }; done
|
|
541
|
+
fi
|
|
542
|
+
for f in fractal-routing.json jev-selection.json; do
|
|
543
|
+
for p in "${SRC[@]}"; do [ -f "$p/$f" ] && { cp -p "$p/$f" "$NEW/$f"; break; }; done
|
|
544
|
+
done
|
|
545
|
+
{
|
|
546
|
+
i=0
|
|
547
|
+
while [ "$i" -lt "$n" ]; do
|
|
548
|
+
printf '%s\t%s\t%s\n' "${SRC[$i]}" "${S_RUN[$i]}" "${S_TIP[$i]}"
|
|
549
|
+
i=$((i+1))
|
|
550
|
+
done
|
|
551
|
+
} > "$NEW/combined-from.txt"
|
|
552
|
+
|
|
553
|
+
compute_waves "$NEW"
|
|
554
|
+
write_results "$NEW"
|
|
555
|
+
trap - INT TERM
|
|
556
|
+
# The integration worktree validates the combined result, so it needs what a task
|
|
557
|
+
# worktree gets: whatever setup the repository asks for. Never fatal.
|
|
558
|
+
mkdir -p "$NEW/tasks/_integration"
|
|
559
|
+
run_worktree_setup "$NEW" "$repo_text" "$NEWINT" "$NEW/tasks/_integration" _integration
|
|
560
|
+
|
|
561
|
+
echo "combined $n plans into $NEW (run forge/$NEWRID): $ntasks tasks, all MERGED"
|
|
562
|
+
echo "integration branch: $NEWBR at $(plans_short "$(cat "$NEW/accepted.integration")")"
|
|
563
|
+
i=0
|
|
564
|
+
while [ "$i" -lt "$n" ]; do
|
|
565
|
+
echo " from ${S_NAME[$i]}: $(all_ids "${SRC[$i]}/tasks.tsv" | tr '\n' ' ')"
|
|
566
|
+
i=$((i+1))
|
|
567
|
+
done
|
|
568
|
+
echo "task branches keep their original names (forge/<original run>/<task>); results.tsv shows the new run id."
|
|
569
|
+
echo "next: forge-parallel.sh run $NEW (verification only; no model is called)"
|
|
570
|
+
echo " forge-parallel.sh integrate $NEW --approved"
|
|
571
|
+
return 0
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# --- split ----------------------------------------------------------------------
|
|
575
|
+
# Moves some tasks of a plan, with their branches, worktrees and saved review state, into
|
|
576
|
+
# a new plan that can run on its own. Copy first, delete last: until the new plan is fully
|
|
577
|
+
# written the source is untouched, and any failure on the way puts the moved worktrees and
|
|
578
|
+
# branches back.
|
|
579
|
+
do_split() {
|
|
580
|
+
local PLAN="${1:?split needs a plan dir}" NEW="" LIST="" DRY=0 id f PICKED
|
|
581
|
+
shift
|
|
582
|
+
[ $# -ge 1 ] && [ "${1#-}" = "$1" ] || die "usage: forge-parallel.sh split <plan-dir> <new-plan> --tasks <id,id> [--dry-run]"
|
|
583
|
+
NEW="$1"; shift
|
|
584
|
+
while [ $# -gt 0 ]; do
|
|
585
|
+
case "$1" in
|
|
586
|
+
--tasks) [ -n "${2:-}" ] || die "split: --tasks needs a comma-separated list of task ids"; LIST="$2"; shift 2 ;;
|
|
587
|
+
--dry-run) DRY=1; shift ;;
|
|
588
|
+
*) die "split: unknown option '$1'" ;;
|
|
589
|
+
esac
|
|
590
|
+
done
|
|
591
|
+
[ -n "$LIST" ] || die "split needs --tasks <id,id>: the tasks to move into the new plan"
|
|
592
|
+
NEW="$(plans_abs "$NEW")"
|
|
593
|
+
[ "$NEW" != "$PLAN" ] || die "split: the new plan cannot be the source plan" 3
|
|
594
|
+
|
|
595
|
+
local T="$PLAN/tasks.tsv" SEL=" "
|
|
596
|
+
[ -s "$T" ] || die "no tasks.tsv in '$PLAN'" 3
|
|
597
|
+
for id in $(printf '%s' "$LIST" | tr ',' ' '); do
|
|
598
|
+
all_ids "$T" | grep -qx "$id" || die "task '$id' is not in this plan" 3
|
|
599
|
+
case "$SEL" in *" $id "*) ;; *) SEL="$SEL$id " ;; esac
|
|
600
|
+
done
|
|
601
|
+
PICKED="$(printf '%s' "$SEL" | sed 's/^ *//; s/ *$//')"
|
|
602
|
+
|
|
603
|
+
# Fractal binds a task's state to hashed output paths under the plan directory; moving
|
|
604
|
+
# the task out would orphan it, and no honest way to re-bind it exists.
|
|
605
|
+
if [ -s "$PLAN/fractal-selection.json" ]; then
|
|
606
|
+
python3 -c 'import json, sys
|
|
607
|
+
try:
|
|
608
|
+
sys.exit(0 if json.load(open(sys.argv[1])).get("enabled") is False else 1)
|
|
609
|
+
except Exception:
|
|
610
|
+
sys.exit(1)' "$PLAN/fractal-selection.json" \
|
|
611
|
+
|| die "this plan runs under Fractal, which binds each task to output paths under the plan directory; a Fractal-backed plan cannot be split" 3
|
|
612
|
+
fi
|
|
613
|
+
for f in repo run_id wt_root accepted.integration; do
|
|
614
|
+
[ -s "$PLAN/$f" ] || die "plan has no $f: it has not run far enough to split ('run' it first)" 3
|
|
615
|
+
done
|
|
616
|
+
local REPO repo_text run_id old_wtr acc
|
|
617
|
+
repo_text="$(cat "$PLAN/repo")"; run_id="$(cat "$PLAN/run_id")"
|
|
618
|
+
old_wtr="$(cat "$PLAN/wt_root")"; acc="$(cat "$PLAN/accepted.integration")"
|
|
619
|
+
[ -d "$repo_text" ] || die "the plan's repository '$repo_text' does not exist" 3
|
|
620
|
+
REPO="$(cd "$repo_text" && pwd -P)"
|
|
621
|
+
git -C "$REPO" rev-parse --verify --quiet "$acc^{commit}" >/dev/null \
|
|
622
|
+
|| die "the accepted integration commit $acc is not in $REPO" 3
|
|
623
|
+
|
|
624
|
+
# Per selected task: what it is, and whether it may move.
|
|
625
|
+
local st dep dst others=0
|
|
626
|
+
for id in $(all_ids "$T"); do
|
|
627
|
+
case "$SEL" in *" $id "*) ;; *) others=$((others+1)) ;; esac
|
|
628
|
+
done
|
|
629
|
+
for id in $(all_ids "$T"); do
|
|
630
|
+
case "$SEL" in *" $id "*) ;; *) continue ;; esac
|
|
631
|
+
st="$(task_status "$PLAN" "$id")"
|
|
632
|
+
case "$st" in
|
|
633
|
+
MERGED) die "task '$id' is MERGED: its work is already in the integration branch and cannot move to another plan" 3 ;;
|
|
634
|
+
RUNNING) die "task '$id' is RUNNING (or a run was killed); 'forge-parallel.sh run $PLAN' settles it first" 3 ;;
|
|
635
|
+
esac
|
|
636
|
+
done
|
|
637
|
+
# Closed under dependents: a task left behind may not depend on a task that moves away.
|
|
638
|
+
for id in $(all_ids "$T"); do
|
|
639
|
+
case "$SEL" in *" $id "*) continue ;; esac
|
|
640
|
+
for dep in $(field "$T" "$id" deps | tr ',' ' '); do
|
|
641
|
+
case "$SEL" in *" $dep "*) die "task '$id' stays behind but depends on '$dep', which is being moved; include '$id' in --tasks (or leave '$dep' too)" 3 ;; esac
|
|
642
|
+
done
|
|
643
|
+
done
|
|
644
|
+
# A moved task may depend on merged work (the new integration branch already has it),
|
|
645
|
+
# but not on a task that is neither moving with it nor merged.
|
|
646
|
+
for id in $(all_ids "$T"); do
|
|
647
|
+
case "$SEL" in *" $id "*) ;; *) continue ;; esac
|
|
648
|
+
for dep in $(field "$T" "$id" deps | tr ',' ' '); do
|
|
649
|
+
[ "$dep" = - ] && continue
|
|
650
|
+
case "$SEL" in *" $dep "*) continue ;; esac
|
|
651
|
+
dst="$(task_status "$PLAN" "$dep")"
|
|
652
|
+
[ "$dst" = MERGED ] || die "task '$id' depends on '$dep', which is not merged (status $dst) and is not being moved; move '$dep' too or finish it first" 3
|
|
653
|
+
done
|
|
654
|
+
done
|
|
655
|
+
local STAYS="" # built in a loop: bash 3.2 cannot parse a `case` inside $( )
|
|
656
|
+
for id in $(all_ids "$T"); do
|
|
657
|
+
case "$SEL" in *" $id "*) ;; *) STAYS="$STAYS $id" ;; esac
|
|
658
|
+
done
|
|
659
|
+
[ "$others" -gt 0 ] || die "that selection is every task in the plan: nothing would stay behind. Choose a subset, or just use a new plan directory" 3
|
|
660
|
+
|
|
661
|
+
local NEWRID NEWWTR NEWBR NEWINT
|
|
662
|
+
NEWRID="$(plans_run_id "$NEW")"
|
|
663
|
+
NEWWTR="$(plans_wt_root_for "$repo_text" "$NEWRID")"
|
|
664
|
+
NEWBR="forge/$NEWRID-integration"; NEWINT="$NEWWTR/_integration"
|
|
665
|
+
plans_target_checks "$REPO" "$NEW" "$NEWRID" "$NEWWTR"
|
|
666
|
+
|
|
667
|
+
if [ "$DRY" = 1 ]; then
|
|
668
|
+
echo "split: would move $PICKED from $PLAN to a new plan $NEW (run forge/$NEWRID)"
|
|
669
|
+
echo " integration branch $NEWBR at $(plans_short "$acc"), worktree $NEWINT"
|
|
670
|
+
for id in $(all_ids "$T"); do
|
|
671
|
+
case "$SEL" in *" $id "*) ;; *) continue ;; esac
|
|
672
|
+
echo " $id ($(task_status "$PLAN" "$id")): branch forge/$run_id/$id -> forge/$NEWRID/$id; worktree $old_wtr/$id -> $NEWWTR/$id"
|
|
673
|
+
done
|
|
674
|
+
echo " stays in $PLAN:$STAYS"
|
|
675
|
+
echo "(dry run — nothing created or changed)"
|
|
676
|
+
return 0
|
|
677
|
+
fi
|
|
678
|
+
|
|
679
|
+
# --- copy ------------------------------------------------------------------------
|
|
680
|
+
plans_reset_undo
|
|
681
|
+
PLANS_U_REPO="$REPO"; PLANS_U_PLAN="$NEW"; PLANS_U_WTR="$NEWWTR"
|
|
682
|
+
trap 'plans_undo; exit 130' INT TERM
|
|
683
|
+
git -C "$REPO" worktree prune >/dev/null 2>&1
|
|
684
|
+
if [ -d "$NEW" ]; then PLANS_U_PLAN_NEW=0
|
|
685
|
+
else mkdir -p "$NEW" || { plans_undo; die "could not create '$NEW'" 3; }; PLANS_U_PLAN_NEW=1
|
|
686
|
+
fi
|
|
687
|
+
if [ -d "$NEWWTR" ]; then PLANS_U_WTR_NEW=0
|
|
688
|
+
else mkdir -p "$NEWWTR" || { plans_undo; die "could not create '$NEWWTR'" 3; }; PLANS_U_WTR_NEW=1
|
|
689
|
+
fi
|
|
690
|
+
plans_make_integration "$REPO" "$NEWBR" "$acc" "$NEWINT" \
|
|
691
|
+
|| { plans_undo; trap - INT TERM; die "could not create $NEWBR and its worktree" 3; }
|
|
692
|
+
|
|
693
|
+
local oldbr newbr oldwt newwt head_ref old_branches=""
|
|
694
|
+
for id in $(all_ids "$T"); do
|
|
695
|
+
case "$SEL" in *" $id "*) ;; *) continue ;; esac
|
|
696
|
+
oldbr="forge/$run_id/$id"; newbr="forge/$NEWRID/$id"
|
|
697
|
+
oldwt="$old_wtr/$id"; newwt="$NEWWTR/$id"
|
|
698
|
+
git -C "$REPO" show-ref --verify --quiet "refs/heads/$oldbr" || continue # never ran
|
|
699
|
+
git -C "$REPO" branch "$newbr" "$oldbr" >/dev/null 2>&1 \
|
|
700
|
+
|| { plans_undo; trap - INT TERM; die "could not create branch $newbr" 3; }
|
|
701
|
+
plans_journal branch "$newbr" ""
|
|
702
|
+
old_branches="$old_branches $oldbr"
|
|
703
|
+
if [ -e "$oldwt/.git" ]; then
|
|
704
|
+
head_ref="$(git -C "$oldwt" symbolic-ref -q HEAD 2>/dev/null)"
|
|
705
|
+
git -C "$REPO" worktree move "$oldwt" "$newwt" >/dev/null 2>&1 \
|
|
706
|
+
|| { plans_undo; trap - INT TERM; die "could not move worktree $oldwt to $newwt" 3; }
|
|
707
|
+
plans_journal move "$oldwt" "$newwt"
|
|
708
|
+
# Same commit, same index, same working tree: only the name HEAD points at changes.
|
|
709
|
+
if [ "$head_ref" = "refs/heads/$oldbr" ]; then
|
|
710
|
+
git -C "$newwt" symbolic-ref HEAD "refs/heads/$newbr" >/dev/null 2>&1 \
|
|
711
|
+
|| { plans_undo; trap - INT TERM; die "could not switch $newwt to branch $newbr" 3; }
|
|
712
|
+
plans_journal head "$newwt" "$oldbr"
|
|
713
|
+
fi
|
|
714
|
+
fi
|
|
715
|
+
done
|
|
716
|
+
|
|
717
|
+
# --- write the new plan -----------------------------------------------------------
|
|
718
|
+
local ok=1
|
|
719
|
+
{
|
|
720
|
+
printf '# id\tdeps\tdifficulty\tfiles\tdwarf\tqa\ttitle\n'
|
|
721
|
+
# Dependencies on merged tasks are satisfied by the new integration branch and are
|
|
722
|
+
# dropped; dependencies between moved tasks are kept.
|
|
723
|
+
awk -F'\t' -v OFS='\t' -v ids="$SEL" '
|
|
724
|
+
/^#/ || !NF { next }
|
|
725
|
+
index(ids, " " $1 " ") {
|
|
726
|
+
n = split($2, d, ","); out = ""
|
|
727
|
+
for (i = 1; i <= n; i++)
|
|
728
|
+
if (d[i] != "" && d[i] != "-" && index(ids, " " d[i] " "))
|
|
729
|
+
out = out (out == "" ? "" : ",") d[i]
|
|
730
|
+
$2 = (out == "" ? "-" : out); print
|
|
731
|
+
}' "$T"
|
|
732
|
+
} > "$NEW/tasks.tsv" || ok=0
|
|
733
|
+
printf '%s\n' "$repo_text" > "$NEW/repo" || ok=0
|
|
734
|
+
printf '%s\n' "$NEWRID" > "$NEW/run_id" || ok=0
|
|
735
|
+
printf '%s\n' "$NEWWTR" > "$NEW/wt_root" || ok=0
|
|
736
|
+
printf '%s\n' "$acc" > "$NEW/accepted.integration" || ok=0
|
|
737
|
+
[ ! -s "$PLAN/integration.base" ] || cp -p "$PLAN/integration.base" "$NEW/integration.base" || ok=0
|
|
738
|
+
[ ! -s "$PLAN/goal.txt" ] || cp -p "$PLAN/goal.txt" "$NEW/goal.txt" || ok=0
|
|
739
|
+
for f in $PLANS_OPTION_FILES; do
|
|
740
|
+
[ ! -e "$PLAN/$f" ] || cp -p "$PLAN/$f" "$NEW/$f" || ok=0
|
|
741
|
+
done
|
|
742
|
+
for id in $(all_ids "$T"); do
|
|
743
|
+
case "$SEL" in *" $id "*) ;; *) continue ;; esac
|
|
744
|
+
plans_copy_taskdir "$PLAN/tasks/$id" "$NEW/tasks/$id" || ok=0
|
|
745
|
+
done
|
|
746
|
+
{ echo "split from $PLAN"; echo "run_id $run_id"; echo "tasks $PICKED"; } > "$NEW/split-from.txt" || ok=0
|
|
747
|
+
[ "$ok" = 1 ] || { plans_undo; trap - INT TERM; die "could not write the new plan" 3; }
|
|
748
|
+
compute_waves "$NEW"
|
|
749
|
+
write_results "$NEW"
|
|
750
|
+
|
|
751
|
+
# --- delete ----------------------------------------------------------------------
|
|
752
|
+
# Only now does the source lose anything. From here an interrupt must NOT undo the move:
|
|
753
|
+
# the new plan is complete, and undoing it would leave the source without its rows.
|
|
754
|
+
trap - INT TERM
|
|
755
|
+
local srctmp="$PLAN/.tasks.tsv.split" marker="$PLAN/split-$(basename "$NEW").txt"
|
|
756
|
+
if ! awk -F'\t' -v ids="$SEL" '/^#/ || !NF {print; next} index(ids, " " $1 " ") {next} {print}' "$T" > "$srctmp" \
|
|
757
|
+
|| ! mv "$srctmp" "$T"; then
|
|
758
|
+
rm -f "$srctmp"; plans_undo; trap - INT TERM
|
|
759
|
+
die "could not update '$T'; nothing was moved" 3
|
|
760
|
+
fi
|
|
761
|
+
{
|
|
762
|
+
echo "moved to $NEW (run forge/$NEWRID) on $(date '+%Y-%m-%dT%H:%M:%S%z')"
|
|
763
|
+
for id in $PICKED; do
|
|
764
|
+
printf '%s\t%s\n' "$id" "forge/$run_id/$id -> forge/$NEWRID/$id"
|
|
765
|
+
done
|
|
766
|
+
} > "$marker"
|
|
767
|
+
for id in $PICKED; do rm -rf "$PLAN/tasks/$id"; done
|
|
768
|
+
compute_waves "$PLAN"
|
|
769
|
+
write_results "$PLAN"
|
|
770
|
+
for oldbr in $old_branches; do
|
|
771
|
+
git -C "$REPO" branch -D "$oldbr" >/dev/null 2>&1 || note "could not delete the old branch $oldbr; it is harmless, delete it by hand"
|
|
772
|
+
done
|
|
773
|
+
mkdir -p "$NEW/tasks/_integration"
|
|
774
|
+
run_worktree_setup "$NEW" "$repo_text" "$NEWINT" "$NEW/tasks/_integration" _integration
|
|
775
|
+
|
|
776
|
+
echo "split: moved $PICKED from $PLAN to $NEW (run forge/$NEWRID)"
|
|
777
|
+
echo "integration branch: $NEWBR at $(plans_short "$acc")"
|
|
778
|
+
for id in $PICKED; do
|
|
779
|
+
echo " $id ($(task_status "$NEW" "$id")): forge/$NEWRID/$id worktree $NEWWTR/$id"
|
|
780
|
+
done
|
|
781
|
+
echo "stays in $PLAN: $(all_ids "$T" | tr '\n' ' ')"
|
|
782
|
+
echo "next: forge-parallel.sh run $NEW [--retry-failed 1] or forge-parallel.sh retry $NEW <task-id>"
|
|
783
|
+
echo " the two plans have separate locks and integration branches; 'combine' joins them again"
|
|
784
|
+
return 0
|
|
785
|
+
}
|